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