Rust 很棒,但是 C/C++ 有时候更香,尤其是有已有库、或者是写超级 unsafe 的代码时。

p.s. Why only C deserves? 大概有两条原因

  • C-FFI 已经成了所有语言兼容的接口标准
  • C compiler 在任何平台上都有

怎么在 Rust 里调用其他语言呢?

理解 Cargo

我们首先得理解 Cargo 做了什么。

大家都知道,多文件的编译过程就是:分别编译成 object 或 library,然后 link 起来。这其中,最关键的难题 是找到文件。我们在编译 C/C++ 时最头痛的就是 not foundundefined symbol 满天飞。

Cargo 是怎么组织编译产物的呢?

对于单个 Crate Dep,它会先(如果存在)在 target/${MODE}/build/${CRATE}-hash 下运行 build script, 然后在 target/${MODE}/build/${CRATE}-anotherhash 下编译,并将产物文件参与其被依赖的编译之中。

对于依赖图,就是不断取;然后把最后一项任务的产物放在 target/${MODE}/build/ 下。

我们的 foreign library 只需要能构建出产物放在产物文件夹中,被 Cargo 找到就行。

链接库

最朴素的做法是和传统 C 一样,编译出库,然后链接它。

在 Rust 里提供了 rustc-link-xx 的 api。

// build.rs
println!("cargo::rustc-link-search=/usr/local/lib");
println!("cargo::rustc-link-lib=uv");

这样 /usr/local/liblibuv 会被链接。

怎么引用库呢?回顾 C 里面,我们用头文件声明了接口,从而能够被链接器链接。

rust 里我们可以在任意位置 extern "C" 声明 C 接口

对于以下的代码

typedef struct{
    char* buf;
    int len;
} name_t;
void foo(name_t *);

应声明

extern "C" {
  pub fn foo(arg1: *mut Name);
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Name { // type name can be changed, but not suggested
    pub buf: *mut core::ffi::c_char,
    pub len: core::ffi::c_int,
}

Bindgen

手动编写接口过于麻烦了。因此官方开发了 bindgen 自动生成接口。

# Cargo.toml
[build-dependencies]
bindgen = {version = "0.69", feature = ["core_ffi_c"]}
// build.rs
let bindings = bindgen::Builder::default()
    .header("src/bindings.h")
    .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
    .generate().expect("Unable to generate bindings")
    // binding generated
    .write_to_file(
        PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs")
    ).expect("Couldn't write bindings!");

实际使用中建议在源码中维护一个 bindgen 出来的文件,只在 C 接口改动时重 bindgen,减少编译时间。

利用其他编译工具

知道了链接外部库的方法,我们可以利用其他编译工具编译完,然后 link。

Rust 里可以轻松地运行子命令

// build.rs
use std::process::Command;

let uv_build=Path::new("deps/libuv/build/");
let uv_output="../output/";

Command::new("mkdir")
    .arg(uv_build)
    .status().unwrap();

if !Command::new("cmake").arg("..")
    .arg(format!("-DCMAKE_INSTALL_PREFIX={uv_output}"))
    .current_dir(uv_build)
    .status().unwrap()
    .success(){panic!("failed make build dir for libuv");}

if !Command::new("cmake").arg("--build").arg(uv_build)
    .arg("--target").arg("install")
    .status().unwrap()
    .success(){panic!("failed run cmake for libuv");}

println!("cargo:rustc-link-search={}/lib", uv_build.join(uv_output).display());
println!("cargo:rustc-link-lib=uv");

这就可以使用 CMake 编译。当然这过于不优雅了,而且会有玄学兼容性问题。下面会有解决方案。

cc-rs

当你的库是 C-Rust 混编库时,除了搓 Makefile,你还可以用 build.rscc 模块来编译 C/C++ 项目。

# Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
cc::Build::new()
    .files([
        "src/clib/foo.c",
        "src/clib/bar.c"
    ])
    .compile("sb");

这样我们的 libsb 库就被编译出来啦!怎么感觉比 Makefile/CMake 还好用。 最大的好处是,rustc-link-librustc-link-search 被自动添加了,不需要我们手动维护。

cmake-rs

当你的库是 CMake 库时,可以用 cmake-rs 来高兼容性编译

# Cargo.toml
[build-dependencies]
cmake = "0.1"
// build.rs
let dst = cmake::Config::new("deps/libuv").build();
println!("cargo:rustc-link-search={}", dst.display());
println!("cargo:rustc-link-lib=uv");

优雅极了!就是为什么不像 cc 一样自动添加…

附录

Syntax: cargo::rustc-link-search=[KIND=]PATH where KIND may be one of:

Param KIND

  • all(default) — Search for all library kinds in this directory.
  • dependency — Only search for transitive dependencies in this directory.
  • crate — Only search for this crate’s direct dependencies in this directory.
  • native — Only search for native libraries in this directory.
  • framework — Only search for macOS frameworks in this directory.

Syntax: cargo::rustc-link-lib=[KIND[:MODIFIERS]=]NAME[:RENAME]

Param KIND

  • dylib — A native dynamic library.
  • static — A native static library (such as a .a archive).
  • framework — A macOS framework.

Param MODIFIERS

  • whole-archive (static only) — +whole-archive means that the static library is linked as a whole archive without throwing any object files away.
  • bundle (static only) — When building a rlib or staticlib +bundle means that the native static library will be packed into the rlib or staticlib archive, and then retrieved from there during linking of the final binary.
  • verbatim — +verbatim means that rustc itself won’t add any target-specified library prefixes or suffixes (like lib or .a) to the library name, and will try its best to ask for the same thing from the linker.