Blog

Rust CXX and Corrosion with MSVC debug CRT

Recently I wanted to integrate a Rust crate into our existing C++ project that is using CMake for the build tool but uses Corrosion Rust binaries. Previously we only had C++ projets and Rust projects. For the first time I wanted to write a static lib in Rust and link it to one of our C++ executables.

I decided to try the CXX crate since it looks like it provides a lot of nice things for building a FFI. It started smoothly but I ran into some issues with MSVC C Runtime and debug builds. Basically, on Windows Rust always links to the release version of the CRT even when building debug crates.

The root problem occurs when compiling a C++ project linking to a Rust project that internally compiles and links to another C++ project. If compiler flags are different enough between the two C++ compilation steps and then problems can occur

- C++ executable
    |-- Rust staticlib
        |-- C++ staticlib

When using the CXX crate, it automatically builds its internal C++ code using the cc crate as part of its build.rs. Since Rust automatically always uses the release C Runtime on windows+MSVC, linking to the Rust lib we get the following errors:

[build] my_rust.lib(cxx.o) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in cmake_pch.cxx.obj
[build] my_rust.lib(cxx.o) : error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MD_DynamicRelease' doesn't match value 'MDd_DynamicDebug' in cmake_pch.cxx.obj

This is currently a known limitation with Rust:

While there are options to switch between the dynamic and static runtime we still cannot control between the MSVC debug/release runtimes.

As an attempted workaround, I tried to compile the Rust crate to a dynamic library in debug and only use a static lib in release, e.g.

[package]
name = "cpp-rust-cpp"
version = "0.0.1"

[lib]
crate-type = ["staticlib", "cdylib", "lib"]

[dependencies]
cxx = "1.0"

Then in our CMake project we conditionally include the dynamic lib in debug while using the static lib in release, avoiding the runtime library issues:

# TODO: Dynamic vs.

set (CRATE_TYPES "bin" "staticlib")
if (CMAKE_BUILD_TYPE EQUALS "Debug")
    set (CRATE_TYPES "bin" "cdylib")
endif()

corrosion_import_crate(MANIFEST_PATH cpp-rust-cpp/Cargo.toml CRATE_TYPES ${CRATE_TYPES})

corrosion_add_cxxbridge(cpp-rust-cpp-cxx
        CRATE cpp-rust-cpp
        FILES lib.rs
)

However this blew up in my face and I started to get link errors with symbols missing from CXX itself. It seems this is also a known issue: https://github.com/dtolnay/cxx/issues/1153.

In the end I just gave up and switched to cbindgen and I manage the FFI boundary manually.

Ironically, after switching and finishing my implementation using cbindgen I found the corrosion actually has some guidance for this case and makes two recommendations on their common issues section.

The first recommendation is to compile the main C++ library also with the release CRT to avoid any conflicts. This is not really a good solution for me as the debug CRT has advantages when developing. The second option is to manually set some compiler flags environment variables so that when cc is invoked it uses differnt flags, e.g. corrosion_set_env_vars(your_rust_lib "CFLAGS=-MDd" "CXXFLAGS=-MDd"). This feels a bit hacky to me but I will test it in the future if reconsidering CXX. In some ways I'm glad that I had issues with CXX as it revealed to me while it's a wonderful library with powerful abstractions it does introduce a fair amount of complexity to the project.