Hiding Symbols in Static Libraries with Xcode or CMake
What is a Static Library?
We all have a vague idea of what a dynamic library is: It is a file containing compiled code, and a list of functions and globals that are “exported” from it and can be called by us from the outside.
.dylib
s are specially packaged code containers not unlike an application’s code. Their source files get translated into machine code, each stored in an object file (LibUtilities.c
becomes LibUtilities.o
, LibActualFunctions.c
becomes LibActualFunctions.o
etc.), those then get handed to the linker ld
, which somehow glues them together into an executable Mach-O file.
So I was a bit surprised to find that this is not usually what happens to static libraries. Static libraries are usually not linked, instead of handing them to ld
, they get handed to ar
, the archiver. ar
is basically something like tar
. A tool that simply takes a bunch of files and glues them together into a single file.
If you use the nm
command line tool to look at the .a
file output by your compiler by default, even a release build without debug symbols, it will contain a lot of readable text: The names of all your functions, the names of your .o files, the names of C++ classes … not just runtime type information like the names of Objective-C classes and methods, no, also names of your internal functions.
Many unnecessary strings that both expose implementation details you might not want others to see if you’re distributing closed-source static libraries, and which also use a whole bunch of disk space.
Is this Apple’s fault?
What I’ve described so far holds true for all Unixes. But despite these similarities, each Unix (or Linux) seems to have its own way of dealing with this. On some, it is a matter of renaming the .o
files before archiving them, to names like 1001.o
, to hide your file paths, and then you just call strip
on them to remove most of the symbols.
On others, there are special tools.
How do I solve this for iOS/macOS?
Apple has chosen an interesting approach: They extended their linker, ld
, to also work on static libraries.
Now, linking a static library is not quite as easy as linking an executable. With an app, you have a main entry point, you can see which code calls which, you can do many kinds of clever optimizations. Moreover, You generally don’t want to link against an app and need a table of contents.
But dylibs have kind of solved most of those problems already. So what Apple do is they perform a “single-object pre-link” on your static library.
Usually, object files aren’t checked against each other. They contain information about their internal symbols (functions, globals), and empty entries for any symbols referenced from another .o
file. By using the linker to merge all .o
files in the archive into a single .o
, Apple can match up cross-references between the object files in the archive immediately, turn it into one larger self-consistent object file. As a side effect, the constituting file names also get discarded.
The linker also knows how to look up libraries, so it can also check that any external symbols that are still unresolved are defined somewhere in the system libraries.
Xcode has a simple checkbox for that. The “Single-Object Pre-Link” under “Build Settings”.
Symbol Hiding
So how does Xcode know which symbols to throw away and which to keep? I mean, I want users of my library to be able to link to the functions my static library exports, to its public API.
You have to tell it.
By default, Xcode just leaves every symbol in a library visible, unless it is obviously private (like static functions or inlined ones, or in Swift ones declared internal or private). But there is a setting to reverse that: “Symbols Hidden by Default” (Clang flag -fvisibility=hidden
).
So how do I now tell it which ones to leave visible? Two approaches.
Attribute Visibility
You decorate either the definition or declaration of a function with an attribute:
__attribute__(( visibility("default") )) void foo( void );
If you wanted every symbol visible, and just wanted to hide a few selectively, you could also do that and use an atribute to selectively hide them:
__attribute__(( visibility("hidden") )) void bar( int x );
Exports Files
An exports file contains raw symbol names, one per line. Xcode has “Exported Symbols File” and “Unexported Symbols File” options to which you pass the file name, and it will either hide or show all the symbols on the list.
CMake
The problem with CMake is that it calls ar
on static libraries, not ld
. We need it to call ld -r
so it will pre-link. How do we achieve that?
A hackish way I found is to simply tell CMake to build a dynamic library, then modify the settings a bit. So instead of
add_library(mylibrary STATIC mylibrary.c mylibraryutils.c)
your CMake file says
add_library(mylibrary SHARED mylibrary.c mylibraryutils.c)
Of course, that just gets us a dylib. But it makes CMake call ld
. Now that ld
has a shot at our object files, we use
target_link_libraries(mylibrary -r)
which will sneakily insert the -r
parameter that indicates we want to pre-link, not generate a dylib. So instead of a file of type MH_DYLIB
we now get an MH_OBJECT
file, a static lib just like we wanted. However, since CMake thinks we’re making a dylib, the generated file will still be named mylibrary.dylib
even though it is static. So we need to override that suffix:
set_target_properties(foo PROPERTIES SUFFIX ".a")
Note that a library built like this can’t be used by CMake in further build steps. All the variables CMake has set for this library think that a dylib was just built, so will use the wrong syntax to link to this static library. Since I had to keep the STATIC
built version for other platforms anyway, I just gave this library a different name and kept using the old one for when I want to link to the library myself using CMake. This build is now simply for distribution to others who want to link against the binary library.
If I have time, I may go back and investigate whether I can tell CMake using some setting to call the linker instead of the archiver. The problem with that is just that the linker needs a lot of information that is provided by the other targets, like the SDK to use, and I’d have to figure out how to correctly forward all of them.
Strip
If you still see symbols that shouldn’t be in there in the file when checking your build product using nm
, you may have to call strip -r -S -x
on your library, which will remove debug symbols and “local” symbols.