Cocoa Dynamic Frameworks
If you don’t know the nuts and bolts of how your code is compiled, linked, and executed on target devices, you aren’t alone. And lets be honest, this is perfectly fine! That’s the great thing about abstraction: not everyone need be an expert at everything in order to be effective.
There are times though where a little bit of knowledge can go a long way to help troubleshoot particularly onerous problems. So I thought I’d explain a bit about how apps work in Cocoa (and by extension, Cocoa Touch), particularly how frameworks work.
Before we get into dynamic frameworks, or even their static counterparts, let’s first take a look into the stuff inside of those binaries to see how your code gets to where it needs to be.
Mach-O Speed!
In case you haven’t come across it before, Mach-O is the file format used within executable files in Cocoa platforms. If you’ve heard of ELF from the Linux world or PE from Windows, this is the same thing: a data structure used to store and reference executable code. It consists of headers combined with data tables for each corresponding header. Apple had some excellent documentation Mach-O, with references to other deeper details than I’ll cover here.
Code is just data
Executable code really is just data! Just like text files, images, or audio files, executable data is structured to be read by something. But instead of being read by an image viewer or text editor, it’s meant to be read (indirectly) by the CPU.
Your executables aren’t directly dumped onto the CPU and run however. The operating system and runtime system manages the reading of your executable code, manages fetching in other resources as needed, pages things in and out of memory, and so forth. And this is where the structure of these files becomes important. These headers are used by the system to know what data lives where.
Code symbols are organized into packages which can be referenced from within the package, or from outside. And the table structure within these files is used to help reference symbols, well, symbolically.
What happens when you compile & link?
If you’ve ever had a strange problem building your project, invariably someone on Stack Overflow will ask the same question: “Have you deleted your DerivedData directory?” It’s the Xcode equivalent of “Have you turned it off, and back on again?”
The DerivedData directory is the area of the file system where Xcode maintains its working data: search indexes, intermediate build products, the actual built products of your application targets, as well as other miscellaneous configuration and state data. This keeps your actual project directory clean without a lot of unnecessary clutter.
If you dig deeper, you’ll notice some directories called “Objects-normal”, inside of which is a separate subdirectory for each CPU architecture you’re building. And for every source file you’re compiling, you’ll see a separate *.o
file. This is a fragment of generated Mach-O data.
Linking a target is essentially the process of combining all those object files together into a single, larger file called an “Archive”. The end result of this is usually an application executable, or library. An archive can be thought of as a sort of zip file containing multiple lookup tables, used to find and load symbols.
What are Static Libraries?
Many people don’t know the significance of static vs. dynamic libraries, but at a low level, the distinction is pretty significant. Static libraries are, more or less, just larger object files. They’re archives of symbols, but they aren’t actually executable products. They’re like intermediate build objects that have yet to be linked and finalized. This means they don’t have a loader section, or initialization headers.
When a static library is linked into an executable product (either an app, or a framework) the entirety of the static library is merged with the same target. The linker can be smart and is able to trim unused symbols, but those symbols all live within the same space of the application executable.
But combining the symbols into the same product can be a problem for several reasons, one of which is symbol conflicts. All symbol names need to be unique to be linkable, and if the same function, class, or even extern variable is defined in multiple libraries, the linker will throw a symbol conflict error, and your build will fail.
This merging of symbols into the same target can be a problem for shared code within an iOS application. If a static library is used by multiple targets within your application (for example, WatchKit or Notification extensions), then you may be pulling in unused or unneeded resources, inflating the size of the executable in memory, or for your app binary size on the App Store.
So what are Dynamic Libraries?
Dynamic libraries (or “dylib” for short) are fully linked executable products, without a main
method. Remember how all iOS applications need a “main.c” file with an int main()
function that kicks off the UIApplication
lifecyle? Well, that’s the symbol that the Cocoa runtime engine uses to start an executable.
Dynamic libraries don’t contain a main
because they’re meant to be consumed dynamically by a regular application that does have a main
.
These libraries can be opened (using dlopen()
) and closed (using dlclose()
) dynamically at runtime. Essentially the linking steps that happen at compile time for static libraries is duplicated at runtime, deferring the work to load those resources.
As a result, classes, functions, and data can be accessed on-demand. This means that the memory is only consumed for a library while it’s loaded, and any of the initialization that would have happened at app launch can be deferred, resulting in faster launch times.
Once you leave C or C++ behind, and move into Objective-C, some of those performance benefits are lost since the Objective-C runtime itself calls dlopen
for you automatically at app launch.
Supporting true dylib libraries can be cumbersome though, because all symbol accesses need to be resolved dynamically; dynamic pointers need to be used every time something within the dylib is used, with the need to fallback if something unexpected happens. As a result, these are usually used for larger or more costly libraries that aren’t used often. For example, the Swift runtime library itself is implemented as plain “dylib” libraries, and not as frameworks.
Dylibs do have some significant advantages however. Symbol conflicts are resolved at runtime, not at compile time, meaning any conflicts that do occur don’t break the app. Additionally, Mach supports the concept of 2-level namespacing, meaning symbols are prepended with the dylan’s name, meaning two different libraries can define their own implementations of C or C++ functions without collision.
Many symbols can also be hidden within a library. Internal-only features may be used by the dylib, but may not be exported externally to other parts of the application (this is where the extern
keyword comes from).
All of this means is that shared code lives in one place in your application, not spread out across multiple targets, which not only decreases your app’s size but improves startup time and memory footprint. And for macOS enables the use of plugins that can be dynamically added or removed without having to be built at compile-time into the application.
Okay, now what about Frameworks?
*phew*! That was a long road to get here, but I hope it was worth it!
A framework in Cocoa is nothing more than a Bundle, with an executable binary that doesn’t contain a main
function. I know, it’s a bit of a letdown, but there’s nothing really special about frameworks on the surface.
Note: If you’re not familiar with it, Bundles are everywhere in Cocoa. A bundle is a directory with a special extension name, containing an
Info.plist
file describing the contents of the bundle. Applications, frameworks, documents, Xcode archives, and many other things are embedded in bundles.
Applications themselves are bundles that may optionally contain other bundles. They can contain frameworks, resource bundles, or even other applications (such as status bar or helper apps in macOS).
The Info.plist
for a framework has a key CFBundlePackageType
with a value FMWK
among other keys describing version information, build information, etc. Optionally a framework can also contain a NSPrincipalClass
key, which I’ll get to later.
Frameworks requires that a dylib file exists within the framework, with the same name as the executable folder name (without the extension) which is used as the framework’s library.
Frameworks, when used by a developer, also contain additional subdirectories containing headers which allows a developer to build something using it. This lets the compiler know about symbols, which assists the editor in knowing which symbols to reference. These headers are stripped out by Xcode in the “Embedded Frameworks” build phase when compiling an application.
Note: Frameworks also contain a “Modules” subdirectory, which contains one or more “Module Maps”. These are the official mechanism used by LLVM to know how the framework is used, how symbols will be exported, and optionally what dependencies need to be linked in to use this framework.
This is a complex topic which I might cover at some other time, but is way beyond the scope of this post. Let me know in the comments if this is something about which you’d like to learn more.
What happens when my application launches?
Ahh where the rubber meets the road, the point of this entire exercise! How does your code go from being symbols kicking around in files, to being machine code percolating through your CPU? Here’s the (very crudely represented) set of steps that are taken when an application is launched:
- Identifies the list of linked libraries used, by name.
- Searches through its “Runpath Search Paths” locations in order.
- Attempts to find a framework matching the expected name.
- If it finds it, it opens it.
- If it can’t find it, it looks in the system frameworks directories.
- If it still can’t find it, a runtime linker error is thrown and the app crashes.
Since linking happens at runtime, the search paths are more constrained than at compile time. The Runpath Search path (LD_RUNPATH_SEARCH_PATHS
build setting in Xcode) is used to identify relative paths where frameworks may be located. There are a few variables used to indicate from what the paths are relative to:
@executable_path
: Path to the directory containing the executable that is currently running (e.g. Application bundle)@loader_path
: Path to the directory containing the binary or library that caused this framework to load.
This last one is important because it allows frameworks embedded inside other targets to control where its frameworks live, without the parent application needing to care. For more information on this topic, please go check out Mike Ash’s excellent article on dynamic linking.
By carefully crafting your runpath search paths, it’s possible to easily share the same common dynamic framework across all of your application’s targets and extensions. This is referred to as Runpath-Dependent Libraries, and can result in your apps taking up less memory and app size while sharing common code across your whole application.
Dynamic library initialization
This doesn’t all come for free however. In the real world, machine language doesn’t work in methods, classes or symbols. Your code is loaded into a page of memory, the CPU is pointed at a particular location in memory, and the CPU just runs until it’s told to stop. When navigating between control structures, between methods or functions, those result in the equivalent of GOTO statements, jumping to one point in memory to another. This is fine within a single compiled package, where the byte offsets between statements is known at link time. But what about dynamic libraries?
Those libraries may be loaded into a shared location in memory where multiple processes can access it. Or they may be loaded, then unloaded, and reloaded into a different place in memory. Many different situations may arise, resulting in the byte offsets to various functions or data segments being, well, dynamic.
As a result, within a given executable (be it an application or dynamic library), the offsets are relative to the 0th byte of the given set of instructions within the library’s archive (more about this in the next section). When a dynamic library is opened (using dlopen if you’ll recall from earlier), it immediately goes through a “fix-up” stage where all those byte offsets are updated to account for their new location in memory. This takes time, and doesn’t come for free.
For more information about the costs of library initialization, and its effect on app startup time, I highly recommend that you refer to Apple’s WWDC 2016 session Optimizing App Startup Time session.
I like big archives, and cannot lie!
This very dated pop-culture reference aside, I’d like to talk briefly about archives that consist of more than one CPU architecture, namely “FAT” archives. These are archives that are essentially multiple sets of instructions (essentially archives of object data) built for multiple CPU architectures combined into a single file.
These are used when building so-called “Archive” builds (e.g. app store or release builds), or when building an app or library for use across multiple device types. The term “FAT archives” commonly refer to builds that include both simulator and device architectures, usually i386, x86_64, armv7 and arm64 architectures. This covers the gamut of macOS simulator types (for 32- and 64-bit Intel processors) as well as physical iOS devices.
This is important because regardless of what language your application is built in, your code still needs to be compiled down into some sort of machine language, which is entirely dependent on the type of CPU doing the work.
When a dynamic library is initialized, its byte offset is usually indicated based on the offset from the architecture in which it is compiled, which in turn may be an offset from the original archive. This is why the UUID of the target library is important. If you ever see a crash report from your application, and you see the bottom section of “Binary Images”, these indicate not only the libraries that were loaded, but the byte offsets within those images where the CPU-specific architectures resided, so the byte offsets of the individual instructions involved in the crashes resided.
NSPrincipalClass: plugins, extensions, and more
Now that you have your fantastic plugins, you might want to do something with them. The most obvious is to utilize the headers exposed in the framework from an application, or another framework, but this requires that your compiler be aware of the classes and types defined within it. What if you wanted to do things the other way around?
This is where the concept of plugins come in. Imagine you have an application, and would like to drop in functionality from other developers producing their work without the host application needing to know specific details. Lets start with a practical example:
Example: In a tabbed application (using
UITabBarController
as its root view controller), lets assume that the application itself doesn’t define any tabs explicitly. Instead, it uses a protocol to define the way the application will retrieve these view controllers.
@protocol TabProvider
+ (nullable UIViewController*)newTabViewController;
@end
It is a naïve implementation since it doesn’t take into consideration the relative order of the controllers, but we’ll start with this. If the application were started, the set of tabs would be empty. However, if the application were to identify a set of classes that supported that view controller, we could iterate over them and ask for a view controller.
This is where the fact that frameworks are bundles comes in handy. We can use the NSBundle
APIs to interrogate our application for the list of bundles to see whether or not it’s a plugin for our application.
Remember earlier when I mentioned the special framework Info.plist
key NSPrincipalClass
? This is very useful in plugins, because it allows the framework to expose information about which class within it should be considered its logical main
interface. For our example, lets create a utility method that takes a framework bundle and retrieves a tab controller from it, if one is available.
- (UIViewController*)tabViewControllerFromBundle:(NSBundle *)bundle {
if (![bundle.principalClass conformsToProtocol:@protocol(TabProvider)]) {
return nil;
}
return [bundle.principalClass newTabViewController];
}
At this point all we need to do is iterate over the bundles available in our application, or if we wanted to be more organized, iterate over the frameworks in a particular subdirectory, such as “PlugIns”. And there you have it, an easily-extensible application that allows an application to be assembled dynamically from parts produced by different teams.
I’ve created an example application that breaks its UI up into multiple plugin frameworks that demonstrates how to put this into action.
If you wanted to take it to the next step, you can add more details to the protocol about event handling, message passing between plugins, and so forth.
Summary
Dynamic frameworks definitely stand apart from static libraries, or even directly linking your code into your target application. There are incidental start-up costs, but in multi-target applications including multiple extensions, these costs can be offset by their other advantages they provide. Furthermore, utilizing frameworks for extensibility or plugin capabilities offer significant architectural advantages that can simplify complex application development, allowing teams to scale.
If you found this useful, or if there’s something I missed, please let me know in the comments. And if you want to know more, check out my next article on LLVM Module Maps, which will dive deeper into how different frameworks, potentially written in different languages (be it C, C++, Objective-C, Swift, or anything else that LLVM may be able to compile in the future) can interoperate.