This is an example of how to create a Modern CMake C++/.Net Project.
This project aim to explain how you build a .NetStandard2.0 native (win-x64,
linux-x64 and osx-x64) nuget multiple package using
.NET Core CLI and the
new .csproj format.
e.g. You have a cross platform C++ library (using a CMake based build) and a .NetStandard2.0 wrapper on it thanks to SWIG.
Then you want to provide a cross-platform Nuget package to consume it in a .NetCoreApp3.1 project...
You'll need:
- "CMake >= 3.18".
- ".Net Core SDK >= 3.1" to get the dotnet cli.
note: We won't/can't rely on VS 2019 since we want a portable cross-platform dotnet/cli pipeline.
The project layout is as follow:
-
CMakeLists.txt Top-level for CMake based build.
-
cmake Subsidiary CMake files.
- dotnet.cmake All internall .Net CMake stuff.
-
ci Root directory for continuous integration.
-
Foo Root directory for
Foolibrary.- CMakeLists.txt for
Foo. - include public folder.
- src private folder.
- dotnet
- CMakeLists.txt for
Foo.Net. - foo.i SWIG .Net wrapper.
- CMakeLists.txt for
- CMakeLists.txt for
-
Bar Root directory for
Barlibrary.- CMakeLists.txt for
Bar. - include public folder.
- src private folder.
- dotnet
- CMakeLists.txt for
Bar.Net. - bar.i SWIG .Net wrapper.
- CMakeLists.txt for
- CMakeLists.txt for
-
FooBar Root directory for
FooBarlibrary.- CMakeLists.txt for
FooBar. - include public folder.
- src private folder.
- dotnet
- CMakeLists.txt for
FooBar.Net. - foobar.i SWIG .Net wrapper.
- CMakeLists.txt for
- CMakeLists.txt for
-
dotnet Root directory for .Net template files
Mizux.DotnetNative.runtime.csproj.incsproj template for the .Net "native" (i.e. RID dependent) package.Mizux.DotnetNative.csproj.incsproj template for the .Net package.Test.csproj.incsproj template for .Net test project.Example.csproj.incsproj template for .Net example project.
-
tests Root directory for tests
- CMakeLists.txt for
DotnetNative.Test.Net. FooTests.csCode of the Mizux.DotnetNative.FooTests project.
- CMakeLists.txt for
-
examples Root directory for examples
- CMakeLists.txt for
DotnetNative.FooApp.Net. FooApp.csCode of theDotnetNative.FooAppapp.
- CMakeLists.txt for
To complexify a little, the CMake project is composed of three libraries (Foo, Bar and FooBar) with the following dependencies:
Foo:
Bar:
FooBar: PUBLIC Foo PRIVATE BarTo Create a native dependent package we will split it in two parts:
- A bunch of
Mizux.DotnetNative.runtime.{rid}.nupkgpackages for each Runtime Identifier (RId) targeted containing the native libraries. - A generic package
Mizux.DotnetNative.nupkgdepending on each runtime packages and containing the managed .Net code.
Actually, You don't need a specific variant of .Net Standard wrapper, simply omit the library extension and .Net magic will pick
the correct native library.
ref: https://www.mono-project.com/docs/advanced/pinvoke/#library-names
note: Microsoft.NetCore.App packages
follow this layout.
note: While Microsoft use runtime-<rid>.Company.Project for native libraries
naming, it is very difficult to get ownership on it, so you should prefer to use
Company.Project.runtime-<rid> instead since you can have ownership on
Company.* prefix more easily.
We have two use case scenario:
-
Locally, be able to build a Mizux.DotnetNative package which only target the local
OS Platform, i.e. building for only one Runtime Identifier (RID).
note: This is useful since the C++ build is a complex process for Windows, Linux and MacOS. i.e. We don't support cross-compilation for the native library generation. -
Be able to create a complete cross-platform (ed. platform as multiple rid) Mizux.DotnetNative package.
i.e. First you generate each native Nuget package (Mizux.DotnetNative.runtime.{rid}.nupkg) on each native architecture, then copy paste these artifacts on one native machine to generate the meta-packageMizux.DotnetNative.
Let's start with scenario 1: Create a Local only Mizux.DotnetNative.nupkg package targeting one
Runtime Identifier (RID).
We would like to build a Mizux.DotnetNative.nupkg package which only depends
on one Mizux.DotnetNative.runtime.{rid}.nupkg in order to work locally.
The pipeline for linux-x64 should be as follow:
note: The pipeline will be similar for
osx-x64 and win-x64 architecture,
don't hesitate to look at the CI log.
disclaimer: In this git repository, we use CMake and SWIG.
Thus we have the C++ shared library libFoo.so and the SWIG generated .Net wrapper Foo.cs.
note: For a C++ CMake cross-platform project sample, take a look at Mizux/cmake-cpp.
note: For a C++/Swig CMake cross-platform project sample, take a look at Mizux/cmake-swig.
So first let's create the local Mizux.DotnetNative.runtime.{rid}.nupkg nuget package.
Here some dev-note concerning this Mizux.DotnetNative.runtime.{rid}.csproj.
- Once you specify a
RuntimeIdentifierthendotnet buildordotnet build -r {rid}will behave identically (save you from typing it).- note: it is NOT the case if you use
RuntimeIdentifiers(notice the 's')
- note: it is NOT the case if you use
- It is recommended
to add the tag
nativeto the nuget package tags<PackageTags>native</PackageTags>
- This package is a runtime package so we don't want to ship an empty assembly file:
<IncludeBuildOutput>false</IncludeBuildOutput>
- Add the native (i.e. C++) libraries to the nuget package in the repository
runtimes/{rid}/native. e.g. for linux-x64:<Content Include="*.so"> <PackagePath>runtimes/linux-x64/native/%(Filename)%(Extension)</PackagePath> <Pack>true</Pack> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content>
- Generate the runtime package to a defined directory (i.e. so later in
Mizux.DotnetNativepackage we will be able to locate it)<PackageOutputPath>{...}/packages</PackageOutputPath>
Then you can generate the package using:
dotnet pack Mizux.DotnetNative.runtime.{rid}note: this will automatically trigger the dotnet build.
If everything good the package (located where your PackageOutputPath was defined) should have this layout:
{...}/packages/Mizux.DotnetNative.runtime.{rid}.nupkg:
\- Mizux.DotnetNative.runtime.{rid}.nuspec
\- runtimes
\- {rid}
\- native
\- *.so / *.dylib / *.dll
...
note: {rid} could be linux-x64 and {framework} could be netstandard2.0
tips: since nuget package are zip archive you can use unzip -l <package>.nupkg
to study their layout.
So now, let's create the local Mizux.DotnetNative.nupkg nuget package which will
depend on our previous runtime package.
Here some dev-note concerning this DotnetNative.csproj.
- Add the previous package directory:
<RestoreSources>{...}/packages;$(RestoreSources)</RestoreSources>
- Add dependency (i.e.
PackageReference) on each runtime package(s) availabe:Thanks to the<ItemGroup> <RuntimeLinux Include="{...}/packages/Mizux.DotnetNative.runtime.linux-x64.*.nupkg"/> <RuntimeOsx Include="{...}/packages/Mizux.DotnetNative.runtime.osx-x64.*.nupkg"/> <RuntimeWin Include="{...}/packages/Mizux.DotnetNative.runtime.win-x64.*.nupkg"/> <PackageReference Include="Mizux.DotnetNative.runtime.linux-x64" Version="1.0" Condition="Exists('@(RuntimeLinux)')"/> <PackageReference Include="Mizux.DotnetNative.runtime.osx-x64" Version="1.0" Condition="Exists('@(RuntimeOsx)')" /> <PackageReference Include="Mizux.DotnetNative.runtime.win-x64" Version="1.0" Condition="Exists('@(RuntimeWin)')" /> </ItemGroup>
RestoreSourcewe can work locally we our just builded package without the need to upload it on nuget.org.
Then you can generate the package using:
dotnet pack Mizux.DotnetNativeIf everything good the package (located where your PackageOutputPath was
defined) should have this layout:
{...}/packages/Mizux.DotnetNative.nupkg:
\- Mizux.DotnetNative.nuspec
\- lib
\- {framework}
\- Mizux.DotnetNative.dll
...
note: {framework} could be netcoreapp3.1 or/and net6.0
We can test everything is working by using the Mizux.DotnetNative.FooApp or Mizux.DotnetNative.FooTests project.
First you can build it using:
dotnet build <build_dir>/dotnet/FooApp
note: Since Mizux.DotnetNative.FooApp PackageReference Mizux.DotnetNative and add {...}/packages to the RestoreSource.
During the build of DotnetNative.FooApp you can see that Mizux.DotnetNative and
Mizux.DotnetNative.runtime.{rid} are automatically installed in the nuget cache.
Then you can run it using:
dotnet run --project <build_dir>/dotnet/FooApp/FooApp.csproj
note: Contrary to dotnet build and dotnet pack you must use --project
before the .csproj path (let's call it "dotnet cli command consistency")
You should see:
$ dotnet run --project build/dotnet/FooApp/FooApp.csproj
[1] Enter DotnetNativeApp
[2] Enter Foo::staticFunction(int)
[3] Enter freeFunction(int)
[3] Exit freeFunction(int)
[2] Exit Foo::staticFunction(int)
[1] Exit DotnetNativeAppLet's start with scenario 2: Create a Complete Mizux.DotnetNative.nupkg package
targeting multiple
Runtime Identifier (RID).
We would like to build a Mizux.DotnetNative.nupkg package which depends on several
Mizux.DotnetNative.runtime.{rid}.nupkg.
The pipeline should be as follow:
note: This pipeline should be run on any architecture,
provided you have generated the three architecture dependent Mizux.DotnetNative.runtime.{rid}.nupkg
nuget packages.
Like in the previous scenario, on each targeted OS Platform you can build the
coresponding Mizux.DotnetNative.runtime.{rid}.nupkg package.
Simply run on each platform:
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Releasenote: replace {rid} by the Runtime Identifier associated to the current OS platform.
Then on one machine used, you copy all other packages in the {...}/packages so
when building Mizux.DotnetNative.csproj we can have access to all package...
This is the same step than in the previous scenario, since we "see" all runtime
packages in {...}/packages, the project will depends on each of them.
Once copied all runtime package locally, simply run:
dotnet build <build_dir>/dotnet/Mizux.DotnetNative
dotnet pack <build_dir>/dotnet/Mizux.DotnetNativeWe can test everything is working by using the Mizux.DotnetNative.FooApp or Mizux.DotnetNative.FooTests project.
First you can build it using:
dotnet build <build_dir>/dotnet/FooApp
note: Since Mizux.DotnetNative.FooApp PackageReference Mizux.DotnetNative and add {...}/packages to the RestoreSource.
During the build of Mizux.DotnetNative.FooApp you can see that Mizux.DotnetNative and
Mizux.DotnetNative.runtime.{rid} are automatically installed in the nuget cache.
Then you can run it using:
dotnet run --project <build_dir>/dotnet/FooApp/FooApp.csproj
You should see something like this
$ dotnet run --project build/dotnet/FooApp/FooApp.csproj
[1] Enter DotnetNativeApp
[2] Enter Foo::staticFunction(int)
[3] Enter freeFunction(int)
[3] Exit freeFunction(int)
[2] Exit Foo::staticFunction(int)
[1] Exit DotnetNativeAppFew links on the subject...
.Net runtime can deduce library extension so don’t use a platform-specific
library name in the DllImport statement.
Instead, just use the library name itself, without any prefixes or suffixes,
and rely on the runtime to find the appropriate library at runtime.
ref: Mono pinvoke#libraryname
- CMake Reference Documentation
- https://llvm.org/docs/CMakePrimer.html
- https://cliutils.gitlab.io/modern-cmake/
- https://cgold.readthedocs.io/en/latest/
- Common MSBuild project properties
- MSBuild well-known item metadata
- Additions to the csproj format for .NET Core
Some issue related to this process
PackageReferenceonly supportTargetFrameworkcondition- Nuget needs to support dependencies specific to target runtime #1660
- Improve documentation on creating native packages #238
- Guide for packaging C# library using P/Invoke
Image has been generated using plantuml:
plantuml -Tsvg docs/{file}.dotSo you can find the dot source files in docs.
Apache 2. See the LICENSE file for details.
This is not an official Google product, it is just code that happens to be owned by Google.