In some SDK integrations, some platforms provide only the DLL without an import library for us to use. In this case, we can only use code to load the DLL to call functions within it. This article documents two usage methods and analyzes their pros and cons.
First, let’s generate a test DLL:
1 | // max_dll.h |
The reason for using extern "C"
is that C++’s name mangling
is implementation-defined
. Simply put, the C++ ABI is unstable, and using C linkage can avoid issues with inconsistent symbol names due to different implementations of name mangling
.
To generate the DLL (for more details, check my previous article—Analysis of C/C++ Compilation and Linking Models):
1 | $ g++ max_dll.cpp -shared -o max_dll.dll |
Loading DLL at Runtime
Next, how can we call the max
function in this DLL from the external code?
Due to the compilation and linking, since there is no import library for this DLL, we cannot specify the import library at link time and directly use the functions in the DLL. However, we can dynamically load the DLL file. On Windows, we can achieve this using the combination of LoadLibrary and GetProcAddress.
Here, LoadLibrary
is responsible for loading the DLL file, GetProcAddress
retrieves the function pointer of the symbol from the DLL, and FreeLibrary
releases the handle of the DLL file:
Its prototype is as follows:
1 |
|
An example of loading a DLL is as follows:
1 |
|
Note: If the max_dll.cpp
from above was not linked using extern "C"
, then the code above will fail to obtain the function pointer at this line:
1 | dll_max max_func = (dll_max)GetProcAddress(hdll, "max"); |
This is because according to C++’s name mangling
rules, the function max
within the DLL has a symbol name that is not max
, but something like the following (the reason for saying “something like” is due to the lack of standardization and reliance on implementation):
We can view the symbol information in the target file through nm
(for the non-extern “C” version of the DLL), while hiding other symbols:
1 | # Non-extern "C" version |
Modifying the erroneous code above to the following will allow successful retrieval of the function pointer (but this method is indeed cumbersome):
1 | dll_max max_func = (dll_max)GetProcAddress(hdll, "_Z3maxii"); |
With the extern "C"
version of the DLL, the symbol information is as follows (since C linkage does not alter any symbols):
1 | # extern "C" version |
Note: On Linux, the counterparts to
LoadLibrary
/GetProcAddress
/FreeLibrary
(where Linux dynamic libraries are.so
) aredlopen
/dlsym
/dlclose
, with their prototypes as follows:
1 |
|
Using DLL’s Import Library
If we have an import library, we can link it as if it were a static link, without needing to load the DLL at runtime and retrieve function pointers:
1 | # Create DLL and generate an import library lib |
-Wl
is an argument passed to the linker, and --out-implib
generates the import library.
Then we can link the symbols from the DLL as if we were using static linking:
1 | // call_max.cpp |
Then we compile, but without linking -c
, only generating the object file call_max.o
:
1 | $ g++ -c call_max.cpp -o call_max.o |
Then include imp_max_dll.lib
in the link:
1 | $ g++ call_max.o imp_max_dll.lib -o call_max.exe |
No undefined symbol errors.
Now to run it:
1 | $ ./call_max.exe |
We check the dynamic libraries that call_max.exe
depends on using the ldd
command:
1 | $ ldd call_max.exe |
We can see that call_max.exe
depends on the max_dll.dll
in the current directory.
What if we delete max_dll.dll
? Let’s try after deletion:
1 | $ ./call_max.exe |
An error indicating that the file cannot be found will be shown.
Dynamically loading DLL at runtime and using DLL’s import library both have their pros and cons. To be continued, I will analyze further when I have time.