Linking Dynamic Libraries with Executables in Swift
Generally speaking, a library is a file containing compiled binary code, they are too useful as they provide the concept of reusability and allow for fast compilation time in modular applications.
However, there are mainly two types of libraries, Static Libraries which are linked to the executable at compile time. Dynamic libraries on the other hand are linked to the executable at runtime. That makes them much more complicated and opens the way for many possible hurdles which will be discussed in this article.
Oftentimes macOS loads all dynamic libraries (*.dylib) for an executable silently, but it sometimes happens to fail. To put that into a perspective, we will build XMLJson which is an executable file that has (dylib) dependencies if we build it with release configuration we will notice that it works perfectly inside the build folder, but what if we want to make it globally available? We can move the compiled binary to
$ cd .build/release $ cp -f xmljson/usr/local/bin/xmljson
However, that’s will not work and we will get the following evil error:
dyld: Library not loaded: @rpath/libSwiftToolsSupport.dylib Referenced from: /usr/local/bin/xmljson Reason: image not found 1 44343 abort xmljson
The error seems pretty self-explanatory which tell us couldn't find libSwiftToolsSupport.dylib library at rpath but we can break it down into pieces:
- dyld: Is the macOS's default dynamic linker tool.
- couln't find libSwiftToolsSupport.dylib at the specified runtime path (we will come to this later).
- Image not found because the binary is missing @rpath.
On macOS, each dynamic library (dylib) has an "install name" property. The install name is a path baked into the dynamic library that says where to find the library at runtime. When you link against the dylib this path is saved in your binary (at link time) so that it can find the dylib later at runtime.
To solve our initial problem there are a set of options we need to demystify before going further:
@executable_path: This is a magic token that, when placed at the beginning of a library's install name, gets resolved to the absolute path of the executable that's loading it, minus the last component. Ex: let's say that
xmljsonexecutable links against
xmljsonis installed in:
/usr/local/bin/, @executable_path will expand to:
@loader_path: It expands to the full path, minus the last component, of whatever is actually causing the target library to be loaded. For example, imagine xmljson depends on liba.dylib, and liba.dylib in its turn depends on libb.dylib, the path of liba will be resolved as the same for xmljson (the caller), and the path for libb will be resolved as the same path for liba (the caller).
@rpath: When placed at the front of an install name, this asks the dynamic linker to search a list of locations for the library. That list is embedded in the executable, and can therefore be controlled by the executable's build process, not the library's. A single copy of a library can thus work for multiple purposes. The value of rpath can be set by the linker during linking the executable in two ways:
- In the Xcode, it's set with LD_RUNPATH_SEARCH_PATH setting.
- In ld command tool it's set with -rpath parameter when linking.
To inspect the executable dependencies and their paths we have two handy command-line tools otool for mac and objdump for linux, run otool -L xmljson to list all its dependencies and their paths you will notice that all the dependencies are packed by absolute path so the executable knows where to load them except for our missing dependency is packed by runtime path.
So far we have seen that the "install name" for "libSwiftToolsSupport.dylib" is @rpath/libSwiftToolsSupport.dylib that means it tells the dynamic linker wherever is the rpath I am there too. But to which value rpath in xmljson is set to? As we mentioned earlier @rpath can be set from the build settings in Xcode project by modifying LD_RUNPATH_SEARCH_PATH settings, let us inspect that too.
Clearly you can see that one of the rpath search paths is @executable_path , which explains why xmljson worked in the build folder but didn't outside because both the executable and dynamic library living in the same directory.
So the solution is pretty easy, right? Just copy both the executable and dynamic library to the same directory i.e bin directory. However, this is a bad practice, there is in fact lib directory dedicated for storing shared libraries. We will be good citizens and move our dynamic library to lib directory also change its install name to match the new path.
We have mentioned earlier that @rpath is part of the executable and is set during the linking time. However, if we want to change it later i.e. during the installation phase there is a handy command-line tool at our disposal: install_name_tool. It changes dynamic shared library install names, it has a bunch of commands but we are interested in -change command, simply it changes the dependent shared library's old install name to a new one in the specified executable @rpath is part of the executable and is set during the linking time. However, if we want to change it later i.e. during the installation phase there is a handy command-line tool at our disposal: install_name_tool. It changes dynamic shared library install names, it has a bunch of commands but we are interested in -change command, simply it changes the dependent shared library's old install name to a new one in the specified executable
Here is the usage example:
install_name_tool -change oldName newName executableFile
To apply this technique to xmljson executable:
- Copy xmljson from release folder to /usr/local/bin
- Copy libSwiftToolsSupport.dylib from release folder to /usr/local/lib
- Run the following command:
install_name_tool -change \ ".build/release/libSwiftToolsSupport.dylib" \ "/usr/local/lib/libSwiftToolsSupport.dylib" \ "/usr/local/bin/xmljson"
What we are doing here is telling xmljson's dynamic linker to change its old dependency path from the build directory to the new path which is the lib directory. And it works again! There are many other handy options in install_name_tool you can find them here
With all that said we have briefly covered what the install name is and how to inspect binary objects and analyze their dependencies. Hopefully, now you have a little bit more about how dynamic linker finds your libraries and how dynamic linking generally works on macOS.
Please let me know your thoughts or any questions you might have on Twitter @alihilal94.