Static libraries are precompiled code modules. Creating and using them is easy in Visual C++. Using them correctly and efficiently, however, is not so straightforward.
I assume you are familiar with static libraries and how to create them. This articles focus on issues with static libraries, not how to create them. The compiler used in the article is Visual C++ 6.0, but the idea should apply to other compilers.
A static library is precompiled into object code. In Visual C++, this is a .lib file that can be linked with other projects. I refer to these projects as clients.
Every project has different settings to compile. The static library should not force the client compiler settings. The project settings are accessible at Project->Settings->C++->Project Options.
Since the static library code is precompiled, conflicting settings may occur. The purpose of this article is, therefore, to give you foresight into these issues so you may avoid them.
A function declared like
void calling_convention Function();
The calling convention in a VC++ project defaults to _cdecl (/Gz). However, there exist other calling conventions that are more efficient (i.e _stdcall /Gr ). We cannot assume clients will use _cdecl as the default, therefore we should declare the calling convention in every interface function exposed in the static library header file. If not, differing calling conventions in the client and the precompiled library usually result in an unresolved error during linking.
Note: Class member functions must use the _thiscall calling convention, so you don't have to declare the calling convention for them. Only free functions and static functions require the declaration.
void _cdecl Free_Function();
static void _cdecl Static_Function();
If you look at the win32 API, you will notice the API functions are declared like so
HWND WINAPI CreateWindowEx(..)
[size="5"]C Runtime Library
More often than not, your static library uses standard C functions. The static library then compiles with the C runtime libraries.
The problem is there are several versions of the runtime library - single, multithreaded, and multithreaded DLL, as well debug versions of those, making a total of 6 versions (/ML, /MT, /MD, /MLd, /MTd, /MDd).
The static library compiles with one version; the client can compile with another. A conflict occurs and usually, a warning is generated.
The solution is
- Force the client to link the same runtime library as our static library.
- Provide 6 different versions of the library.
My solution is
- Provide 6 different versions of the static library
- Define a naming convention for naming the .lib file.
- Provide a name decoration scheme that links in the correct .lib file based on client project settings
So, for example, if my static library is called MyLibrary, I have six .lib files:
MyLibrary.lib // single threaded
MyLibraryMT.lib // multithreaded
MyLibraryRT.lib // multithreaded dll
MyLibraryD.lib // debug versions
By using #ifdef and detecting if the macros are defined, I create a library name decoration macro that's a string, and add the correct suffix. The client links in the library using the macro
#pragma comment( lib, LIB_NAME_DECORATION("MyLibrary") );
[size="5"]Exception handling and RTTI
I recently got a strange phenomenon in my code. Calling typeid on an object would crash the program and the crash occurred in the depths of kernel32.dll, so it was almost impossible to debug. (Ok, I'm not so good at reading assembly code, especially when it's with MS code). Anyway, it was crashing at some kernel thread switching call, and I couldn't figure out why.
The problem? I was linking in a static library that was compiled without RTTI and I was calling typeid on one of the library objects.
The most dangerous part is the compiler does not produce warnings, even at level 4. Ouch.
The moral of the story is compile the static library with exceptions enabled (/GX) and RTTI (/GR).
Some of you may think, "I'm not going to use exceptions and RTTI so this overhead is unacceptable".
Well, you may not use those features but can you be certain your clients don't? The alternative is to create another 6 versions of static library that are optimized not to use exceptions and RTTI. You wouldn't like that, would you?
While you are at it, add the flag /EHa to the compiler to enable asynchronous exception handling. It is not enabled by default and you have to add it manually in the project settings dialog.
To me, correctness is preferable over speed so I prefer async EH over sync EH.
So, you shipped your static library and clients are happily using it. Then one day, your client comes to you, "This code causes a crash inside your static library, but I can't really find out whether it's your bug or my fault because the debugger doesn't work".
Finding crash bugs without debugging symbols is like finding a needle in a haystack. You need to read (gasp) assembly code to figure out the bug.
The situation is easily avoided by compiling the static library with debugging info so your client can use the debugger to find out the problem. (And maybe in the process help you find a bug in your static library.)
Tip: Compile the static library with debugging information.
I recommend compiling with C7 compatible debug (/Z7) for a static library. This allows debugging info to be placed inside the .lib files, so it saves the hassle of locating the program database if you generate it separately.
One important thing to take note is with debugging info enabled, the client can reverse engineer your static library. If this is not acceptable, then compile with the Line Number Only (/Zd). This gives the debugger access to public functions and data only (which of course the client should have access to already).
BTW, when compiling a release version of the library, it is advisable to compile with debugging info too. I recommend compiling with Line Number Only for release versions of the static library.
Even if you don't use the debugger to debug, don't disable your clients' from doing so.
[size="5"]Additional Include Directory
This tip concerns the static library writer (you) rather than the clients.
Say, you have a version of the library installed on your machine. You are working on a new version of the library. Somehow, the changes you make to the header do not seem to have any effect.
Depending on how you set up your project structure, the above can happen if you added the old version of the library to the compiler environment search path. The compiler is confused between the local header and the one in the old version. One remedy is to add the local directory to the compile path so the local header is always used first. This is done by adding a .(a dot which stands for local directory) in Project->Settings->C++->Preprocessor->Additional Include Directories or using (/I".").
Tip: Add the local directory in the additional compiler search path.
There are probably a few minor flags that I didn't mention (e.g. struct alignment size). Normally, those flags are not changed and even if they are, they should be done using pragmas not using compiler flags.
Keep a lookout for subtle errors. A lot of them can be avoided by good foresight and experience.
Some quote I heard before:
[nbsp][nbsp][nbsp] "Good judgment is gained from experience. Experience is gained from bad judgment."