在 CMake C++ 环境中引入 Rust

2024.03.23
SF-Zhou

最近一直在写 Rust,并且希望在当前的 C++ 项目中使用 Rust 来实现一些新功能,计划像忒修斯之船一样逐步替换现有的 C++ 模块。折腾了一下,发现 CMake 项目引入 Rust 还是比较简单的。TLDR,点击此处访问样例,按 CI 步骤编译非常简单。

1. 生成绑定接口

这里使用 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++ 在内存布局上的差异了。

2. CMake 引入 Rust

为了保持最大的灵活与兼容,这里引入 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();
}

3. 优化性能

使用全套的 LLVM 工具链,引入 LTO 优化。在顶层目录增加 .cargo/config.toml 文件:

[build]
rustflags = ["-Clinker=clang++-16", "-Clink-arg=-fuse-ld=lld"]

注意改成你使用的编译器版本,另外安装对应的 lld 工具。然后在 CMake 中增加一行 add_link_options(-fuse-ld=lld)

References

  1. cxx
  2. XiangpengHao/cxx-cmake-example