无法在 C++ 项目中使用 CXX link Rust 编写的库

Failing to use CXX to link Rust-written library in C++ project

我正在用一个非常简单的项目 CXX 测试 link 一个 Rust 库到 C++ 可执行文件中。

我写了一个 foo() -> () Rust 函数并尝试从 C++ 访问它但是 linker 没有找到它。

这是我的:

// 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();
}

运行 cargo build 生成 target\debug\libcpprust.so。 然后我尝试使用(编辑:g++ 命令不正确,请参阅问题末尾的注释)来创建项目:

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

这里有什么问题?

编辑:prog-fh 的出色回答正确地指出我需要将 build.rs 包含在 C++ 编译中,即使没有 C++ 在板条箱中编译和访问.但是,即使在实施了他们的回答之后,我仍然收到相同的错误消息。事实证明我还有另外两个问题:1) 我对 g++ 的参数的 order 不正确,我也需要 pthread -l dl。它应该是: g++ -o cpprust main.cpp -I ../target/cxxbridge -L../target/debug -lcpprust -pthread -l dl 2) 我的 Cargo.toml 文件也生成了 "rlib", "dylib" 库类型,但不知何故也会导致上述错误;它仅在生成 staticlib 时有效。

考虑到 this documentationbuild.rs 脚本应该生成您尝试中丢失的 lib.rs.h

注意文档中的示例认为主程序来自Rust,C++代码是一个扩展。 在你的问题中,情况恰恰相反:你的主程序来自 C++,但由一些 Rust 代码扩展。

这个答案由两部分组成:

  • 一个与你的非常相似的最小示例(没有要从 Rust 调用的 C++ 代码),
  • 一个更完整的C++和Rust双向交互的例子(但是主程序还是在C++端)

编辑以回答评论中的后续问题

如下面第二个 build.rs 的评论所述,.compile("cpp_from_rust") 中选择的名称将用于命名包含已编译 C++ 代码的库(例如 libcpp_from_rust.a) . 然后 Rust 将使用该库来扩展 Rust 代码:Rust 生成的 libcpprust.a 主要目标包含 libcpp_from_rust.a.

如果在 .compile() 之前没有提供 C++ 文件(如下面第一个最小示例),此 C++ 库仅包含启用从 C++ 进行 extern "Rust" 访问的符号。

$ 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

另一方面,您已经在文档中发现允许多次调用 .file() 以便为 C++ 库提供来自各种源文件的更多代码。

另一个问题是关于我们希望 Rust 生成的库类型。 This documentation 列举了 Rust 可以生成的各种二进制目标,尤其是各种库。 由于在您最初的问题中您希望主要可执行文件位于 C++ 端,这意味着 Rust 应该生成一个可以被视为 system 库的库,而不是特定于 Rust 的库,因为在生成可执行文件时将不再涉及 Rust。 在上述文档中,我们可以看到只有 staticlibcdylib 适合这种用法。

在我的示例中,为了简单起见,我选择了staticlib,但也可以使用cdylib。 然而,它有点复杂,因为主库 (libcpprust.so) 是动态生成的,Rust 不会将 C++ 库 (libcpp_from_rust.a) 插入其中;因此,我们不得不link反对这个C++库,这不是很方便。

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

当然,因为我们现在处理的是共享库,所以我们必须在运行时找到它。

$ LD_LIBRARY_PATH=target/debug ./cpp_program

我不知道某些其他类型的库 (crate-type) 是否可以(偶然地)与此 C++ 主程序一起工作,但文档指出只有 staticlibcdylib 用于此用途。

最后,请注意,如果您在 Cargo.toml 中使用 crate-type = ["staticlib", "rlib", "dylib"] (如您评论中所述),您将生成三个库:

  • target/debug/libcpprust.a 来自 staticlib,
  • target/debug/libcpprust.rlib 来自 rlib,
  • target/debug/libcpprust.so 来自 dylib.

不幸的是,当 link 使用命令 g++ ... -l cpprust ... 时,link 用户将 更喜欢 .so .a;您将处于与上述 cdylib 相同的情况。


最小示例的目录布局

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;
}

cargo build命令会在target/debug中生成libcpprust.a个静态库。 构建主程序只需依赖常用的命令,前提是我们找到相关的头文件和库(见代码中的注释)。

请注意,主程序的 C++ 源代码位于此处的 src 目录中,但它可以放在其他任何地方。


双向示例的目录布局

cpprust
├── Cargo.toml
├── build.rs
└── src
    ├── cpp_from_rust.cpp
    ├── cpp_from_rust.hpp
    ├── lib.rs
    └── main.cpp

我们刚刚添加了一对 .hpp/.cpp 个文件。

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");
}

请注意,这次构建过程实际上处理了一些要从 Rust 调用的 C++ 代码(见下文)。

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.tomlsrc/main.cpp 和构建过程(cargo buildg++ ...)仍然与前面的示例相同。