Rust 很棒,但是 C/C++ 有时候更香,尤其是有已有库、或者是写超级 unsafe 的代码时。
p.s. Why only C deserves? 大概有两条原因
- C-FFI 已经成了所有语言兼容的接口标准
- C compiler 在任何平台上都有
怎么在 Rust 里调用其他语言呢?
理解 Cargo
我们首先得理解 Cargo 做了什么。
大家都知道,多文件的编译过程就是:分别编译成 object 或 library,然后 link 起来。这其中,最关键的难题
是找到文件。我们在编译 C/C++ 时最头痛的就是 not found
和 undefined 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/lib
的 libuv
会被链接。
怎么引用库呢?回顾 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.rs
中 cc
模块来编译 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-lib
与 rustc-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
一样自动添加…
附录
rustc-link-search
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.
rustc-link-lib
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.