This is the first post in a series of posts where we will learn how to build a cross-platform C++ library which can be seamlessly called from .NET Core applications using P/Invoke on all supported platforms. We will gradually build a library which will take us from a simple “Hello world” to more complex tasks like string and structure manipulations.
All source code is available at my GitHub repo: https://github.com/olegtarasov/CrossplatformNativeTest. You can easily inspect the code for each post using tags. For this post the tag is
In this post will set up the environment and cover basic things we will need to compile our native library.
As you probably know, C++ libraries are generally compiled straight to machine code without the use of any intermediate language. This prevents us from compiling a single binary and using it on all platforms. Instead, we will compile a separate binary for each supported platform.
We will also create a single .NET Core application which will call the appropriate native library depending on the platform. This takes us to the task of managing the native binaries. We could just bundle those binaries along with our assembly and it would work. But I prefer to pack native dependencies inside the .NET assembly and then unpack the needed native library at runtime depending on the platform.
Now let's setup our environments under Windows, Linux and MacOs. You can easily omit platforms you are not interested in.
Setting up the environment
Under Windows things are pretty simple: just install Visual Studio 2019 with the following workloads:
- Desktop development with C++
- .NET Core cross-platform development
Make sure to select
C++ CMake tools for Windows in “Installation details” pane under “Desktop development with C++” workload.
In this post we will use Ubuntu to build and test our library. First of all, we will install essential build tools:
sudo apt install build-essential
Then we will need to install latest CMake. In this post I will cover CMake 3.14 which is the latest version at the time of writing. We will have to compile CMake from source, since most Ubuntu distributions still use older versions in their package repos.
The steps to build CMake are easy:
- Visit https://cmake.org/download/ and write down the version of the latest stable release. In my case it's
- Execute these commands substituting the version from step 1:
wget https://github.com/Kitware/CMake/releases/download/v3.14.5/cmake-3.14.5.tar.gz tar xf cmake-3.14.5.tar.gz cd cmake-3.14.5 ./configure make sudo make install
You can now execute
cmake --version. If everything went smoothly, you will see the version you've just installed.
After this we will install .NET Core. There is always an up-to-date version of .NET Core installation instructions here: https://dotnet.microsoft.com/download/linux-package-manager/ubuntu18-04/sdk-current. You can choose your Linux version from the drop-down list and follow the instructions.
Under MacOs we will need to install XCode Command Line Tools. If you already have full XCode installed, skip this step.
Open the Terminal and run
If you already have the tools installed, it will pring gcc version. If not, the dialog will appear which will prompt to install either XCode or just the Command Line Tools. You can install full XCode, but Command Line Tools alone will suffice for our job.
Now let's install Homebrew. It's a command-line package manager for MacOs, much like
apt for Ubuntu. We will use Homebrew to install CMake.
Go to Homebrew page at https://brew.sh and execute the installation command at the top of the page.
Now just run:
brew cask install cmake
And after that install latest .NET Core SDK using instructions at https://dotnet.microsoft.com/download.
Creating a cross-platfrom C++ library
Now we will create a simple C++ library with
hello() exported function which will print “Hello from [OS]” to console depending on the platform it's run on.
First of all, let's create a heder file for our library.
Let's see what happened here.
There is a standard include guard to prevent our header to be included multiple times. I'm aware of
#pragma once, but we will use old-style include guards for reasons stated here: caveats.
Library functions are not exported by default on Windows, so if you want to make some function available to be called from outside the library, you need to decorate this function with
__declspec(dllexport). And if you want to call some exported function, you need to declare it with
__declspec(dllimport) in your code.
Taking this into account we will have to distinguish between two cases:
- The header was included in the library itself and the fucntion needs to be exported.
- The header was included in some client code and the function needs to be imported.
Also, we always need to export and import our function with plain C semantics, as C++ tends to mangle function names in order to support classes. We can only call unmangled functions from .NET, so we need to always use
extern "C" on our functions.
To make our function declarations easy, we define the
LIB_API macro that takes the return type of the function as an argument. We also check for
DLL_EXPORTS macro, which we will always define when compiling our library. This way, our library will export functions defined with
LIB_API macro, and client applications will import those functions since there will be no
DLL_EXPORTS macro defined.
As you can see, we also check for
WIN32 macro, which means that our code is being compiled on Windows.
And finally, we declare our function:
This is a simple function which doesn't receive any arguments and doesn't return anything.
Now let's write the actual source code for our library:
This is a dead-simple source file. First of all, we use some compiler predefined macros to distinguish between platforms: Windows, Linux or MacOs. If you want to have more fine-grained checks and detect stuff like iOS, you will have to write more sophisticated checks. We define the
OS macro to substitute a string depending on the platform.
Then we just implement our
hello() function and print a greeting to standard output.
The last piece of the puzzle we need to compile our library is the makefile. We are using CMake, so we will have something like this:
This is, again, a very simple CMake file which will let us build our shared library under all three platforms. Notice the
DLL_EXPORTS that we define for our build.
Building the library
Now we will build our native library for each platform. Note that to use CMake under Windows, you will need to use Developer Command Prompt for VS. Find it in the Start menu. Under Linux and MacOs you can just use your terminal of choice.
We will perform an out-of-source CMake build. This means that we will create a separate build directory and store all build artifacts there. Commands are identical for all three platforms:
cd [clone_dir]/TestLib mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_GENERATOR_PLATFORM=x64 .. cmake --build . --config Release
You will find a compiled binary in
build directory on Linux and MacOs and in
build\Release directory on Windows. The library will have the
.dll extension on Windows,
.so extension on Linux and
.dylib on MacOs.
Congratulations! You've just compiled a completely cross-platfrom library ready to be called from .NET Core!
In the next post we will create a .NET Core Console app, pack our native binaries as resources and learn how to extract and call them under different platforms.