How can I build a managed C++/CLI DLL library using CMake?

801 views Asked by At

I need to build a C++/CLI DLL as part of a larger CMake build. I found no preferred way to do this when I checked the documentation.

I tried to set up the CMake file for building a regular shared library:

cmake_minimum_required(VERSION 3.20.0)

project(ExampleProject LANGUAGES CXX)

add_library(Example SHARED)
add_subdirectory(src)  # All source files are added here...

target_compile_features(Example PRIVATE cxx_std_17)
target_compile_definitions(Example PRIVATE
    UNICODE _UNICODE WIN32 DEBUG)
target_compile_options(Example PRIVATE
    /CLR /MP /W3 /Zi /Od /MDd /Zc:__cplusplus)
target_link_options(Example PRIVATE
    /DEBUG /LTCG /NXCOMPAT /DYNAMICBASE)

Yet, CMake adds the option /RTC1 incompatible with /CLR. So I wonder if there is managed C++ support for visual studio already built in CMake.

How do I build a managed C++/CLI DLL library using CMake?

2

There are 2 answers

0
Tsyvarev On BEST ANSWER

CMake provides COMMON_LANGUAGE_RUNTIME target property for adjust "managed" aspects of C++ code.

E.g. for make the target to contain a mixed (unmanaged/managed) code, set this property to the empty string:

set_target_properties(Example PROPERTIES COMMON_LANGUAGE_RUNTIME "")

Note, that property COMMON_LANGUAGE_RUNTIME is supported only for Visual Studio generators. E.g. for Ninja generator configured with cl compiler the property will have no effect.

0
Flovdis On

While the answer from Tsyvarev works fine with Visual Studio generators (as he explained), making a build work with regular C++ code and C++/CLI code universally (e.g. ninja build) requires more steps.

See the following minimal example CMakeLists.txt file:

cmake_minimum_required(VERSION 3.20.0)

project(ExampleProject LANGUAGES CXX)

set(CMAKE_CXX_FLAGS "")
set(CMAKE_CXX_FLAGS_DEBUG "")
set(CMAKE_CXX_FLAGS_RELEASE "")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "")

add_library(Example SHARED)
add_subdirectory(src) # All sources are added here

foreach(_lib_name mscorlib.dll System.dll System.Core.dll System.Data.dll System.DirectoryServices.dll System.DirectoryServices.AccountManagement.dll)
    list(APPEND _cli_compile_libs "/FU${_lib_name}")
endforeach()
if(CMAKE_BUILD_TYPE MATCHES "Release|RelWithDebInfo")
    target_compile_definitions(Example PRIVATE
            UNICODE _UNICODE WIN32 _WINDOWS NDEBUG)
    target_compile_options(Example PRIVATE
            /MP /W3 /Zi /O2 /MD /GL /Zc:__cplusplus /CLR ${_cli_compile_libs})
    target_link_options(Example PRIVATE
            /DEBUG /LTCG /NXCOMPAT /DYNAMICBASE /OPT:ICF /CLR)
else()
    target_compile_definitions(Example PRIVATE
            UNICODE _UNICODE WIN32 _WINDOWS DEBUG)
    target_compile_options(Example PRIVATE
            /MP /W3 /Zi /Od /MDd /Zc:__cplusplus /CLR ${_cli_compile_libs})
    target_link_options(Example PRIVATE
            /DEBUG /NXCOMPAT /DYNAMICBASE /CLR)
endif()

In the first step, the CMake variables CMAKE_CXX_FLAGS... are emptied. This has to be done before the add_library statement to prevent the contents from being copied into the target.

set(CMAKE_CXX_FLAGS "")
set(CMAKE_CXX_FLAGS_DEBUG "")
set(CMAKE_CXX_FLAGS_RELEASE "")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "")

The problem, why this is necessary, are the flags /RTC1 and /EHsc in these variables cause a conflict when sources are built with /CLR. By clearing these variables, you can manually control all flags for the individual targets with the target_... statements.

In this example I only cover the Debug, Release and ReleaseWithDebInfo build profiles. If you use more or different ones, you must also clear the corresponding variables.

foreach(_lib_name mscorlib.dll System.dll System.Core.dll System.Data.dll System.DirectoryServices.dll System.DirectoryServices.AccountManagement.dll)
    list(APPEND _cli_compile_libs "/FU${_lib_name}")
endforeach()

The next section prepares /FU options that include required .NET libraries for your build.

if(CMAKE_BUILD_TYPE MATCHES "Release|RelWithDebInfo")

The if statement applies different flags for debug and release builds.

    target_compile_definitions(Example PRIVATE
            UNICODE _UNICODE WIN32 _WINDOWS NDEBUG)
    target_compile_options(Example PRIVATE
            /MP /W3 /Zi /O2 /MD /GL /Zc:__cplusplus /CLR ${_cli_compile_libs})
    target_link_options(Example PRIVATE
            /DEBUG /LTCG /NXCOMPAT /DYNAMICBASE /OPT:ICF /CLR)

I use target_compile_definitions, target_compile_options and target_link_options to apply the flags only for one target. Most of the definitions and flags in my example are required defaults for the Visual Studio 2017 compiler to build C++/CLI DLL.

The flags in the example build a release library but create a debug database for the release and the debug builds.

Further Notes

  • When the flags in the CMAKE_CXX_... variables are cleared, they affect all following add_library and add_executable targets. Therefore, if you like to affect only this single library, backup the contents of all these variables and restore them at the end of your CMake file.
  • I extracted this example from our local build system. If you work with large projects with many targets like this, write a small CMake library that allows you to apply all these settings with a single function call. It will make maintenance and upgrading much easier.
function(my_compiler_setup)
    set(options APPLICATION LIBRARY SHARED CLI)
    set(oneValueArgs TARGET)
    set(multiValueArgs "")
    cmake_parse_arguments(CXX_SETUP "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    # Configure the target here...
endfunction()

After writing this function, you can import it and set up the target like this:

cmake_minimum_required(VERSION 3.20.0)

project(ExampleProject LANGUAGES CXX)

add_library(Example SHARED)
add_subdirectory(src)
my_compiler_setup(TARGET Example LIBRARY SHARED CLI)

The statements that clear the variables with the flags are placed in the main CMake project file to ensure they do not affect any target in your build.