Cross-platform without complexity: shared libraries

Spread the love

Continuing from our cross-platform project skeleton, let’s try to add a shared library. Windows users may know these as dynamic link libraries (DLLs), while Linux users would recognize them as shared objects (.so). To make things even trickier, most Windows compilers must explicitly export DLL members, while Linux compilers usually export everything by default. Alas, cross-platform is a must so we cannot take any hard dependencies on these Win32/Linux-isms.

Luckily, CMake has us covered. Using GenerateExportHeader, we can easily account for platform specific behaviors without any ugly #ifdefs (at least none that we have to write personally). This is the desired revised project structure:

  • inc (public header file root)
    • core (header files for `core` lib)
    • lib (header files for shared `lib`)
  • src (source code root)
    • app (executable target)
    • core (static lib target)
    • lib (shared lib target)
  • test (test code root)

…and this is the CMake project to implement it, with minor additions and edits from the previous version:

cmake_minimum_required(VERSION 3.17)
project(sample LANGUAGES CXX)


add_library(sample-core STATIC

set_property(TARGET sample-core


add_library(sample-lib SHARED

    BASE_NAME samplelib




 . . . (remainder omitted as it is unchanged)

The summary of changes is as follows:

  • Include GenerateExportHeader to use the export header functionality.
  • Add explicit STATIC library type for the existing sample-core — just for clarity, not technically required.
  • Enable POSITION_INDEPENDENT_CODE for sample-core; this is required to avoid compilation errors when linking the shared library with the static library.
  • Add the new shared library sample-lib.
  • Generate an export header for sample-lib (with custom settings for nicer symbol names).
  • Link sample-lib with sample-core.
  • Link sample-app with sample-lib.

Now we need to implement the code. We’ll start with the public header file, SampleLib.h:


#include <samplelib_export.h>

extern "C"
    SAMPLE_LIB_EXPORT void *SampleInit(const char *name);

    SAMPLE_LIB_EXPORT const char *SampleGetName(void *p);

    SAMPLE_LIB_EXPORT void SampleDestroy(void *p);


You may notice the CMake magic showing up here; instead of using compiler-specific features, the generated samplelib_export.h header defines symbols like SAMPLE_LIB_EXPORT to mark public members. Also note the standard practice of flattening the C++ API into a plain C interface which will ensure a stable (enough) ABI.

Now the implementation file, SampleLib.cpp:

#include <SampleLib.h>
#include <Sample.h>
#include <memory>

using sample::core::Sample;

extern "C"
    void *SampleInit(const char *name)
        return new Sample(name);

    const char *SampleGetName(void *p)
        return static_cast<Sample *>(p)->get_name().c_str();

    void SampleDestroy(void *p)
        delete static_cast<Sample*>(p);

Yes, that is a naked new and delete. We’re forced to do this because we are defining explicit memory management functions to avoid leaking any C++ details outside the library. Bad things could happen if we tried to export a function with unique_ptr in its signature.

The final step is using the shared library from the executable in our main.cpp:

#include <iostream>
#include <memory>
#include <SampleLib.h>

using std::cout;
using std::endl;
using std::unique_ptr;

int main(int argc, const char *argv[])
    if (argc != 2)
        cout << "Please pass a string!" << endl;
        return 1;

    unique_ptr<void, void(*)(void*)> p(SampleInit(argv[1]), &SampleDestroy);
    cout << "Hello, " << SampleGetName(p.get()) << "!" << endl;
    return 0;

Here we are at least trying to make things safe by declaring a unique_ptr with custom deleter.

Since our CMake project already includes tests that launch the executable we can even show that this works by running our command line build scripts on Linux:

== ctest
Test project /mnt/x/root/CMakeSampleVS/out/build/Linux-Clang-Debug
    Start 1: SampleTest.GetName
1/3 Test #1: SampleTest.GetName ...............   Passed    0.02 sec
    Start 2: sample-app-UsageError
2/3 Test #2: sample-app-UsageError ............   Passed    0.01 sec
    Start 3: sample-app-HelloWorld
3/3 Test #3: sample-app-HelloWorld ............   Passed    0.01 sec

100% tests passed, 0 tests failed out of 3

Total Test time (real) =   0.09 sec

…and on Windows:

== ctest.exe
Test project X:/root/CMakeSampleVS/out/build/x64-Debug
    Start 1: SampleTest.GetName
1/3 Test #1: SampleTest.GetName ...............   Passed    0.01 sec
    Start 2: sample-app-UsageError
2/3 Test #2: sample-app-UsageError ............   Passed    0.05 sec
    Start 3: sample-app-HelloWorld
3/3 Test #3: sample-app-HelloWorld ............   Passed    0.01 sec

100% tests passed, 0 tests failed out of 3

Total Test time (real) =   0.12 sec

GitHub repo with all the changes so far: CMakeSampleVS

As you can see, cross-platform support for C/C++ is possible without any real sacrifice. You can use the VS IDE, you can use command line build scripts, and you can even create shared libraries for each OS from one codebase. Vive la [plate-forme?] différence!

One thought on “Cross-platform without complexity: shared libraries

  1. Pingback: Cross-platform without complexity: .NET interop – WriteAsync .NET

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload the CAPTCHA.