syntax-highlighter

Sunday, January 31, 2010

Portable Dynamic Libraries

It's often useful to encapsulate components of a program in dynamic libraries. These allow you you make minor changes to a module without rebuilding the whole program and swap out components at runtime. This post shows how to do this from the command-line on Windows, Linux and Mac platforms for a simple example. I am assuming that explicit linking is being used; in my opinion the point of using dynamic libraries is to not need things like import libraries and the like...

To start with, there needs to be a module to make a dynamic library from. I'm going to work from the following, trivial function (assumed to be saved as module.cpp)

// module.cpp
int ModuleFunction( void ){ 
    return 5; 
}


In order to build this as a dynamic library some boiler-plate needs to be added. First of all the whole function needs be wrapped in an extern "C" block. This prevents C++ name mangling from changing the name of the function in the module. Secondly, under Windows the function has to be declared with __declspec(dllexport) in order to be accessible from the library. This is unnecessary on Linux and Mac machines, so this declaration is only applied if building on Windows machines. With these changes, tmp.cpp becomes:

// module.cpp
#ifdef WIN32
    #define EXPORT __declspec(dllexport)
#else
    #define EXPORT
#endif
extern "C" {
    EXPORT int ModuleFunction( void ){ 
        return 5; 
    }
}


With the module source handled, the library has to be built. The command-lines to do this differ depending on what platform and compiler you're using:

Windows, Visual C++
vcvarsall.bat (or vcvars32.bat)
cl.exe -o module.dll module.cpp /link /DLL

Windows, MinGW/ Linux, GCC
g++ -fPIC -shared module.cpp -o module.dll (or module.so)

Mac, GCC
g++ -dynamiclib tmp.cpp -o module.so

Note that for non-trivial examples you would have to specify additional source files and include/library paths.

At this point, there should be a dynamic library named module.dll (Windows) or module.so (Linux/Mac). To actually use this library there are minor differences between Windows and Linux/Mac. The following header (DLL.h) handles these variations allowing a common interface under all three platforms:

// DLL.h
#ifndef DLL_H
#define DLL_H

#ifdef _WIN32
#include<windows.h>
#define DLLPtr    HMODULE
#define LoadDLL(a) LoadLibraryA( a )
#define FreeDLL(a) FreeLibrary( a )
#else
#include<dlfcn.h>
#define DLLPtr    void*
#define LoadDLL(a) dlopen( a, RTLD_LAZY )
#define FreeDLL(a) dlclose( a )
#endif

template<typename Func>
__inline Func GetFunc( DLLPtr dll, const char *symbol ){
 #ifdef _WIN32
  return (Func)GetProcAddress( dll, symbol );
 #else
  return (Func)dlsym( dll, symbol );
 #endif
}

#endif


This provides functions to load and free dynamic libraries as well as retrieve function pointers from dynamic libraries. An example of its usage for the simple library defined above is:

// main.cpp
int main( int argc, char **argv ){
 DLLPtr lib = LoadDLL( "blah_0.dll" );
 if( !lib ){
  // handle error;
  return 0;
 }

 int (*F)(void) = GetFunc<int(*)(void)>( lib, "blah" );
 if( !F ){
  // handle error
  return 0;
 }
 std::cout << "Result: " << F() << std::endl;
 FreeDLL(lib);
 return 0;
}


Windows, Visual C++
vcvarsall.bat (or vcvars32.bat)
cl.exe -o test.exe main.cpp /link

Windows, MinGW /Mac GCC
g++ -o test main.cpp (test.exe on Windows)

Linux, GCC
g++ -o test -ldl main.cpp

Running the compiled executable should print the following string "Result: 5".

Some Notes:
1. I have found that repeatedly loading and unloading DLLs that were compiled with MinGW under Windows causes a crash. This appears to be isolated to MinGW, since the same programs run fine when compiled with Visual C++, or on Linux and Mac systems with GCC.

2. It is possible to build a dynamic library with one compiler for use with an application built by another compiler. This could be useful for allowing users to write their own plugins for a proprietary system. However for this to work properly, it seems that the runtime libraries used by both library and executable must be the same (if platform specific code is called) and any objects passed between the library and executable must be binary compatible. This frequently means that things like stl containers cannot be passed, since different compilers will likely use different implementations of stl.

No comments: