Skip to content

Can't build a working DLL with cargo on Windows #1153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Tim-Evans-Seequent opened this issue Jan 17, 2023 · 4 comments
Open

Can't build a working DLL with cargo on Windows #1153

Tim-Evans-Seequent opened this issue Jan 17, 2023 · 4 comments
Labels
linking Issues that manifest as link failures windows Issues that manifest on Windows

Comments

@Tim-Evans-Seequent
Copy link

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

@dtolnay dtolnay added windows Issues that manifest on Windows linking Issues that manifest as link failures labels Jan 17, 2023
@Tim-Evans-Seequent
Copy link
Author

Tim-Evans-Seequent commented Jan 18, 2023

I have found why __declspec(dllexport) wasn't working. The C++ code is first built into a static .lib file then that lib is linked into the main DLL that Rust and Cargo produce. By default functions exported inside static libs aren't re-exported but the /WHOLEARCHIVE:library_name.lib linker option will re-export them. Start with the build.rs that adds the dllexport attributes and add this command:

println!("cargo:rustc-link-arg=/WHOLEARCHIVE:cxx_msvc_shared.lib");

That gets the add function into the DLL, but it doesn't get Error::what. Turns out that will need a dllexport attribute added wherever it is actually defined in cxx.cc and it's own /WHOLEARCHIVE command in build.rs.

Typically these attributes would be defined using a preprocessor command like this:

#if defined(_MSC_VER)
#   define CXX_DLL_EXPORT __declspec(dllexport)
#else
#   define CXX_DLL_EXPORT 
#endif

If you want to generate the same code for static libs and DLLs you might want to include some other test in there to allow switching between static and DLL mode at compile time rather than generate time.

It is also customary to have that CXX_DLL_EXPORT evaluate to __declspec(dllimport) in public header files for things that will be found in a DLL you're linking to. I'm less sure if that is necessary.

My adjusted build.rs looked like this:

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",
    );
    write(PATH, cc).unwrap();
    build
        .flag_if_supported("-std=c++17")
        .compile("cxx_msvc_shared");
    println!("cargo:rerun-if-changed=src/lib.rs");
    println!("cargo:rustc-link-arg=/WHOLEARCHIVE:cxx_msvc_shared.lib");
}

@Tim-Evans-Seequent
Copy link
Author

Tim-Evans-Seequent commented Jan 18, 2023

Trying these same workarounds on slightly more real code, I also found missing symbols with names like ?data@?$Vec@UFoundPoint@myrtle@@@cxxbridge1@rust@@QEBAPEBUFoundPoint@myrtle@@XZ. That is the data() member function of a particular instance of the rust::cxxbridge1::Vec template. I don't know how to export that sort of thing from a DLL with MSVC sadly.

I would lean toward putting templates like Box, Vec, and Slice into the header as inline functions. No DLL export/import problems, the code can be inlined for speed, and it matches what the C++ STL already does for templates like std::vector.

@IlTeo285
Copy link

Any news on this topic? I'm facing similar issue with static library on windows.

@spangaer
Copy link

spangaer commented Dec 5, 2024

Building the Rust FFI C-ABI generated Cpp wrappers in the client application seem to work out for me so far.

Documented in great detail here
#880 (comment)

See

include_directories(${RUST_TARGET}/cxxbridge/my-cxx/src)
add_executable(my-examples-cpp src/main.cpp ${RUST_TARGET}/cxxbridge/my-cxx/src/lib.rs.cc)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
linking Issues that manifest as link failures windows Issues that manifest on Windows
Projects
None yet
Development

No branches or pull requests

4 participants