Reputation: 5722
I am testing CXX with a very simple project to link a Rust library into a C++ executable.
I write a foo() -> ()
Rust function and try to access it from C++ but the linker does not find it.
Here's what I have:
// lib.rs
#[cxx::bridge]
mod ffi {
extern "Rust" {
pub fn foo() -> ();
}
}
pub fn foo() -> () {
println!("foo")
}
# Cargo.toml
[package]
name = "cpprust"
version = "0.1.0"
edition = "2021"
[lib]
name = "cpprust"
path = "src/lib.rs"
crate-type = ["staticlib", "rlib", "dylib"] # EDIT: this is incorrect, see note at the end of question
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cxx = "1.0"
// main.cpp
void foo(); // I tried including lib.rs.h but it was not generated!
int main() {
foo();
}
Running cargo build
generates target\debug\libcpprust.so
.
I then try to make the project with (EDIT: g++
command is incorrect, see note at the end of question):
g++ -L../target/debug/ -lcpprust -o cpprust main.cpp
/tmp/ccOA8kJy.o: In function `main':
main.cpp:(.text+0x5): undefined reference to `foo()'
collect2: error: ld returned 1 exit status
make: *** [Makefile:2: cpprust] Error 1
What is wrong here?
EDIT: prog-fh's great answer correctly points out that I need to include build.rs
with C++ compilation, even without having C++ to compile and access within the crate. However, even after implementing their answer, I was still getting the same error message. It turns out that I had two other problems: 1) the order of my arguments to g++
were incorrect, and I needed pthread -l dl
as well. It should have been:
g++ -o cpprust main.cpp -I ../target/cxxbridge -L../target/debug -lcpprust -pthread -l dl
2) My Cargo.toml
file was also generating "rlib", "dylib"
library types, but that somehow also causes the error above; it works when only staticlib
is generated.
Upvotes: 8
Views: 4766
Reputation: 1
If you are windows and building in VSCode. You might want to add the following to .cargo/config.toml, else the c++ build would complain of mismatch detected for 'RuntimeLibrary'
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
Upvotes: 0
Reputation: 16805
Considering this documentation, the build.rs
script should generate the lib.rs.h
which was missing in your attempt.
Note that the example in the documentation considers that the main program comes from Rust, and that the C++ code is an extension. In your question, it is the opposite situation: your main program comes from C++ but is extended by some Rust code.
This answer is made of two parts:
edit to answer subsequent questions in the comments
As said in the comment of the second build.rs
below, the name chosen in .compile("cpp_from_rust")
will be used to name a library containing the compiled C++ code (libcpp_from_rust.a
for example).
This library will then be used by Rust to extend the Rust code: the libcpprust.a
main target produced by Rust contains libcpp_from_rust.a
.
If no C++ file is provided before .compile()
(as in the first, minimal example below), this C++ library only contains the symbols enabling extern "Rust"
access from C++.
$ nm ./target/debug/build/cpprust-28371278e6cda5e2/out/libcpp_from_rust.a
lib.rs.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T _Z13rust_from_cppv
U cxxbridge1$rust_from_cpp
On the other hand, you already found in the documentation that multiple invocations of .file()
are allowed in order to provide the C++ library with much more code from various source files.
Another question was about the kind of library we want Rust to produce.
This documentation enumerates the various binary targets Rust can produce, especially various kinds of libraries.
Since in your original question you wanted the main executable to be on the C++ side, this means that Rust should produce a library which can be considered as a system library, not a Rust specific one, because Rust won't be involved anymore when generating the executable.
In the aforementioned documentation, we can see that only staticlib
and cdylib
are suitable for this usage.
In my examples, I chose staticlib
for the sake of simplicity, but cdylib
can be used too.
However, it is a bit more complicated because, as the main library (libcpprust.so
) is generated as a dynamic one, Rust does not insert the C++ library (libcpp_from_rust.a
) into it; thus, we have to link against this C++ library, which is not very convenient.
g++ -std=c++17 -o cpp_program src/main.cpp \
-I .. -I target/cxxbridge \
-L target/debug -l cpprust \
-L target/debug/build/cpprust-28371278e6cda5e2/out -l cpp_from_rust \
-pthread -l dl
And of course, because we are now dealing with a shared library, we have to find it at runtime.
$ LD_LIBRARY_PATH=target/debug ./cpp_program
I don't know whether some other kinds of libraries (crate-type
) could work (by chance) with this C++ main program or not, but the documentation states that only staticlib
and cdylib
are intended for this usage.
Finally, note that if you use crate-type = ["staticlib", "rlib", "dylib"]
in Cargo.toml
(as stated in your comment), you will produce three libraries:
target/debug/libcpprust.a
from staticlib
,target/debug/libcpprust.rlib
from rlib
,target/debug/libcpprust.so
from dylib
.Unfortunately, when linking with the command g++ ... -l cpprust ...
, the linker will prefer the .so
to the .a
; you will be in the same situation as cdylib
above.
The layout of the directory for the minimal example
cpprust
├── Cargo.toml
├── build.rs
└── src
├── lib.rs
└── main.cpp
Cargo.toml
[package]
name = "cpprust"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"]
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
build.rs
fn main() {
// This will consider the ffi part in lib.rs in order to
// generate lib.rs.h and lib.rs.cc
// minimal example: no C++ code to be called from Rust
cxx_build::bridge("src/lib.rs")
.compile("cpp_from_rust");
}
src/lib.rs
#[cxx::bridge]
mod ffi {
extern "Rust" {
fn rust_from_cpp() -> ();
}
}
pub fn rust_from_cpp() -> () {
println!("called rust_from_cpp()");
}
src/main.cpp
/*
Building this program happens outside of the cargo process.
We simply need to link against the Rust library and the
system libraries it depends upon
g++ -std=c++17 -o cpp_program src/main.cpp \
-I .. -I target/cxxbridge \
-L target/debug -l cpprust \
-pthread -l dl
*/
// consider the ffi part of Rust code
#include "cpprust/src/lib.rs.h"
#include <iostream>
int
main()
{
std::cout << "starting from C++\n";
rust_from_cpp();
std::cout << "finishing with C++\n";
return 0;
}
The cargo build
command will generate the libcpprust.a
static library in target/debug
.
Building the main program simply relies on usual commands, provided that we find the relevant headers and libraries (see the comments in the code).
Note that the C++ source code for the main program is in the src
directory here, but it could have been put anywhere else.
The layout of the directory for the bidirectional example
cpprust
├── Cargo.toml
├── build.rs
└── src
├── cpp_from_rust.cpp
├── cpp_from_rust.hpp
├── lib.rs
└── main.cpp
We just added a pair of .hpp
/.cpp
files.
build.rs
fn main() {
// This will consider the ffi part in lib.rs in order to
// generate lib.rs.h and lib.rs.cc
// The generated library (libcpp_from_rust.a) contains the code
// from cpp_from_rust.cpp and will be inserted into the generated
// Rust library (libcpprust.a).
cxx_build::bridge("src/lib.rs")
.file("src/cpp_from_rust.cpp")
.flag_if_supported("-std=c++17")
.compile("cpp_from_rust");
}
Note that this time the build process actually handles some C++ code (see below) to be called from Rust.
src/lib.rs
#[cxx::bridge]
mod ffi {
extern "Rust" {
fn rust_from_cpp() -> ();
}
unsafe extern "C++" {
include!("cpprust/src/cpp_from_rust.hpp");
fn cpp_from_rust() -> ();
}
}
pub fn rust_from_cpp() -> () {
println!("entering rust_from_cpp()");
ffi::cpp_from_rust();
println!("leaving rust_from_cpp()");
}
src/cpp_from_rust.hpp
#ifndef CPP_FROM_RUST_HPP
#define CPP_FROM_RUST_HPP
// declare a usual C++ function (no Rust involved here)
void
cpp_from_rust();
#endif // CPP_FROM_RUST_HPP
src/cpp_from_rust.cpp
#include "cpp_from_rust.hpp"
#include <iostream>
// define a usual C++ function (no Rust involved here)
void
cpp_from_rust()
{
std::cout << "called " << __func__ << "()\n";
}
Cargo.toml
, src/main.cpp
and the build process (cargo build
, g++ ...
) still are the same as in the previous example.
Upvotes: 7