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.");