Once again we meet on the road trying to print a Hello World on our consoles.

Today’s script.

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:

  1. install_name_tool assigns an identification name to the binary located at Hello.framework/Versions/A/Hello. This name represents the expected runtime location of the binary within the framework structure.
  2. 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.
  3. 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.