DLL Symbol Visibility in Nim - Hiding NimMain

DLL Symbol Visibility in Nim - Hiding NimMain

Developing DLLs with Nim for Windows presents unique challenges, especially regarding symbol visibility. A common issue is the exposure of NimMain, an automatic inclusion in Nim-compiled DLLs that is essential for initializing the runtime but not intended for public use. Its consistent presence across DLLs creates a recognizable signature, which can be problematic for various use cases. Additionally, managing the visibility of other symbols, such as ensuring certain functions remain private, demands careful attention. This guide covers the use of a .def file in conjunction with Nim's pragmas to conceal NimMain and selectively expose or hide other functions.

Challenge: Hiding NimMain and Specific Functions

NimMain serves as a clear indicator of a DLL's origins from Nim, which might not always be desirable. Furthermore, it is important to control which functions are exposed outside the DLL.

Solution: Employing a .def File

To address these challenges, we use a .def file:

EXPORTS
      NimMain NONAME PRIVATE

This configuration ensures NimMain is kept private, effectively removing it from the DLL's export table and mitigating its use as a Nim signature.

A .def file is typically used to define all public symbols/functions. However, in this case, we will use it to hide NimMain and manually control the visibility of other functions in our library using {.exportc, dynlib.}.

Nim Code Adjustments

Adjustments in the Nim code involve using {.exportc, dynlib.} pragmas to mark functions for export, similar to __declspec(dllexport) in C/C++. Additionally, ensure that functions meant to be private, like subtract, do not carry these pragmas:

import winim/lean

{.passl: "dll.def".}

proc NimMain() {.cdecl, importc.}

# Exporting the 'add' function
proc add*(a, b: int): int {.exportc, dynlib.} = a + b

# Keeping 'subtract' private
proc subtract(a, b: int): int = a - b

# Custom DllMain for Nim runtime initialization
proc DllMain(hinstDLL: HINSTANCE, fdwReason: DWORD, lpReserved: LPVOID): BOOL {.stdcall, exportc, dynlib.} =
  if fdwReason == DLL_PROCESS_ATTACH: NimMain()
  return TRUE

In this setup, add is explicitly marked for export, making it publicly available, while subtract remains internal to the DLL.

Compilation Strategy

Compile the DLL using the following command to include the .def file, ensuring that NimMain and unwanted symbols like subtract remain hidden:

nim c -d=release -d=strip --opt=size --mm=orc --threads=on --app:lib --nomain --cpu:amd64 --out=app.dll dll.nim

In this approach, the DLL interface is designed to be clean and focused, with NimMain concealed in order to obscure the DLL's origin in Nim. This selective exposure of functions ensures that only the relevant ones are visible to the user.

Conclusion

As demonstrated, the NimMain function is no longer visible in the DLL, even though it is technically still exported. The function has been marked as invisible to maintain the desired level of obscurity.

When you try to examine the invisible symbols within the DLL, you will find that the function name is no longer NimMain; it is randomly Ordinal2. This subtle change further reinforces the concealment of the DLL's Nim origin, making it more difficult for users to identify its true source.

Using a .def file and Nim's {.exportc, dynlib.} pragmas provides precise control over DLL symbol visibility in Nim, similar to __declspec(dllexport) in C/C++. This method ensures NimMain stays hidden, addressing the issue of its presence as a signature in Nim-compiled DLLs. This maintains a minimal and secure interface for your DLLs.