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[32] _error;
25     char[96] _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.ptr; }
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.ptr; }
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 addErr(const(char)* errstr, const(char)* message)
196 {
197     if(_errors.length == 0 || _errorCount >= _errors.length) {
198         allocErrs();
199     }
200 
201     auto pinfo = &_errors[_errorCount];
202     strcpy(pinfo._error.ptr, errstr);
203 
204     if(message) {
205         strncpy(pinfo._message.ptr, message, pinfo._message.length);
206         pinfo._message[pinfo._message.length - 1] = 0;
207     }
208     else {
209         sysError(pinfo._message.ptr, pinfo._message.length);
210     }
211     ++_errorCount;
212 }
213 
214 version(Windows)
215 {
216     import core.sys.windows.windows;
217     extern(Windows) @nogc nothrow alias pSetDLLDirectory = BOOL function(const(char)*);
218     pSetDLLDirectory setDLLDirectory;
219 
220     void* loadLib(const(char)* name)
221     {
222         return LoadLibraryA(name);
223     }
224 
225     void unloadLib(void* lib)
226     {
227         FreeLibrary(lib);
228     }
229 
230     void* loadSymbol(void* lib, const(char)* symbolName)
231     {
232         return GetProcAddress(lib, symbolName);
233     }
234 
235     void sysError(char* buf, size_t len)
236     {
237         char* msgBuf;
238         enum uint langID = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT);
239 
240         FormatMessageA(
241             FORMAT_MESSAGE_ALLOCATE_BUFFER |
242             FORMAT_MESSAGE_FROM_SYSTEM |
243             FORMAT_MESSAGE_IGNORE_INSERTS,
244             null,
245             GetLastError(),
246             langID,
247             cast(char*)&msgBuf,
248             0,
249             null
250         );
251 
252         if(msgBuf) {
253             strncpy(buf, msgBuf, len);
254             buf[len - 1] = 0;
255             LocalFree(msgBuf);
256         }
257         else strncpy(buf, "Unknown Error\0", len);
258     }
259 
260     /**
261         Adds a path to the default search path on Windows, replacing the path set in a previous
262         call to the same function.
263 
264         Any path added via this function will be added to the default DLL search path as documented at
265         https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw.
266 
267         Generally, when loading DLLs on a path that is not on the search path, e.g., from a subdirectory
268         of the application, the path should be prepended to the DLL name passed to the load function,
269         e.g., "dlls\\SDL2.dll". If `setCustomLoaderSearchPath(".\\dlls")` is called first, then the subdirectory
270         will become part of the DLL search path and the path may be omitted from the load function. (Be
271         aware that ".\\dlls" is relative to the current working directory, which may not be the application
272         directory, so the path should be constructed appropriately.)
273 
274         Some DLLs may depend on other DLLs, perhaps even attempting to load them dynamically at run time
275         (e.g., SDL2_image only loads dependencies such as libpng if it is initialized at run time with
276         support for those dependencies). In this case, if the DLL and its dependencies are placed in a subdirectory and
277         loaded as e.g., "dlls\\SDL2_image.dll", then the dependencies will not be found; the
278         system loader will look for them on the regular DLL search path. When that happens, the solution
279         is to call `setCustomLoaderSearchPath` with the subdirectory before initializing the library.
280 
281         Calling this function with `null` as the argument will reset the default search path.
282 
283         When the function returns `false`, the relevant `ErrorInfo` is added to the global error list and can
284         be retrieved by looping through the array returned by the `errors` function.
285 
286         When placing DLLs in a subdirectory of the application, it should be considered good practice to
287         call `setCustomLoaderSearchPath` to ensure all DLLs load properly. It should also be considered good
288         practice to reset the default search path once all DLLs are loaded.
289 
290         This function is only available on Windows, so any usage of it should be preceded with
291         `version(Windows)`.
292 
293         Params:
294             path = the path to add to the DLL search path, or `null` to reset the default.
295 
296         Returns:
297             `true` if the path was successfully added to the DLL search path, otherwise `false`.
298     */
299     public
300     bool setCustomLoaderSearchPath(const(char)* path)
301     {
302         if(!setDLLDirectory) {
303             auto lib = load("Kernel32.dll");
304             if(lib == invalidHandle) return false;
305             lib.bindSymbol(cast(void**)&setDLLDirectory, "SetDllDirectoryA");
306             if(!setDLLDirectory) return false;
307         }
308         return setDLLDirectory(path) != 0;
309     }
310 }
311 else version(Posix) {
312     import core.sys.posix.dlfcn;
313 
314     void* loadLib(const(char)* name)
315     {
316         return dlopen(name, RTLD_NOW);
317     }
318 
319     void unloadLib(void* lib)
320     {
321         dlclose(lib);
322     }
323 
324     void* loadSymbol(void* lib, const(char)* symbolName)
325     {
326         return dlsym(lib, symbolName);
327     }
328 
329     void sysError(char* buf, size_t len)
330     {
331         auto msg = dlerror();
332         strncpy(buf, msg != null ? msg : "Unknown Error", len);
333         buf[len - 1] = 0;
334     }
335 }
336 else static assert(0, "bindbc-loader is not implemented on this platform.");