iOS Development Journal

Tips and Tricks Learned the Hard Way

Static Libraries Linking to Embedded Frameworks

Are you seeing errors like this:

1
2
3
4
5
Undefined symbols for architecture x86_64:
  "_OBJC_CLASS_$_TestClass", referenced from:
  objc-class-ref in liblibrary.a(library.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Are you trying to access symbols inside an embedded framework from a static library? dylib and .a mixtures not working?

Well, I was, and here’s what I did.

History

iOS developers have long leveraged the static library, or .a file. This allowed to common code to be shared across multiple projects, or to have multiple teams deliver into one app. It linked by static linking, which was the only option given the tools Apple had and the sandboxing present. Most modern programming uses shared libraries, which are dynamically linked.

  • Static Library
    1. Embedded into the main application binary.
    2. Code only available to application it is linked to.
    3. Code loaded with application.
  • Shared Library
    1. Shipped as a separate file along with the application binary.
    2. Code available to all applications it is linked to.
    3. Code loaded on demand.

iOS 8 introduced extensions, and with it Cocoa Touch Frameworks. These enable embedding a shared library in your application to allow code to be shared between your main application and the extensions (which are all independent binaries).

Problem

Generally, these work well and any problems you run into are likely already covered by Stack Overflow. I ran into a weird one though. Here’s the setup.

Photo of Project settings

  • An app that has both a static library and an embedded framework.
  • The app only calls code in the static library, not the framework.
  • the static library calls code provided by the framework.

If I set up the project like this, and build, it fails with the following error:

1
2
3
4
5
Undefined symbols for architecture x86_64:
  "_OBJC_CLASS_$_TestClass", referenced from:
  objc-class-ref in liblibrary.a(library.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Have a look for yourself.

What’s the issue?

It turns out that since the embedded framework is a dynamic library that isn’t used directly by any app code, the symbols will only load at run time. Since the static library requires symbols to resolve at link time, this fails.

We could solve this by simply using some framework code in our app. However, this option wasn’t available to me.

Solution

Part One

This first step of the solution was to modify the library code. Instead of using the framework classes directly, we’ll use them dynamically.

1
TestClass *tc = [[TestClass alloc] init];

now becomes

1
id tc = [[NSClassFromString(@"TestClass") alloc] init];

Now we can build our app. However, it doesn’t actually work.

Part Two

The problem is that at launch time iOS doesn’t have any reason to load our shared library. It doesn’t see any use of it (hence the original problem). So we have to force loading of the framework. We can do this in our library:

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <dlfcn.h>

@implementation Library

- (BOOL)confirmAllSystemsGo {
    void *dlhandle = dlopen("Test.framework/Test", RTLD_LAZY|RTLD_LOCAL);

    id tc = [[NSClassFromString(@"TestClass") alloc] init];
    dlclose(dlhandle);
    return [tc objectExists];
}

@end

This works now, but the first run of a library method is slow. And what if we have multiple entry points based on user behavior, then this gets complicated.1

Complete Solution

The final solution that I used was to move the library load inside of the app delegates -[applications:didFinishLaunchingWithOptions:].

1
dlopen("Test.framework/Test", RTLD_LAZY|RTLD_GLOBAL);

And now everything is actually working.2


  1. So, it looks like calls to dlclose are actually ignored in iOS. Once you dlopen a framework, it lives as long as the app is in memory. That simplifies things some, but makes me super paranoid about not calling dlclose here.

  2. We intentionally don’t worry about dlclose since we want symbols available throughout the entire app as long as it is running. Also, see above.