最近一直在写 Rust,并且希望在当前的 C++ 项目中使用 Rust 来实现一些新功能,计划像忒修斯之船一样逐步替换现有的 C++ 模块。折腾了一下,发现 CMake 项目引入 Rust 还是比较简单的。TLDR,点击此处访问样例,按 CI 步骤编译非常简单。
这里使用 cxx 生成绑定接口,约定在 crate 的 src 目录下增加一个 cxx.rs
文件声明桥接的代码。目录结构如下:
.
├── build.rs
├── Cargo.toml
└── src
├── cxx.rs
└── lib.rs
lib.rs
的内容:
mod cxx;
#[derive(Default)]
pub struct RustType {
pub value: i32,
}
impl RustType {
fn inc(&mut self) {
self.value += 1;
}
fn show(&self) {
println!("current value is {}", self.value);
}
}
pub fn concat_two_strings(a: &str, b: &str) -> String {
a.to_string() + b
}
mod tests {
#[test]
fn test_concat_two_strings() {
assert_eq!(super::concat_two_strings("1", "2"), "12".to_string());
}
}
cxx.rs
的内容:
use crate::*;
#[::cxx::bridge]
mod ffi {
extern "Rust" {
type RustType;
fn inc(&mut self);
fn show(&self);
fn create_a_rust_object() -> Box<RustType>;
fn concat_two_strings(a: &str, b: &str) -> String;
}
}
fn create_a_rust_object() -> Box<RustType> {
Default::default()
}
build.rs
的内容:
fn main() {
let _ = cxx_build::bridge("src/cxx.rs");
println!("cargo:rerun-if-changed=src/cxx.rs");
}
Cargo.toml
的内容:
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
[lib]
crate-type = ["staticlib"]
执行 cargo build
后,会在 target/cxxbridge
目录下生成对应的桥接代码,这里生成的 C++ 接口如下:
struct RustType;
#ifndef CXXBRIDGE1_STRUCT_RustType
#define CXXBRIDGE1_STRUCT_RustType
struct RustType final : public ::rust::Opaque {
void inc() noexcept;
void show() const noexcept;
~RustType() = delete;
private:
friend ::rust::layout;
struct layout {
static ::std::size_t size() noexcept;
static ::std::size_t align() noexcept;
};
};
#endif // CXXBRIDGE1_STRUCT_RustType
::rust::Box<::RustType> create_a_rust_object() noexcept;
::rust::String concat_two_strings(::rust::Str a, ::rust::Str b) noexcept;
还是比较容易理解的。返回 Rust 对象时返回的是指针,不支持访问内部对象,也就不用在乎 Rust 与 C++ 在内存布局上的差异了。
为了保持最大的灵活与兼容,这里引入 Cargo 的 Workspace 概念,在整个项目的顶层声明 Workspace。样例的项目文件结构如下:
.
├── Cargo.toml
├── cmake
│ └── add_crate.cmake
├── CMakeLists.txt
├── LICENSE
├── README.md
└── src
├── CMakeLists.txt
├── demo
│ ├── build.rs
│ ├── Cargo.toml
│ └── src
│ ├── cxx.rs
│ └── lib.rs
└── main
├── CMakeLists.txt
└── main.cc
顶层的 Cargo.toml
内容如下。使用 Workspace 囊括项目中所有的 Rust 模块,方便直接执行 cargo build/test
等命令,产物也统一生成在顶层的 target
文件夹中。
[workspace]
members = [
"src/demo"
]
resolver = "2"
[workspace.package]
authors = ["SF-Zhou <sfzhou.scut@gmail.com>"]
edition = "2021"
license = "MIT"
[profile.release-cmake]
debug = true
inherits = "release"
lto = true
增加 add_crate.cmake
用以编译和引入 Rust 项目,其内容如下:
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CARGO_CMD cargo build)
set(TARGET_DIR "debug")
else ()
set(CARGO_CMD cargo build --release)
set(TARGET_DIR "release")
endif ()
add_custom_target(
cargo_build_all ALL
COMMAND ${CARGO_CMD}
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
)
macro(add_crate NAME)
set(LIBRARY "${PROJECT_SOURCE_DIR}/target/${TARGET_DIR}/lib${NAME}.a")
set(SOURCES
"${PROJECT_SOURCE_DIR}/target/cxxbridge/${NAME}/src/cxx.rs.h"
"${PROJECT_SOURCE_DIR}/target/cxxbridge/${NAME}/src/cxx.rs.cc"
)
add_custom_command(
OUTPUT ${SOURCES} ${LIBRARY}
COMMAND ${CARGO_CMD}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${NAME}"
)
add_library(${NAME} STATIC ${SOURCES} ${LIBRARY})
target_link_libraries(${NAME} pthread dl ${LIBRARY})
target_include_directories(${NAME} PUBLIC "${PROJECT_SOURCE_DIR}/target/cxxbridge")
target_compile_options(${NAME} PRIVATE -Wno-dollar-in-identifier-extension)
add_dependencies(${NAME} cargo_build_all)
endmacro()
src/CMakeLists.txt
的内容:
add_crate(demo) # 引入 demo 目录下的 Rust 项目,生成同名的 library
add_subdirectory(main)
src/main/CMakeLists.txt
的内容:
add_executable(main main.cc)
target_link_libraries(main demo) # 增加 Rust 生成的 library 依赖
src/main/main.cc
的内容:
#include "demo/src/cxx.rs.h"
#include <iostream>
#include <string_view>
int main() {
auto result = concat_two_strings("hello", " world!");
std::cout << std::string_view(result.data(), result.size()) << std::endl;
auto a = create_a_rust_object();
a->show();
a->inc();
a->show();
}
使用全套的 LLVM 工具链,引入 LTO 优化。在顶层目录增加 .cargo/config.toml
文件:
[build]
rustflags = ["-Clinker=clang++-16", "-Clink-arg=-fuse-ld=lld"]
注意改成你使用的编译器版本,另外安装对应的 lld
工具。然后在 CMake 中增加一行 add_link_options(-fuse-ld=lld)
。