crud89 / DotNetWithCMake

Your swiss army knife for creating .NET assemblies with CMake and integrating unmanaged code.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

.NET with CMake

This project contains a .NET dummy application that can be build with CMake. It demonstrates different project types and how to configure them.

Note that .NET projects require Visual Studio to be build. CMake only manages and sets up the required build scripts for the your Visual Studio version.

Quick-Start your Project

This repository is a template repository, which means that you can quickly create your own repository from a copy of the latest version of this repository. Simply click the Use this template button on the top and start customizing the project. Common tasks you want to do include:

  • Renaming the projects in the CMakeLists.txt files.
  • Removing projects that you do not need.
  • Changing the license.
  • Updating the readme.
  • Enjoy! 🎉

Project Structure

There are two top-level projects: WinFormsApp and WpfApp. Both projects create executables and depend on the projects CSharpLib and CppCliLib, which create managed DLL assemblies. Both of them depend on a common CommonLib project, which also is a managed DLL assembly and is used to demonstrate how to call a C# library from a C++/CLI library. The CppCliLib project also references a completely unmanaged project, called UnmanagedLib, which defines a shared dynamically linked library.

The CommonLib defines the IHello interface, which looks like this:

public interface IHello
{
    string SayHello();
    int AnswerEverything();
}

Both, the CSharpLib and CppCliLib implement a class that returns a string from this interface. The AnswerEverything method is implemented differently - the CSharpLib returns a value directly, while the CppCliLib performs a call to the UnmanagedLib.

The WpfApp and WinFormsApp both create instances of those implementations and call them using the IHello interface to display the result inside a message box.

Building

.NET support for CMake is closely tied to Windows and Visual Studio environments, hence there might be some "incompatibilities" for the general purpose. The template has not been tested with Mono or .NET Core / Linux environments.

From Command Line

Building the template from command line is straightforward:

cmake . -B build/ -A x64
cmake --build build/

If you want to instead create an x86 build, change the -A parameter to Win32.

Using Visual Studio CMake Integration

The template contains a pre-defined CMakeSettings.json file that you can use in your own project, if you want to use Visual Studios integrated CMake support.

When you are doing this on your own, you have to explicitly specify the generator, since Ninja (the default generator) currently does not support .NET. When trying to build any managed assemblies using Ninja, you will receive an error similar to this:

CMake Error: CMAKE_CSharp_COMPILER not set, after EnableLanguage.

Note on IntelliSense

The VS-integrated CMake support (i.e. the CMake target view) does not work with IntelliSense for managed CMake projects. The workaround is to open the generated solution file (build/[env]/Example.sln) from a second Visual Studio instance. I've created a bug report for this issue.

Automatically Restoring NuGet Packages

Since Visual Studio 2019, msbuild can be configured to automatically restore NuGet packages. CMake 3.15 simplified specifying NuGet package references. CMake 3.18 fixed an issue that occured when restoring NuGet packages. So if you are running Visual Studio 2019 with CMake 3.18, you can try to automatically resolve and restore NuGet dependencies. In order to add a package reference to your project, specify the following property:

SET_PROPERTY(TARGET ${PROJECT_NAME} PROPERTY VS_PACKAGE_REFERENCES "Serilog_2.9.0;Serilog.Sinks.Console_3.1.1")

This will define a package reference inside a project. When building from command line, MSBuild should detect the .csproj target and automatically restore the packages. When using the Visual Studio CMake integration, it is possible to tell MSBuild to restore package dependencies before building by specifying the -r switch. You can do this by adding a build argument in the CMakeSettings.json file:

{
  "configurations": [
    {
      "name": "x64-Debug",
      "generator": "Visual Studio 16 2019 Win64",
      "configurationType": "Debug",
      "inheritEnvironments": [ "msvc_x64" ],
      "buildRoot": "${projectDir}\\build\\${name}",
      "installRoot": "${projectDir}\\install\\${name}",
      "buildCommandArgs": "-r"
    }
  ]
}

Using NuGet-Packages in C++/CLI projects

Whilst in theory it is possible for C++/CLI projects to use NuGet packages that target the same .NET Framework version, it is currently not properly implemented by Microsoft. The naïve approach of defining a VS_PACKAGE_REFERENCE, as you would do for a C# project, will result in NuGet not beeing able to resolve the target framework for the project:

You are trying to install this package into a project that targets 'native,Version=v0.0', but the package does not contain any assembly references or content files that are compatible with that framework.

There are two possible workarounds for this issue, both of which require an intermediate C# project that has a VS_PACKAGE_REFERENCE set, so that MSBuild is able to restore the package.

  1. Write a wrapper class for all the interfaces you wish to call. Obviously not very feasible.
  2. Use a hard reference (i.e. VS_DOTNET_REFERENCE_*) to the package assembly. However, this approach is not ideal, since you have to know where the current NuGet package cache resides, which might cause upwards compatibility problems.

For more information, I've created this issue to track a possible solution for this problem.

Installing NuGet-Packages

Packages should be installed automatically, if they are defined as VS_PACKAGE_REFERENCE. However, there appears to be an issue, if the reference is only defined in the top level (or executable) project. It will be restored during build, but CMake apparently is unable to pick it up and copy it during install. I am not sure if this is an CMake bug, or some configuration issue. I could not find any issues within the project files, but couldn't be bothered to investigate this further. A quick workaround is to define a intermediate C# library project, move all the references there and add a dependency to the executable. If you have any further information on this problem, open an issue in this repository or (if it's actually related to CMake) at the CMake repository.

Building Managed Assemblies as AnyCPU

The top-level CMakeLists.txt file checks the generator used to build the project and sets the target platform accordingly. Note that AnyCPU assemblies might cause problems when loaded from an unmanaged context. This is why this template explicitly sets the target platform to either x64 or x86, depending on which generator platform is used. In case an unsupported platform is detected or none is provided (using the command line parameter -A), the script will issue a warning and default to AnyCPU.

If you know what you are doing and want to explicitly build AnyCPU assemblies anyway, you have to set the CMAKE_CSharp_FLAGS accordingly:

SET(CMAKE_CSharp_FLAGS "/platform:AnyCPU")

Debugging

In order to debug build results under Windows, you need to ensure, that the build artifacts are output into a common directory. If they do not reside in the same directory, any attempt in starting the application will result in errors due to missing references. This template achieves this by setting the RUNTIME_OUTPUT_DIRECTORY to the same intermediate folder for all projects:

SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/binaries/")

This allows to properly debug the application, if the solution has been created from the command line and/or has been opened using Visual Studio directly. The integrated CMake support (the one with the IntelliSense problems, as mentioned above) can also launch the debugger from the installation directory.

Mixed-Mode Debugging

In order to step into (or set breakpoints in) mixed-mode C++/CLI assemblies or unmanaged libraries, the linker options /DEBUG and /ASSEMBLYDEBUG need to be set for the C++ projects:

TARGET_LINK_OPTIONS(${PROJECT_NAME} PUBLIC /DEBUG /ASSEMBLYDEBUG)

The CppCliLib and UnmanagedLib projects use a generator expression to only use those options in Debug and RelWithDebInfo builds.

Stepping into Unmanaged Code

In order to step into unmanaged code, the top-level executable project needs to have the EnableUnmanagedDebugging setting be set to true. Also the unmanaged target needs to generate a proper debug database file (.pdb), which is done by default in Debug and RelWithDebInfo build modes. To enable native code debugging for those modes in the top-level projects, the template set thes VS_GLOBAL_EnableUnmanagedDebugging project property in those cases:

IF(NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE MATCHES "Debug|RelWithDebInfo")
    SET_PROPERTY(TARGET ${PROJECT_NAME} PROPERTY VS_GLOBAL_EnableUnmanagedDebugging "true")
ENDIF (NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE MATCHES "Debug|RelWithDebInfo")

About

Your swiss army knife for creating .NET assemblies with CMake and integrating unmanaged code.

License:MIT License


Languages

Language:CMake 80.2%Language:C# 12.6%Language:C++ 7.2%