1 
2 //          Copyright 2018 - 2021 Michael D. Parker
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 /// Cross-platform interface to system APIs for manually loading C libraries.
8 module bindbc.loader.sharedlib;
9 
10 import core.stdc.stdlib;
11 import core.stdc.string;
12 
13 /// Handle to a shared library
14 struct SharedLib {
15     private void* _handle;
16 }
17 
18 /// Indicates an uninitialized or unassigned handle.
19 enum invalidHandle = SharedLib.init;
20 
21 /// Holds information about failures in loading shared libraries and their symbols.
22 struct ErrorInfo {
23 private:
24     char* _error;
25     char* _message;
26 
27 public @nogc nothrow @property:
28     /**
29         Returns the string "Missing Symbol" to indicate a symbol load failure, and
30         the name of a library to indicate a library load failure.
31     */
32     const(char)* error() return const { return _error; }
33 
34     /**
35         Returns a symbol name for symbol load failures, and a system-specific error
36         message for library load failures.
37     */
38     const(char)* message() return const { return _message; }
39 }
40 
41 private {
42     ErrorInfo[] _errors;
43     size_t _errorCount;
44 }
45 
46 @nogc nothrow:
47 
48 /**
49     Returns a slice containing all `ErrorInfo` instances that have been accumulated by the
50     `load` and `bindSymbol` functions since the last call to `resetErrors`.
51 */
52 const(ErrorInfo)[] errors()
53 {
54     return _errors[0 .. _errorCount];
55 }
56 
57 /**
58     Returns the total number of `ErrorInfo` instances that have been accumulated by the
59     `load` and `bindSymbol` functions since the last call to `resetErrors`.
60 */
61 size_t errorCount()
62 {
63     return _errorCount;
64 }
65 
66 /**
67     Sets the error count to 0 and erases all accumulated errors. This function
68     does not release any memory allocated for the error list.
69 */
70 void resetErrors()
71 {
72     _errorCount = 0;
73     memset(_errors.ptr, 0, _errors.length * ErrorInfo.sizeof);
74 }
75 
76 /*
77 void freeErrors()
78 {
79     free(_errors.ptr);
80     _errors.length = _errorCount = 0;
81 }
82 */
83 
84 /**
85     Loads a symbol from a shared library and assigns it to a caller-supplied pointer.
86 
87     Params:
88         lib =           a valid handle to a shared library loaded via the `load` function.
89         ptr =           a pointer to a function or variable whose declaration is
90                         appropriate for the symbol being bound (it is up to the caller to
91                         verify the types match).
92         symbolName =    the name of the symbol to bind.
93 */
94 void bindSymbol(SharedLib lib, void** ptr, const(char)* symbolName)
95 {
96     // Without this, DMD can hang in release builds
97     pragma(inline, false);
98 
99     assert(lib._handle);
100     auto sym = loadSymbol(lib._handle, symbolName);
101     if(sym) {
102         *ptr = sym;
103     }
104     else {
105         addErr("Missing Symbol", symbolName);
106     }
107 }
108 
109 /**
110     Formats a symbol using the Windows stdcall mangling if necessary before passing it on to
111     bindSymbol.
112 
113     Params:
114         lib =           a valid handle to a shared library loaded via the `load` function.
115         ptr =           a reference to a function or variable of matching the template parameter
116                         type whose declaration is appropriate for the symbol being bound (it is up
117                         to the caller to verify the types match).
118         symbolName =    the name of the symbol to bind.
119 */
120 void bindSymbol_stdcall(T)(SharedLib lib, ref T ptr, const(char)* symbolName)
121 {
122     import bindbc.loader.system : bindWindows, bind32;
123 
124     static if(bindWindows && bind32) {
125         import core.stdc.stdio : snprintf;
126         import std.traits : ParameterTypeTuple;
127 
128         uint paramSize(A...)(A args)
129         {
130             size_t sum = 0;
131             foreach(arg; args) {
132                 sum += arg.sizeof;
133 
134                 // Align on 32-bit stack
135                 if((sum & 3) != 0) {
136                     sum += 4 - (sum & 3);
137                 }
138             }
139             return sum;
140         }
141 
142         ParameterTypeTuple!f params;
143         char[128] mangled;
144         snprintf(mangled.ptr, mangled.length, "_%s@%d", symbolName, paramSize(params));
145         symbolName = mangled.ptr;
146     }
147     bindSymbol(lib, cast(void**)&ptr,  symbolName);
148 }
149 
150 /**
151     Loads a shared library from disk, using the system-specific API and search rules.
152 
153     libName =           the name of the library to load. May include the full or relative
154                         path for the file.
155 */
156 SharedLib load(const(char)* libName)
157 {
158     auto handle = loadLib(libName);
159     if(handle) return SharedLib(handle);
160     else {
161         addErr(libName, null);
162         return invalidHandle;
163     }
164 }
165 
166 /**
167     Unloads a shared library from process memory.
168 
169     Generally, it is not necessary to call this function at program exit, as the system will ensure
170     any shared libraries loaded by the process will be unloaded. However, it may be useful to call
171     this function to release shared libraries that are no longer needed by the program during runtime,
172     such as those that are part of a "hot swap" mechanism or an extension framework.
173 */
174 void unload(ref SharedLib lib) {
175     if(lib._handle) {
176         unloadLib(lib._handle);
177         lib = invalidHandle;
178     }
179 }
180 
181 private:
182 void allocErrs() {
183     size_t newSize = _errorCount == 0 ? 16 : _errors.length * 2;
184     auto errs = cast(ErrorInfo*)malloc(ErrorInfo.sizeof * newSize);
185     if(!errs) exit(EXIT_FAILURE);
186 
187     if(_errorCount > 0) {
188         memcpy(errs, _errors.ptr, ErrorInfo.sizeof * _errors.length);
189         free(_errors.ptr);
190     }
191 
192     _errors = errs[0 .. newSize];
193 }
194 
195 void copyString(char** dst, const(char)* src)
196 {
197     *dst = cast(char*)malloc(strlen(src) + 1);
198     strcpy(*dst, src);
199 }
200 
201 void addErr(const(char)* errstr, const(char)* message)
202 {
203     if(_errors.length == 0 || _errorCount >= _errors.length) {
204         allocErrs();
205     }
206 
207     auto pinfo = &_errors[_errorCount];
208     copyString(&pinfo._error, errstr);
209 
210     if(message) {
211         copyString(&pinfo._message, message);
212     }
213     else {
214         sysError(pinfo);
215     }
216     ++_errorCount;
217 }
218 
219 version(Windows)
220 {
221     import core.sys.windows.windows;
222     extern(Windows) @nogc nothrow alias pSetDLLDirectory = BOOL function(const(char)*);
223     pSetDLLDirectory setDLLDirectory;
224 
225     void* loadLib(const(char)* name)
226     {
227         return LoadLibraryA(name);
228     }
229 
230     void unloadLib(void* lib)
231     {
232         FreeLibrary(lib);
233     }
234 
235     void* loadSymbol(void* lib, const(char)* symbolName)
236     {
237         return GetProcAddress(lib, symbolName);
238     }
239 
240     void sysError(ErrorInfo* pinfo)
241     {
242         char* msgBuf;
243         enum uint langID = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT);
244 
245         FormatMessageA(
246             FORMAT_MESSAGE_ALLOCATE_BUFFER |
247             FORMAT_MESSAGE_FROM_SYSTEM |
248             FORMAT_MESSAGE_IGNORE_INSERTS,
249             null,
250             GetLastError(),
251             langID,
252             cast(char*)&msgBuf,
253             0,
254             null
255         );
256 
257         if(msgBuf) {
258             copyString(&pinfo._message, msgBuf);
259             LocalFree(msgBuf);
260         }
261         else
262         {
263             copyString(&pinfo._message, "Unknown Error");
264         }
265     }
266 
267     /**
268         Adds a path to the default search path on Windows, replacing the path set in a previous
269         call to the same function.
270 
271         Any path added via this function will be added to the default DLL search path as documented at
272         https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw.
273 
274         Generally, when loading DLLs on a path that is not on the search path, e.g., from a subdirectory
275         of the application, the path should be prepended to the DLL name passed to the load function,
276         e.g., "dlls\\SDL2.dll". If `setCustomLoaderSearchPath(".\\dlls")` is called first, then the subdirectory
277         will become part of the DLL search path and the path may be omitted from the load function. (Be
278         aware that ".\\dlls" is relative to the current working directory, which may not be the application
279         directory, so the path should be constructed appropriately.)
280 
281         Some DLLs may depend on other DLLs, perhaps even attempting to load them dynamically at run time
282         (e.g., SDL2_image only loads dependencies such as libpng if it is initialized at run time with
283         support for those dependencies). In this case, if the DLL and its dependencies are placed in a subdirectory and
284         loaded as e.g., "dlls\\SDL2_image.dll", then the dependencies will not be found; the
285         system loader will look for them on the regular DLL search path. When that happens, the solution
286         is to call `setCustomLoaderSearchPath` with the subdirectory before initializing the library.
287 
288         Calling this function with `null` as the argument will reset the default search path.
289 
290         When the function returns `false`, the relevant `ErrorInfo` is added to the global error list and can
291         be retrieved by looping through the array returned by the `errors` function.
292 
293         When placing DLLs in a subdirectory of the application, it should be considered good practice to
294         call `setCustomLoaderSearchPath` to ensure all DLLs load properly. It should also be considered good
295         practice to reset the default search path once all DLLs are loaded.
296 
297         This function is only available on Windows, so any usage of it should be preceded with
298         `version(Windows)`.
299 
300         Params:
301             path = the path to add to the DLL search path, or `null` to reset the default.
302 
303         Returns:
304             `true` if the path was successfully added to the DLL search path, otherwise `false`.
305     */
306     public
307     bool setCustomLoaderSearchPath(const(char)* path)
308     {
309         if(!setDLLDirectory) {
310             auto lib = load("Kernel32.dll");
311             if(lib == invalidHandle) return false;
312             lib.bindSymbol(cast(void**)&setDLLDirectory, "SetDllDirectoryA");
313             if(!setDLLDirectory) return false;
314         }
315         return setDLLDirectory(path) != 0;
316     }
317 }
318 else version(Posix) {
319     import core.sys.posix.dlfcn;
320 
321     void* loadLib(const(char)* name)
322     {
323         return dlopen(name, RTLD_NOW);
324     }
325 
326     void unloadLib(void* lib)
327     {
328         dlclose(lib);
329     }
330 
331     void* loadSymbol(void* lib, const(char)* symbolName)
332     {
333         return dlsym(lib, symbolName);
334     }
335 
336     void sysError(ErrorInfo* pinfo)
337     {
338         auto msg = dlerror();
339         if(!msg) copyString(&pinfo._message, "Uknown Error");
340         else copyString(&pinfo._message, msg);
341     }
342 }
343 else static assert(0, "bindbc-loader is not implemented on this platform.");