Avoiding dependency collisions in iOS static library managed by CocoaPods

author: Kamil Burczyk

After previous 2 tutorials we have a CocoaPods project that can be easily built for multiple architectures and is merged into one static library, but...

What happens if user adds AFNetworking to his app (it is already a dependency in our static library)?

He gets this:
Duplicated symbols

You can try this on your own by cloning ReposBrowserStatic repository and adding our built library to it.

It is caused by the structure of library .a file. Simple explanation is that each class from .m file is compiled into an object .o file that contains a set of defined symbols and then all object files are linked (merged) together into one static library .a which extracts those symbols and places them in flat common space. You can read more about it e.g. on Wikipedia.

The same thing occurs when you include a library in your app - all the symbols are merged together into a common space. That's why we get those duplication errors.

We can investigate those symbols using nm command. If you walk into your Derived Data subfolder and type something like:

$ nm libNetworkLib-debug-0.1.a | grep AFN

You will get a list of all symbols connected to AFNetworking:

000000000000bea0 s -[UIWebView(_AFNetworking) af_HTTPRequestOperation].eh  
0000000000000030 t -[UIWebView(_AFNetworking) af_setHTTPRequestOperation:]  
000000000000bec8 s -[UIWebView(_AFNetworking) af_setHTTPRequestOperation:].eh  
0000000000000190 t ___44-[UIWebView(AFNetworking) requestSerializer]_block_invoke  
000000000000bf18 s ___44-[UIWebView(AFNetworking) requestSerializer]_block_invoke.eh  

How can we avoid those collisions? I came up with 3 or even 4 solutions:

  • Pretend they don't exist ;) - it may not be the solution but if you use some rare libraries and you are 100% sure they will not be used in your or your customer's project you can just leave it like this.
  • Write in docs which dependencies are required - you can explicitly say that you require e.g. AFNetworking in version 1.3.3 and basically force user of your library to provide such version during build time but it is just mean (if they don't their code will not compile at all). You force someone else to do your job and you don't leave them any choice about version of those dependencies.
  • Export all dependencies' headers - alongside your own headers you can export headers of dependencies. Users of your library will get them basically for free - they can just import what's necessary and start development. The only major disadvantage is that it forces them to use version specified in your library. They cannot upgrade those dependencies without new version of your lib.
  • Build library with prefixed symbols - in my opinion the best way. We would like to add a prefix to all of the symbols in final .a file so that e.g. AFHTTPSessionManager becomes SIGMAPOINT_AFHTTPSessionManager. Your version of dependency is not connected to version used by developer and no collisions occurs. The only disadvantage is bigger output file because when someone uses the same library the final file will contain the same symbols: once prefixed and once not prefixed. But the whole build process will work smooth and without errors. This is the solution we will investigate further in this article.

When I faced this problem I used solution described in this blog.

The concept is to export all the symbols from built library, generate some macros that will replace original symbols with prefixed versions and place those macros in build process in such way that final library contains only prefixed symbols.

We will use this script to export symbols and generate macros.

The most important part of the script is to find only those symbols that should be replaced. It's done by lines similar to this one:

nm $CODESIGNING_FOLDER_PATH -j | sort | uniq | grep "_OBJC_CLASS_\$_" | grep -v "\$_AGSGT" | grep -v "\$_CL" | grep -v "\$_NS" | grep -v "\$_UI" | sed -e 's/_OBJC_CLASS_\$_\(.*\)/#ifndef \1\'$'\n''#define \1 __NS_SYMBOL(\1)\'$'\n''#endif\'$'\n''/g' >> $header  

Breakdown:

  • variable $CODESIGNING_FOLDER_PATH is set by Xcode during compilation,
  • flag -j prints only names, without addresses etc.,
  • sort and uniq are self-describing
  • grep "_OBJC_CLASS_\$_" selects all the symbols containing that string,
  • grep -v ... rejects given string, so here we will reject all compiled symbols with CL (Core Location) UI (UIKit), NS (NextStep) etc. Those are the symbols we don't want to prefix. If you use some other Apple frameworks you should extend that script with the symbols you want to preserve.
  • in the end sed command generates a macro and places it in NamespacedDependencies.h file.

So let's generate it! All you have to do is to select Pods project in Xcode, select Pods-NetworkLib target and add Run Script phase at the end. As a script you just paste this file.
Your configured environment should look like this:

Run script phase added to Pods target

If you hit ⌘B you should get NamespacedDependencies.h file in your Pods directory. You can copy it somewhere and remove Run Script phase because we don't actually need to generate it during each build. Although you must generate this file again if you change something in Podfile like e.g. you add new dependency.

Macros in that file look like this:

#ifndef AFHTTPSessionManager 
#define AFHTTPSessionManager ADD_PREFIX(AFHTTPSessionManager) 
#endif

they simply state: if you find a symbol AFHTTPSessionManager call ADD_PREFIX(...) macro which replaces that symbol with SIGMAPOINT_AFHTTPSessionManager.

Now the trick is that if we add that file in the end of Pods-NetworkLib-environment.h it will be included in each CocoaPods dependency, because this header works as standard .pch file from Xcode project!

When you add it like this:

Add NamespacedDependencies.h to Pods

and compile your lib again you can check which symbols are generated inside e.g. libPods-NetworkLib.a file by running:

nm libPods-NetworkLib.a | grep SIGMAPOINT

It should give you output similar to the one below:

000000000000b720 s l_OBJC_$_CATEGORY_ SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView  
000000000000b6d8 s l_OBJC_$_PROP_LIST_ SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView  
                 U _OBJC_CLASS_$_ SIGMAPOINT_AFHTTPRequestOperation
                 U _OBJC_CLASS_$_ SIGMAPOINT_AFHTTPRequestSerializer
                 U _OBJC_CLASS_$_ SIGMAPOINT_AFHTTPResponseSerializer

which lists all symbols prefixed with SIGMAPOINT.

From this point it's only one step to have fully-functional prefixed library - you just need to include generated macros in your Project-Prefix.pch file so they are visible globally in your code.

Drag&drop NamespacedDependencies.h to our NetworkLib project and edit NetworkLib-Prefix.pch file so it looks similar to the one below:

Edited NetworkLib-Prefix.pch to avoid collisions

You can visually check if generated macros by checking the color of each third party symbol - if they are brown and if they link to a particular macro when ⌘-clicked then your configuration is fine (being a visualizer helps in that case too ;) ).

External symbols changed to macros

Hit ⌘B to build a library using AggregateLib target and check final symbols by running nm in Derived Data/NetworkLib directory:

nm -j libNetworkLib-debug-0.1.a | grep SIGMAPOINT  

Example output:

_OBJC_CLASS_$_SIGMAPOINT_AFURLConnectionOperation  
l_OBJC_$_CATEGORY_SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView  
l_OBJC_$_PROP_LIST_SIGMAPOINT_AFURLConnectionOperation_$__UIProgressView  
_OBJC_CLASS_$_SIGMAPOINT_AFHTTPRequestOperation  
_OBJC_CLASS_$_SIGMAPOINT_AFHTTPRequestSerializer  
_OBJC_CLASS_$_SIGMAPOINT_AFHTTPResponseSerializer  

With such prepared library our ReposBrowserStatic project which includes AFNetworking and our compiled library should compile and run without errors even with our own AFNetworking dependency!

comments powered by Disqus