Skip to content

Questions about using Cargo to build a Rust library used from C++ #291

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

Closed
Timmmm opened this issue Sep 10, 2020 · 10 comments
Closed

Questions about using Cargo to build a Rust library used from C++ #291

Timmmm opened this issue Sep 10, 2020 · 10 comments
Labels

Comments

@Timmmm
Copy link

Timmmm commented Sep 10, 2020

I'm trying to set up a simple Rust library with a C++ interface, based on the demo in this repo, using Cargo. I have quite a few questions, hope that's ok:

  1. In build.rs it passes src/demo.cc to the code generator/compiler. If I'm only exposing Rust code to C++, and not the other way around, presumably I only need this if I have custom C++ bridging code, e.g. friendlier wrappers of the shared structs?

  2. If I remove these two lines:

     .file("src/demo.cc")
     .flag_if_supported("-std=c++14")
    

then I get C++11 errors, in other words C++11 is always required. But doesn't that mean that the compiler should add -std=c++11 even if you don't explicitly pass that flag? Also what happens if you're using MSVC? I would have expected an API like .cpp_standard(CppStandard::cxx14).

  1. I got a basic program working. After many link errors I figured out I have to link with the Rust dynamic library (libdemo.dylib) and with the cxxbridge static library (libcxxbridge-demo.a) and the cxx library (libcxxbridge04.a). Can you explain what all those are?

  2. libcxxbridge-demo.a and libcxxbridge04.a don't seem to get copied to a canonical location. In other words I have to link with target/debug/build/demo-78993d207bfe982b/out/libcxxbridge-demo.a and target/debug/build/cxx-f3cb2e64024d3552/out/libcxxbridge04.a. That doesn't seem right? I'd have expected them all to be copied or moved to target/debug/.

  3. If I have a function like this: fn get_cpp_string() -> CxxString; it says returning C++ string by value is not supported. Is that a permanent thing? I'd really like the exposed C++ API to use standard C++ types rather than cxx types because it is a public API rather than something internal. I can't see any reason why you couldn't return a std::string by value.

Thanks!

@dtolnay
Copy link
Owner

dtolnay commented Sep 11, 2020

In build.rs it passes src/demo.cc to the code generator/compiler. If I'm only exposing Rust code to C++, and not the other way around, presumably I only need this if I have custom C++ bridging code, e.g. friendlier wrappers of the shared structs?

You likely still need it. Unless the generated file is empty (which you can check by running the code generator CLI command on the source file, cxxbridge path/to/lib.rs), there is important stuff in there.

@dtolnay
Copy link
Owner

dtolnay commented Sep 11, 2020

If I remove these two lines:

    .file("src/demo.cc")
    .flag_if_supported("-std=c++14")

then I get C++11 errors, in other words C++11 is always required. But doesn't that mean that the compiler should add -std=c++11 even if you don't explicitly pass that flag?

Those are not from this crate. You are looking at https://docs.rs/cc/1.0.59/cc/struct.Build.html#method.file and https://docs.rs/cc/1.0.59/cc/struct.Build.html#method.flag_if_supported which are from https://github.com/alexcrichton/cc-rs.

We don't pre-add a std flag to the returned cc::Build because if your cxx::bridge module contains an include! which relies on a newer standard, or if you were adding additional of your own source files into the build and those need a newer standard, then we have no way to tell. Various compilers may not put up with duplicate inconsistent std flags on the same compiler invocation.

Also what happens if you're using MSVC?

I believe MSVC defaults to c++14 so this still works.

For newer standards you would need to pass a flag formatted as /std:c++17 etc.

I would have expected an API like .cpp_standard(CppStandard::cxx14).

A cross platform abstraction for compiler flags is outside the scope of this crate. It would be a good thing for somebody to develop (or might already exist; I have not looked) but shouldn't be coupled to the cxx crate. Other forms of building C++ inside build.rs would want that too.

@dtolnay
Copy link
Owner

dtolnay commented Sep 11, 2020

I got a basic program working. After many link errors I figured out I have to link with the Rust dynamic library (libdemo.dylib) and with the cxxbridge static library (libcxxbridge-demo.a) and the cxx library (libcxxbridge04.a). Can you explain what all those are?

libdemo.dylib (or libdemo.so or libdemo.dll depending on platform) would be the Rust crate containing the cxx::bridge invocation inside a crate called demo.

libcxxbridge-demo.a is the generated C++ code, from:

.compile("cxxbridge-demo");

libcxxbridge04.a is the handwritten C++ component of the cxx crate, from:

cxx/build.rs

Line 7 in 2599f88

.compile("cxxbridge04");

@dtolnay
Copy link
Owner

dtolnay commented Sep 11, 2020

libcxxbridge-demo.a and libcxxbridge04.a don't seem to get copied to a canonical location. In other words I have to link with target/debug/build/demo-78993d207bfe982b/out/libcxxbridge-demo.a and target/debug/build/cxx-f3cb2e64024d3552/out/libcxxbridge04.a. That doesn't seem right? I'd have expected them all to be copied or moved to target/debug/.

This is a Cargo thing 🙁. If you are able to use a build system that's built to support multi-language builds with C++ code depending on Rust code (Bazel, etc) then you would never deal with this.

If you need this to work with Cargo such that a cargo build sticks compiled dependencies at the top of target/debug/, you could try adding this kind of thing to the end of your build script, but it builds in assumptions about how Cargo is laying out its target directory that may not be stable.

use std::env;
use std::fs;
use std::path::Path;

let out_dir = env::var("OUT_DIR").unwrap();
let staticlib = Path::new(&out_dir).join("libcxxbridge-demo.a");
let put = staticlib
    .parent().unwrap()
    .parent().unwrap()
    .parent().unwrap()
    .parent().unwrap()
    .join("libcxxbridge-demo.a");
fs::copy(&staticlib, put).unwrap();

@dtolnay
Copy link
Owner

dtolnay commented Sep 11, 2020

If I have a function like this: fn get_cpp_string() -> CxxString; it says returning C++ string by value is not supported. Is that a permanent thing? I'd really like the exposed C++ API to use standard C++ types rather than cxx types because it is a public API rather than something internal. I can't see any reason why you couldn't return a std::string by value.

You can't make or have a std::string by value in Rust because it has a move constructor, which is incompatible with Rust's language level move behavior. For example a small string optimized std::string implementation may rely on internal pointers that need to be repointed during moves.

If you need to expose std::string for the public C++ signature, you could make it a wrapper that calls the private bridge signature, where the bridge produces either RustString and the wrapper uses operator std::string to copy out of it, or produces UniquePtr<CxxString> and the wrapper uses std::move(*ptrstring) to move out of it and drop the ptr's allocation.

@Timmmm
Copy link
Author

Timmmm commented Sep 11, 2020

Thanks for the answers - very helpful!

you can check by running the code generator CLI command on the source file

This might be a stupid question, but how do I install the cxxbridge command?

You can't make or have a std::string by value in Rust because it has a move constructor, which is incompatible with Rust's language level move behavior.

Ah that makes sense. I can't really imagine why a std::string move constructor would in practice be incompatible with Rust. Here's libc++'s implementation and as far as I can tell it just moves the stack data, and then zeroes the source string (which presumably wouldn't be necessary in Rust because you can't access it anyway). But I guess it could do something more than that so it makes sense not to risk it.

you could make it a wrapper that calls the private bridge signature

Yeah I think that is what I will end up doing. I was kind of hoping cxx would take care of that for me (kind of like SWIG does) but I guess that is a higher level problem than it is trying to solve.

you could try adding this kind of thing to the end of your build script, but it builds in assumptions about how Cargo is laying out its target directory that may not be stable.

That works, I also came up with this, but I'm not sure it is any better (probably won't work with cross compilation):

    let out_dir = env::var("OUT_DIR").unwrap();
    let staticlib = Path::new(&out_dir).join("libcxxbridge-demo.a");
    let this_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let profile = env::var("PROFILE").unwrap();

    let put = Path::new(&this_dir).join("target").join(profile).join("libcxxbridge-demo.a");
    fs::copy(&staticlib, put).unwrap();

Thanks for the help!

@Timmmm Timmmm closed this as completed Sep 11, 2020
@dtolnay
Copy link
Owner

dtolnay commented Sep 11, 2020

This might be a stupid question, but how do I install the cxxbridge command?

cargo install cxxbridge-cmd (this is shown in the readme as part of the non-Cargo workflow)


I can't really imagine why a std::string move constructor would in practice be incompatible with Rust.

For example standard library implementations with the following string layout, which takes advantage of not needing a branch for dereferences of the data pointer.

long strings             short strings
   +---+                     +---+
   |ptr to heap              |ptr to data
   +---+                     +---+
   |length                   |data
   +---+                     |   |
   |capacity                 |   |
   +---+                     +---+

From #250 (comment), "So far as I know that is indeed something that our standard library does (and even if it didn't, I wouldn't want to guarantee that it wouldn't in future)".

@dtolnay dtolnay changed the title Some questions Questions about using Cargo to build a Rust library used from C++ Sep 11, 2020
@Jashwanth537
Copy link

@Timmmm did you face similar errors in 3. ?

image

@Timmmm
Copy link
Author

Timmmm commented Jul 8, 2023

Doesn't ring a bell, but that looks like you're trying to link two libraries that have each been linked with different versions of Microsoft's Visual C++ Runtime Library (MSVCRT). It comes in a few different flavours - statically/dynamically linked, debug/release, single/multithreaded, etc.

I have no idea how that could have happened though sorry! I'd suggest building with all the commands shown (however you do that) and finding the flags that are set to choose which library to use.

@Jashwanth537
Copy link

Cargo defaults to MSVC dynamic release by default. by adding in the rustflags = ["-Ctarget-feature=+crt-static"], i was able to get a static build but not sure for a debug build.
I have a legacy visual c++ project that has MSVC runtime library /MTd. @dtolnay any help on how do i set cargo / cxx to MSVC /MTd build?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants