Skip to content

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

Open
@Tim-Evans-Seequent

Description

@Tim-Evans-Seequent

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    linkingIssues that manifest as link failureswindowsIssues that manifest on Windows

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions