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.