How we got around pinning users of the GeotriggerSDK for iOS to the same versions of the open source libraries we use.
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 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.
- 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.
- 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.shbut 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
extsubfolder 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
exttarget 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.
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.
- 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.
- 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.
- 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™.
- 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.
- 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.
- 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.