Use of dynamic link libraries: loading and linking

动态链接库的使用:加载和链接

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// max_dll.h
#ifndef MAX_DLL_H_
#define MAX_DLL_H_

extern "C" int max(int,int);
typedef int (*dll_max)(int,int);

#endif

// max_dll.cpp
#include "max_dll.h"

extern "C" int max(int lhs, int rhs)
{
return lhs > rhs ? lhs : rhs;
}

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
2
3
$ g++ max_dll.cpp -shared -o max_dll.dll
$ ls
max_dll.cpp max_dll.dll* max_dll.h

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <Windows.h>
// Loads the specified module into the address space of the calling process. The specified module may cause other modules to be loaded.
// Return Value
// If the function succeeds, the return value is a handle to the module.
// If the function fails, the return value is NULL. To get extended error information, call GetLastError.
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);
// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
// Return Value
// If the function succeeds, the return value is the address of the exported function or variable.
// If the function fails, the return value is NULL. To get extended error information, call GetLastError.
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
// Frees the loaded dynamic-link library (DLL) module and, if necessary, decrements its reference count. When the reference count reaches zero, the module is unloaded from the address space of the calling process and the handle is no longer valid.
BOOL FreeLibrary(
HMODULE hLibModule
);

An example of loading a DLL is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "max_dll.h"
#include <cstdio>
#include <tchar.h>
#include <Windows.h>

#define DLL_FILE _T("max_dll.dll")

int main()
{
HINSTANCE hdll = LoadLibrary(DLL_FILE);
if (hdll != NULL)
{
dll_max max_func = (dll_max)GetProcAddress(hdll, "max");
if (max_func != NULL)
{
std::printf("%d\n", max_func(123, 456));
FreeLibrary(hdll);
}
else
{
std::printf("From DLL Get function Address is failed.\n");
}
}
else
{
std::printf("LoadLibrary is failed.\n");
}
return 0;
}

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
2
3
# Non-extern "C" version
$ nm max_dll.dll
00000000660814b0 T _Z3maxii

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
2
3
# extern "C" version
$ nm max_dll.dll
00000000660814b0 T max

Note: On Linux, the counterparts to LoadLibrary/GetProcAddress/FreeLibrary (where Linux dynamic libraries are .so) are dlopen/dlsym/dlclose, with their prototypes as follows:

1
2
3
4
5
6
7
8
#include <dlfcn.h>

// dlopen, returns library handle on success, or NULL on error
void *dlopen(const char *libfilename, int flags);
// dlsym, returns Address of symbol, or NULL if symbol is not found.
void *dlsym(void *handler, char *symbol);
// dlclose, returns 0 on success, or -1 on error
int dlclose(void *handler);

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
2
# Create DLL and generate an import library lib
g++ max_dll.cpp -shared -o max_dll.dll -Wl,--out-implib,imp_max_dll.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
2
3
4
5
6
7
8
9
// call_max.cpp
#include <cstdio>
#include "max_dll.h"

int main(void)
{
std::printf("%d\n", max(123, 456));
return 0;
}

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
2
$ ./call_max.exe
456

We check the dynamic libraries that call_max.exe depends on using the ldd command:

1
2
3
4
5
6
$ ldd call_max.exe
ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ffa14b80000)
KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ffa12b00000)
KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ffa11d90000)
msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ffa128a0000)
max_dll.dll => /c/Users/visionsmile/Desktop/code/max_dll.dll (0x66080000)

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
2
$ ./call_max.exe
call_max.exe: error while loading shared libraries: max_dll.dll: cannot open shared object file: No such file or directory

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.

Further Reading

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:Use of dynamic link libraries: loading and linking
Author:LIPENGZHA
Publish Date:2018/10/15 20:50
Word Count:2.8k Words
Link:https://en.imzlp.com/posts/18949/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!