Hello Dynamic Framework
Once again we meet on the road trying to print a Hello World on our consoles.
Today’s script.
Dynamic Framework demo
Small Differences
This is the same as the static framework with some differences I’ll discuss further below.
# emit a dynamic library, not object code
swiftc -emit-library ... Hello.swift
# set the identification name of the framework
install_name_tool -id @rpath/Hello.framework/Hello \
Hello.framework/Versions/A/Hello
# add the current path to the list of framework search paths
install_name_tool -add_rpath @executable_path/. UseHello
Going dynamic
With static libraries, the library code is embedded directly into the main executable during the build process, including time-intensive optimizations. This slows down the build process.
When using dynamic libraries
- At build time the static linker (ld) stores the path to the library in the application binary.
- At run time the dynamic linker (dyld) loads the whole library to memory.
Therefore compilation is faster because we skip the time spent on optimizations, but execution is slower. Overall this speeds up iteration because optimizations take way more time than loading libraries.
There are a few other consequences:
- Dynamic libraries can have their own initialization and cleanup routines, which execute when the library is loaded or unloaded.
- Libraries can tell the dynamic linker to load additional code.
- Parallel compilation of libraries since they are independent products.
- Libraries can be shared and updated independently.
Install name tool
I used this command in the script:
install_name_tool \
-id @rpath/Hello.framework/Hello \
Hello.framework/Versions/A/Hello
The install_name_tool
records the expected location of frameworks in their own binaries. This helps executables to load their dependencies.
Here is how it works:
install_name_tool
assigns an identification name to the binary located atHello.framework/Versions/A/Hello
. This name represents the expected runtime location of the binary within the framework structure.- When an executable that depends on this framework is compiled, the compiler will read the location of the framework from its identification name and store it in the executable as a LC_LOAD_DYLIB command.
- Upon launching the executable, the system attempts to load the framework using the path stored in the LC_LOAD_DYLIB command to ensure the framework is available to handle calls made by the application.
Install name variables
The variable @rpath
refers to the runtime path, which isn’t just a single directory but rather a list of potential directories. During execution, the system appends /Hello.framework/Hello
to each path to locate the framework.
There are two other install name variables that can be used:
@executable_path
is the path of the executable that depends on the framework.@loader_path
is the path of the binary (executable or another library) that depends on the framework.
Example: if the app executable is MyApp.app/Contents/MacOS/MyApp
then MyApp.app/Contents/MacOS
is the @executable_path
and this can be used to refer to MyApp.app/Contents/Frameworks
passing @executable_path/../Frameworks
.
The man dyld
command provides a definition of these variables and more details.
Framework Search Paths
At build time the framework search paths are specified by the -F flag. Additionally, paths can be explicitly embedded into the executable in two ways:
- using the
install_name_tool
, - or passing an option to the linker with
-Xlinker -rpath -Xlinker <path>
.
Paths embedded in the executable can be displayed with otool:
# look for the command load runtime path and show me 2 lines of context
otool -l UseHello | grep -A2 LC_RPATH
At run time the system first checks the paths that were embedded in the executable during the build phase. If the framework is not found, it defaults to checking several standard system locations:
/Library/Frameworks
/System/Library/Frameworks
~/Library/Frameworks
- Paths in the
DYLD_FRAMEWORK_PATH
variable.
These mechanisms ensure that applications can dynamically link to the necessary frameworks providing some flexibility to their installation paths.
The Dynamic Linker
dyld
is the dynamic loader that helps the kernel to launch programs. It has several functions:
- Load dynamic libraries and frameworks into the program’s address space.
- Resolve symbolic references between programs and libraries, sometimes lazily for performance reasons.
- Calls initialization routines for loaded libraries.
- Looks for frameworks in the Framework Search Paths, interpreting the install name variables @rpath, @executable_path, and @loader_path.
Whenever you hear that the system is loading a library it is actually dyld
who is doing the loading.
Inspecting the binary
dyld_info
is a tool that provides information about dynamic linking and loading of executables and libraries. For instance, it lists the symbols exported in a binary:
% dyld_info -exports libHello.dylib | swift demangle | grep 'hello()'
0x000043E0 dispatch thunk of Hello.Greeter.hello() -> ()
0x00004E1C method descriptor for Hello.Greeter.hello() -> ()
Using the framework from a SPM
Once again, the script encapsulates the dependency in a XCFramework. The script is clear except for the install name trickery. After building I look for the executable and copy it to the current folder.
% find .build -name UseHello
.build/apple/Products/Release/UseHello
% cp .build/apple/Products/Release/UseHello .
% ./UseHello
./UseHello
dyld[46851]: Library not loaded: @rpath/Hello.framework/Hello
Referenced from: <922B895D-1938-302F-9567-D955FAD16CF8>
/Users/jano/Desktop/Hello/UseHello
Reason: tried: '/Users/jano/Desktop/lib/Hello.framework/Hello' (no such file),
'/Users/jano/Desktop/lib/Hello.framework/Hello' (no such file)
OK, install name doesn’t point to the xcframework. Let’s see it in full.
% otool -l UseHello | grep -A2 LC_RPATH
cmd LC_RPATH
cmdsize 40
path @executable_path/../lib (offset 12)
Aha! look at us using our arcane Mach-O LC commands to diagnose the issue. Let’s point it to the dynamic framework inside the xcframework.
% install_name_tool -add_rpath @executable_path/Hello.xcframework/macos-arm64_x86_64/. UseHello
% UseHello
Hello World!
Yes, this is fragile. You have to decide on a location for the framework, whether absolute or relative to the executable. In real life is not an issue because either we use a system location or we encapsulate executable and its dependencies in an .app.
Packaging SPM as a framework
Previously I compiled a dummy file Greeter.swift to a dylib and packaged it to a framework.
Another source for a dylib could be a SPM package of type library. Just run a variant of the following command to get the dylib. The rest of the procedure is the same.
swift build -c release --arch arm64 --arch x86_64
Conclusion
In this article, we explored how to create dynamic frameworks, which are extremely popular within the Apple ecosystem. However, what to do when two platforms share the same binary architecture (x86_64)? such is the case for iOS simulator and macOS. The solution lies in XCFrameworks, which I’ll cover in the final article. Additionally I’ll talk about “mergeable libraries,” which offer two interesting features: link the same library statically or dynamically, and/or merge two frameworks into one.