The Problem
Symbol collisions. How to deal with them? When creating a static library for iOS that uses open source libraries, you don't want to
hinder your users' abilities to include those same open source libraries in their app. Without some foresight and care, your users
will be slapped with a bunch of "duplicate symbols" errors at link time. Providing a difficult experience integrating your library
is one of the quickest ways to leave a sour taste in a user's mouth.
This article describes the solution that we used to address this problem, as well as some of the alternative solutions that we explored.
The Solution
The solution we chose was inspired by this answer on StackOverflow, but it took
me a while to get all the pieces in place. I wanted to write this article to illustrate the specifics in a bit more detail, so that your process will go more smoothly. The idea is to compile
all external dependencies into the SDK but to add a prefix to all of the symbols at compile time. We accomplish this via a header file that defines
preprocessor macros for every symbol in these dependencies. These macros map the calls to the original symbols to the "new" prefixed symbols. This
tricks our code into thinking it's using the regular version of the libraries, however the compiled .a file contains only the prefixed versions of these symbols.
This is done by first compiling the external libraries into a temporary .a file and running a shell script which uses nm (a command line
utility which lists symbols in compiled object files) to find all of the symbols in that .a file that are not from the OS/Cocoa
Frameworks. It then writes preprocessor macros into a header file that add our class prefix to the symbol names. Then, in our build
process, we make sure to include this generated header in all files that reference the external dependencies.
The Good
- Simple for your users - As with the other renaming solutions, it Just Works?.
- Simple for you - After some initial time spent setting it up (which, in fairness, isn't exactly "simple") it also Just Works?,
including after upgrading versions of your dependencies.
- Relatively fool-proof - It is not completely fool-proof; if you do find that the script missed something you can go
add a special case for that thing and it will stay solved until that thing changes.
The Bad
- Increases app size - Doubles the app size impact of any libraries that are used by both your library and the user's app. However this is also true if you rename them manually.
- Adds complexity- Your build process gets some complexity added to it which can be a maintainability problem,
but ultimately I find the tradeoff in minimized support problems makes this worthwhile.
- Increases your build times - Because you're basically building the external dependencies twice, your build times will increase slightly.
Here's the step-by-step version of how to set accomplish this:
- Add a new target to your project for the external dependencies, I called mine
ext . This target will build the temporary .a file and
run the script which generates the namespaced header file.
- In the target's Build Phases tab in Xcode:
- Add all of the dependencies' source files to the Compile Sources phase.
- Add any libraries needed by the dependencies to the Link Binary With Libraries phase.
- Add a new script phase (Editor > Add Build Phase > Add Run Script Build Phase). I prefer to put the script contents in a file and refer
to that in the Shell text box:
/bin/sh Scripts/generate_namespace_header.sh but you can also just put the following script in the text
area below that. It's up to you where you want to put it, but here is the script we use. It is slightly modified from
this one. You'll need to edit the header and prefix variables at the top of that script. You'll also probably want to
add the NamespacedDependencies.h file to your xcodeproject. I put it in an ext subfolder along with the external dependencies' sources.
- If your dependency has any special build properties or compiler flags that need to be set, set those for this target.
- In any file that you use the dependencies, add an
#import "NamespacedDependencies.h" before the imports of their header(s). I chose to do
this in the precompiled header.
- In your library's target's Build Phases tab add the
ext target as a dependency. This makes sure the external dependencies lib is compiled
and the header is generated before compiling your lib.
To verify everything is set up and working properly build your .a file and then run nm -a lib.a and you should see you external dependencies'
symbols all with the prefix you specified in the shell script.
Alternative Solutions
There are several other options for a library developer to avoid their users encountering these errors. Let's go over a few of them and
see why we went with the above solution.
Declare External Dependencies
You could declare your third party dependencies and require users of your library to also link to these
libraries. This is certainly the simplest way of dealing with the issue for you, but not necessarily for your users. Things
like CocoaPods can make this easier, but doesn't solve the other problems with this method.
The Good
- Simple for you - You just declare that in order for anybody to use your library they must first get and link to these other
libraries.
- Minimizes built app size - Since your library and the app your user is building will link to the same library object files,
there is no duplication of symbols in the end product.
The Bad
- Complicated for your users - If your users aren't already using the same libraries then this is an extra step for them to get
up and running with your library, which is almost never a Good Thing?. Again, CocoaPods alleviates the complication
for your users, but can't address the next Bad, due to Objective-c's lack of namespacing or packaging.
- Forces your users to use the same versions of the libraries as you are using - If your users are already using the same
libraries, they have to be sure they are using a version of these libraries that is compatible with the version you're using.
So now you are dictating what external libraries users of your library are using, which can be very frustrating for them and a
maintenance/support nightmare for you.
Compile External Dependencies
You can include your external dependencies in your library's compiled output and let any users who are using the same libraries worry
about renaming these libraries if they are also using them in their project.
The Good
- Simple for you - You just include the source for your dependencies in the Compile Sources build phase and you're done.
- Simple for your users who don't the external libraries - As far as these users are concerned, all they have to do is link to your
library and it Just Works?.
The Bad
- Makes your library basically unusable for users who are also using those external libraries - These users could rename all of the
symbols in their version of the external libraries... but forcing your users to do this is just mean, and will make users reconsider if
it's worth using your library (or the external libraries) at all. And that's just not cool.
Manually Rename All of the Symbols
The alternative to just compiling the dependencies in just as they are is to first rename the symbols in all of your external dependencies
yourself, then compile them into your library.
The Good
- Simple for your users - Just like compiling them without renaming them, as far as your users are concerned, it Just Works?,
and this time it works for your users regardless of whether they are using the same libraries or not.
- (Mostly) simple for you - A simple Find & Replace in your IDE for the external libraries' prefix should take care of it.
The Bad
- Manually renaming the symbols could be error-prone - As the good above says, a Find & Replace should catch everything. But
sometimes it doesn't, and then you're back at square one.
- Updating your external libraries becomes a hassle - You have to do this manual renaming process anytime you want to update these libraries
and that reintroduces the possible error vectors.
- Doubles the app size impact of any libraries that are used by both your library and the user's app - Since the compiler sees your
renamed symbols as completely seperate objects, if a user is using the same version of the same library as you are, they are effectively
including the library twice. This sounds bad, but it's usually quite minimal, and definitely worth what you gain by doing so.
|