Description
What I'm trying to do is use CXX and cargo in "cdylib" mode to build my Rust code into a shared library (DLL) that exports the API defined by the extern "Rust"
block in my Rust code.
My Rust code in lib.rs
looks like this:
use cxx::*;
#[derive(Debug)]
pub struct OverflowError {}
impl std::error::Error for OverflowError {}
impl std::fmt::Display for OverflowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "integer overflow")
}
}
pub fn add(left: i32, right: i32) -> Result<i32, OverflowError> {
left.checked_add(right).ok_or(OverflowError {})
}
#[cxx::bridge(namespace = cxx_msvc_shared)]
mod ffi {
extern "Rust" {
fn add(left: i32, right: i32) -> Result<i32>;
}
}
And my build.rs
looks like this:
fn main() {
cxx_build::bridge("src/lib.rs")
.flag_if_supported("-std=c++17")
.compile("cxx_msvc_shared");
println!("cargo:rerun-if-changed=src/lib.rs");
}
All simple and obvious and it compiles correctly. The generated C++ code for the add
function look like this:
::std::int32_t add(::std::int32_t left, ::std::int32_t right) {
::rust::MaybeUninit<::std::int32_t> return$;
::rust::repr::PtrLen error$ = cxx_msvc_shared$cxxbridge1$add(left, right, &return$.value);
if (error$.ptr) {
throw ::rust::impl<::rust::Error>::error(error$);
}
return ::std::move(return$.value);
}
So I can see that the base function comes from my Rust code and there is a wrapper defined in the C++ to do the error handling. That all look good.
Then I tried to use this DLL in some C++ code and it failed to link. It turns out that cxx_msvc_shared::add
is not exported from the DLL, and neither is rust::cxxbridge1::Error::what
. Looking at it with dumpbin.exe
these are the exports that match \badd\b
:
1 0 00001D30 cxx_msvc_shared$cxxbridge1$add = cxx_msvc_shared$cxxbridge1$add
That is the the function from the Rust code but where is the C++ wrapper function?
The problem here is that the Windows linker link.exe
does not export symbols by default. You need to either add the symbols to a *.def
file and pass that (what Rust itself does AFAICT but it deletes that file after compiling), pass each symbol individually with /export:name
, or add __declspec(dllexport)
before the function in the C++ code. CXX does none of those things so the symbols don't get exported and the DLL is unusable.
To fix this I first tried to add those __declspec(dllexport)
attributes to the C++ code after it is generated. I did this with code in build.rs
that ran after cxx_build::bridge
is called but before calling compile
on the build object. I'm pretty sure I got the right file (C:/Work/experiments/cxx_msvc_shared/target/debug/build/cxx_msvc_shared-9083c676d5a1ba67/out/cxxbridge/sources/cxx_msvc_shared/src/lib.rs.cc
) and I can see the modifications in there but the symbols still don't get exported. I have not been able to work out why. This was the contents of build.rs
with these changes:
use std::fs::{read_to_string, write};
const PATH: &str = "C:/Work/experiments/cxx_msvc_shared/target/debug/build/cxx_msvc_shared-9083c676d5a1ba67/out/cxxbridge/sources/cxx_msvc_shared/src/lib.rs.cc";
fn main() {
let mut build = cxx_build::bridge("src/lib.rs");
let cc = read_to_string(PATH)
.unwrap()
.replace(
"::std::int32_t add",
"__declspec(dllexport) ::std::int32_t add",
)
.replace(
"class Error final : public std::exception",
"class __declspec(dllexport) Error final : public std::exception",
);
write(PATH, cc).unwrap();
build
.flag_if_supported("-std=c++17")
.compile("cxx_msvc_shared");
println!("cargo:rerun-if-changed=src/lib.rs");
}
The second thing I tried was passing extra flags to the linker for each symbol, by adding these lines to my build.rs
instead:
println!("cargo:rustc-link-arg=/EXPORT:add");
println!("cargo:rustc-link-arg=/EXPORT:?what@Error@cxxbridge1@rust@@UEBAPEBDXZ");
This worked! The problem is that finding the correct names for each symbol and pasting them in here isn't really practical. These two new lines appear in the dumpbin.exe
output, and these are the functions I'm looking for:
2 0 00030700 ?what@Error@cxxbridge1@rust@@UEBAPEBDXZ = ?what@Error@cxxbridge1@rust@@UEBAPEBDXZ (public: virtual char const * __cdecl rust::cxxbridge1::Error::what(void)const )
1 1 000480D0 add = ?add@cxx_msvc_shared@@YAHHH@Z (int __cdecl cxx_msvc_shared::add(int,int))
So I think the fix is for CXX to either add the __declspec(dllexport)
attributes to all the functions it generates or output the right mangled names itself as exported symbols from the build
function. Ideally the attributes would work; I don't know why they didn't work when I put them in myself. Any ideas?
I have also attached my whole test project here, with the hack in build.rs
to make it work:
cxx_msvc_shared.tar.gz