Rust 程序可以链接外部 C/C++ 库,使用其中的变量、类型和函数等内容。
本文介绍 Rust 程序编译和链接 C/C++ 程序源码或库的方式。
对于外部的 C/C++ 程序,Rust 有三种集成方式:
- 源文件集成:将 C/C++ 源码集成到 Rust 项目中,在编译 Rust 代码前先自动编译 C/C++ 源码,然后让 Rust 程序自动链接生成的 C/C++ 库;
- 库文件集成:在编译 Rust 程序时链接外部的 C/C++ 库文件,库文件可以位于 Rust 项目目录中,或者所在操作系统文件目录中;
- 混合方式:混合 1 和 2;
Rust 程序链接外部 C/C++ 库的4 种方式:
- 通过
rustc -L/-l参数; - 通过
#[link]属性宏; - 通过 build.rs 在 stdout 打印编译和链接指令(cargo:: 开头的字符串);
- 通过 rustc 的 -Clink-arg 或 -Clink-args 参数;
1-3 方式是 rustc 自己链接,而 4 是 rustc 调用外部链接器(如 ld)来链接。
-Clink-arg 或 -Clink-args 链接方式 #
rustc 的 -Clink-arg 或 -Clink-args 链接方式,适合链接系统标准库,如:
# 使用 bindgen 工具从 C 头文件 hello.h 生成对应的 Rust binding 文件 hello.rs
bindgen hello.h -o src/hello.rs
# 编译 C 源文件,打包到静态库文件 libhello.a
gcc -c -o hello.o hello.c
ar -cr libhello.a hello.o
在 Rust 项目的 .cargo/config.toml 中配置链接参数:
[build]
rustflags = ["-C", "link-args=-L. -lhello"]
Rust 程序导入和使用 C 库中的 print_line 函数:
mod hello;
use std::ffi::CString;
use hello::print_line; // 导入 bindgen 生成的 Rust extern 声明
fn main() {
let s_ptr = CString::new("Hello world!").unwrap().as_ptr();
unsafe {
print_line(s_ptr);
}
}
反过来,如果要将 Rust crate 定义的函数或全局对象导出到其它语言,如 C/C++ 中使用,则需要将该 crate type 定义为 staticlib 或 cdylib。 它会将该 Rust 项目及它依赖的其它 crate 和 Rust 标准库都打包到生成的 staticlib 或 cdynlib 中,从而可以被 C/C++ 程序链接。
rustc -L/-l 链接方式 #
在 FFI 场景,如 Rust crate 程序调用外部 C/C++ 库中的函数,在编译该 crate 时,rustc 会调用系统连接器来链接该外部库。
rustc 的 -L 和 -l 参数,指定在编译 crate 时(bin、lib、proc-macro 等类型)链接的外部库的参数:
- -L
:指定外部库文件的搜索路径; - -l
: 指定链接特定外部库的参数;
例如: -l static:+whole-archive=mylib
语法: -l [KIND[:MODIFIERS]=]NAME[:RENAME]
KIND 指定链接的外部库类型:
- dylib(默认) — 动态库,如 *.so
- static — 静态库,如 *.a
- framework — macOS framework
KIND 默认为 dylib,即动态链接外部库,但如果编译静态可执行程序,则默认为 static。
- KIND 的 dylib 为外部动态库格式(native dynamic lib),对于 linux 为 *.so。而后文编译 Rust crate 时,如果指定 crate type 为 dylib,则表示生成 Rust 动态库格式。
- cdylib 为系统动态库格式;
MODIFIERS 是逗号分割的 MODIFIER 列表,每个 MODIFIER 以 + 或 - 开头,表示启用或关闭对应特性。
NAME:RENAME 用于指定链接到外部库名称和实际名称。
- NAME 是
#[link]中使用的 ATTR_NAME; - RENAME 是真实的库文件名称;
支持的 MODIFIER 如下:
whole-archive:- 只对 static KIND 有效;
- 表示完整链接该外部 static 库(而不是只链接依赖的部分),默认关闭(-whole-archive)
- bundle:
- 只对 static KIND 有效(默认对 static KIND 开启)
- +bundle 表示将外部库打包到生成的 Rust rlib(Rust 静态库格式)或 staticlib 库中;
- 如,xx-sys crate 使用的 cc crate 会将 C/C++ 程序编译为静态库,然后通过 build.rs 来生成 static KIND 类型的链接参数,从而将静态库打包到生成的 crate 的 rlib 文件中。
- verbatim:用于指示 rustc 在链接外部库时,不在 name 前后添加 lib 和 .a,而是直接使用 name 作为外部库名称;
除了 -l 参数外,rustc 还支持 -Clink-arg=xxx 来配置调用外部链接器(如 ld)时的参数,例如,链接 *.o 文件 -Clink-arg=file.o
参考:
#[link] 属性宏 #
除了使用命令行参数 -L/-l 来指定链接外部库参数,在 Rust 源码中还可以使用 #[link] 属性宏来指定 NAME、KIND、MODIFIERS:
// 静态(static)外部库 readline,将 readline 库所有内容打包到生成的 crate 库对象文件中。
//
// kind: dylib(默认)、static、framework、raw-dylib(只用于 windows)
// modifiers: +/-whole-archive, +/-bundle, +/-verbatim
#[link(name = "readline", kind = "static", modifiers = "+whole-archive")]
extern {
// extern block 中只能使用 static 变量和函数签名。
// 静态常量
static rl_readline_version: libc::c_int;
// 静态变量
static mut rl_prompt: *const libc::c_char;
// 函数签名
fn with_name(format: *const u8, args: ...);
// 变长参数函数签名( 只有 extern block 中的函数签名的最后一个参数支持变参)
fn foo(x: i32, ...);
}
xx-sys crate #
一般使用专门的 crate 来封装 C/C++ 源码和库文件,该 crate 的命名惯例是 xx-sys , 如 libduckdb-sys。
xx-sys crate 一般包含如下内容:
- C/C++ 源代码文件;
- C/++ 二进制库文件(静态库或动态库);
- C/C++ 类型、函数、全局变量和常量的 Rust 封装声明,如使用
extern "ABI" {}来声明 C/C++ 库中变量或函数;
xx-sys crate 一般还使用 build script(build.rs) 机制来实现如下功能:
- 从源码编译生成外部 C/C++ 静态库文件:一般使用
cc crate来生成静态的外部库文件; - 调用 bingen 命令或 SDK,从 C/C++ 头文件自动生成 Rust 封装声明(如 bindgen.rs,然后被导入到 src/lib.rs 中);
- 打印
cargo::编译链接指令字符串,cargo 解析后会传递给 rustc,如链接上一步生成的 C/C++ 库文件的指令;
由于 build.rs 可以从源码生成外部库文件,所以对于 C/C++ 外部库一般建议源码集成,而非二进制库集成。
- 但对于默认安装到系统标准库目录的外部库,可以在编译 crate 时直接链接(通过
#[link(name=MySysLib)]来指定),从而不需要在项目的源码中通过源码或库文件集成。
其它 Rust 项目,只需要导入该 xx-sys crate 中 extern 声明的 Rust 对象,后续构建该 Rust 项目时,cargo 会自动执行 xx-sys crate 的 build.rs。
示例:https://github.com/rusqlite/rusqlite/tree/master/libsqlite3-sys
build script #
build.rs 在 FFI 中得到广泛应用,它可以用来编译 C/C++ 源码生成静态库,然后生成 Cargo 指令来将 Rust 程序与之链接。
// https://doc.rust-lang.org/cargo/reference/build-script-examples.html
// build.rs
use std::process::Command;
use std::env;
use std::path::Path;
fn main() {
// 保存中间产物和最终库文件的目录
let out_dir = env::var("OUT_DIR").unwrap();
Command::new("gcc").args(&["src/hello.c", "-c", "-fPIC", "-o"])
.arg(&format!("{}/hello.o", out_dir))
.status().unwrap();
Command::new("ar").args(&["crus", "libhello.a", "hello.o"])
.current_dir(&Path::new(&out_dir))
.status().unwrap();
// 添加保存生成的 libhello.a 库文件的搜索路径
println!("cargo::rustc-link-search=native={}", out_dir);
// 链接生成的外部库
println!("cargo::rustc-link-lib=static=hello");
// 如果 src/hello.c 发生变化,下次重新执行该 build script
println!("cargo::rerun-if-changed=src/hello.c");
}
cargo build 在编译 Rust 源程序前先自动编译和执行 build.rs(包括依赖库的 build.rs)。
编译 build.rs 时使用 [build-dependencies] 中声明的依赖,而不使用 [dependencies] 和 [dev-dependencies]。
buid.rs 是一个带 main() 函数的可执行程序,可以执行任何业务逻辑:
- 由 cargo build 等触发编译和自动执行;
- 只能通过 cargo 传递的环境变量来获取输入参数;
- 在 stdout 输出 cargo metadata 指令(以 cargo:: 开头),用于向 cargo 传递编译、链接参数;
- 一般用于链接 build.rs 编译生成的外部库。
- Rust 代码不再需要通过
#[link]来指定链接参数。
build script 结果是有缓存的,当再次编译时,只有当 src/ 目录下的文件(或者 Cargo.toml 配置的 exclude 和 include 列表文件)或依赖包发生变化时,build script 才会被重新执行。
可以使用 cargo::rerun-if-changed 指令来自定义 change detection 算法,当指定的文件发生变化时才重新运行 build script。
build script 输入:
- 只能通过 cargo 环境变量进行配置。
- 程序的退出码为 0 时表示正常退出。
- 程序也可以返回 Result,然后由调用方来决定是否失败退出。
- cargo 为 build script 设置的环境变量列表
build script 输出:
- 生成的文件或中间数据:统一保存到
OUT_DIR环境变量指定的目录下, 脚本不应该修改除了该目录下的其它任意文件。
OUT_DIR位于 target/ 目录下,如:target/x86_64-unknown-linux-gnu/debug/build/libsqlite3-sys-ff205f18bcd8618f/out/。- 示例:
let out_dir = env::var("OUT_DIR").unwrap(); let out_path = Path::new(&out_dir).join("bindgen.rs");
- cargo 交互:需要打印到 stdout,cargo 逐行检查 script 输出,将 cargo:: 开头的行解释为 cargo 指令,cargo 再转换为调用的 rustc 的编译链接参数。
- 示例:
println!("cargo:rustc-link-lib=framework=Security"); - 注意 cargo:: 指令的顺序影响传递给 rustc 的参数顺序。
- build script 的输出默认是隐藏的,可以为 cargo 指定
-vv参数来打印输出。
- 脚本的 stdout 和 stderr 保存位置:target/debug/build/
/output
cargo 指令 #
常见的 cargo 指令:https://doc.rust-lang.org/cargo/reference/build-scripts.html
重新运行条件:
- cargo::rerun-if-changed=PATH
- cargo::rerun-if-env-changed=VAR
链接器参数:
cargo::rustc-link-arg=FLAG:对应 rustc 的-C link-arg=FLAG;cargo::rustc-link-arg-bin=BIN=FLAG:对应 rustc 的-C link-arg=FLAG,但只对名为 BIN 的 bin 有效;cargo::rustc-link-arg-bins=FLAG:对应 rustc 的-C link-arg=FLAG,对所有 bin crate type 有效;cargo::rustc-link-arg-tests=FLAG: 编译 tests 时的链接器参数;cargo::rustc-link-arg-examples=FLAG:编译 examples 时的链接器参数;cargo::rustc-link-arg-benches=FLAG:编译 benchmark 时的链接器参数;cargo::rustc-link-lib=LIB:为 Rust 程序指定要链接的外部库文件名称和类型,对应 rustc 的-l flag, 一般用于使用 FFI 链接外部库的场景。
- LIB 的格式和 rustc 的 -l 参数值格式一致:
[KIND[:MODIFIERS]=]NAME[:RENAME]
cargo::rustc-link-search=[KIND=]PATH:外部库文件搜索路径,对应 rustc 的-L flag
编译器参数:
cargo::rustc-flags=FLAGS:只能指定 rustc 的 -l 和 -L 参数,使用空格分割。等效于 rustc-link-lib 和 rustc-link-search;cargo::rustc-cfg=KEY[="VALUE"]:指定 rustc 的--cfg flag,可用于条件编译;cargo::rustc-check-cfg=CHECK_CFG:指定 rustc 的--check-cfg flag, 用于对自定义的 config name 和 value 进行检查,如果不满足则发送 WARN;
// build.rs
// 创建一个 check-cfg 配置:foo 的值必须是 bar
println!("cargo::rustc-check-cfg=cfg(foo, values(\"bar\"))");
if foo_bar_condition {
// 配置 foo,值为 bar,满足上面的 check-cfg 的要求。
println!("cargo::rustc-cfg=foo=\"bar\"");
}
环境变量参数:
- cargo::rustc-env=VAR=VALUE:设置环境变量;
- cargo::rustc-cdylib-link-arg=FLAG:为 cdylib crate 设置链接器参数;
错误提示参数:
- cargo::error=MESSAGE:在 build script 执行结束后打印一条 error 信息,并失败退出;
- cargo::warning=MESSAGE:打印 warning 消息;
元数据参数:
- cargo::metadata=KEY=VALUE:设置链接脚本使用的 Metadata;
cc crate #
上面 build script 的问题:
- 写死了 gcc 编译命令,会有跨平台的问题;
- 不支持交叉编译;
使用 cc crate 来重写 build.rs,可以更好的解决上面的问题:
// Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
// cc 自动使用系统缺省编译器;
cc::Build::new()
.file("src/hello.c")
.compile("hello");
println!("cargo::rerun-if-changed=src/hello.c");
}
cc crate 简化了集成 C/C++ 源码的编译步骤,和 build script 能很好的协作:
- 自动使用系统缺省的编译器;
- 考虑 HOST、TARGET 环境变量,给编译器传递合适的编译参数,从而 支持交叉编译 ;
- 自动处理 build.rs 环境变量,如 OPT_LEVEL, DEBUG, HOST, TARGET,自动在 stdout 生成 cargo:: 指令,自动在 OUT_DIR 环境变量对应的目录下保存文件。
- 自动向 stdout 发送 cargo:: 指令,用于指示 rustc 正确的搜索和链接编译生成的外部库。发送的指令类型如下:https://docs.rs/cc/1.2.27/cc/struct.Build.html
rustc-link-lib=static=compiled lib:rust crate 静态链接生成的外部库rustc-link-search=native=target folder
cc crate 将 C/C++ 代码编译为一个静态库,同时生成类似于上面的静态链接的 cargo 指令,最终生成一个静态链接的 Rust 可执行程序或库。
// https://docs.rs/cc/latest/cc/
// build.rs
// 编译 foo.c 和 bar.c,生成一个 libfoo.a 静态库
cc::Build::new()
.file("foo.c")
.file("bar.c")
.compile("foo"); // 编译器生成一个 lib + <name> + .a 的静态库,自动发送 cargo:: 指令。
// 不需要手动 println cargo:: 指令
然后通过 FFI 机制来使用生成的 libfoo.a 库中的 C 函数:
// 声明 libfoo.a 中提供的对象,
// 不需要指定 #[link(name = "foo")], 因为 build.rs 中的 cc 会自动生成链接 libfoo.a 的 cargo:: 指令。
// 编译该 package 时,会自动执行依赖 package 的 build.rs。
extern "C" {
fn foo_function();
fn bar_function(x: i32) -> i32;
}
pub fn call() {
unsafe {
foo_function();
bar_function(42);
}
}
fn main() {
call();
}
bindgen #
对于外部 C/C++ 项目的集成,除了需要准备对应的二进制库文件外,还需要准备的 Rust extern block 封装的 C/C++ 对象声明,如函数声明、类型声明和全局变量声明等。
bindgen crate 提供了命令行工具和 SDK,支持从 C/C++ 头文件自动生成 Rust extern block 代码。
推荐在 build.rs 中使用 bindgen SDK,这是因为 C/C++ 头文件有很多平台相关特性,在 build.rs 中使用 bindgen SDK 时,会自动感知 cargo 传递的 HOST、TARGET 等环境变量,从而生成对应平台的 bindings。
基本步骤:
- 创建一个 xx-sys crate;
- 拷贝依赖的 C/C++ 源文件(头文件、C 文件等)到 crate 目录中,示例:
- https://github.com/rusqlite/rusqlite/blob/master/libsqlite3-sys/sqlite3/sqlite3.h
- https://github.com/rusqlite/rusqlite/blob/master/libsqlite3-sys/sqlite3/sqlite3.c
- 在 xx-sys crate 的 build.rs 中:
- 调用 cc crate 来编译包含的 C/C++ 源文件和生成静态库,自动添加对应的 cargo:: 编译链接指令;
- 创建一个 wrapper.h 头文件,将所有的头文件都 include 进来,该 wrapper.h 头文件作为 bindgen 输入头文件;
- 示例:https://github.com/rusqlite/rusqlite/blob/master/libsqlite3-sys/wrapper.h
- 调用 bindgen SDK,为头文件生成对应的 Rust extern block 封装文件,一般为 buildgen.rs;
- 示例:https://github.com/rusqlite/rusqlite/blob/master/libsqlite3-sys/build.rs#L544
- 在 xx-sys crate 的 src/lib.rs 中通过 include!() 来包含生成的 bindgen.rs 文件;
- 示例:https://github.com/rusqlite/rusqlite/blob/master/libsqlite3-sys/src/lib.rs#L24
- lib.rs 导出的内容可以被其它 crate 使用,以及生成 crate docs:
- 示例:https://docs.rs/libsqlite3-sys/0.34.0/src/libsqlite3_sys/opt/rustwide/target/x86_64-unknown-linux-gnu/debug/build/libsqlite3-sys-ff205f18bcd8618f/out/bindgen.rs.html#2409
后续,其它 crate 只需导入这个 xx-sys crate 即可, 在编译该 crate 前会自动先编译导入的 xx-sys,进而先执行 xx-sys 的 build.rs 来编译静态链接的二进制以及 bindgen.rs 文件。
对于已经有库文件的 C/C++ 源码,则上面通过 cc 的源码编译就不需要了,只需要生成对应的 bindgen.rs 即可。后续使用该 xx-sys crate 时,需要通过 #[link] 或 rustc 命令行参数 -L/-l 指定静态链接的系统库名称。
cfg!() 条件编译 #
https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options
使用 #[cfg()]/#[cfg_attr()]/cfg!() 来对源码进行条件编译。
编译器根据 configuration predicate 是否为 true/false 来决定是否编译,包含如下类型:
-
A
configuration option. It is true if the option is set and false if it is unset. -
all()with a comma separated list of configuration predicates. It is false if at least one predicate is false. If there are no predicates, it is true. -
any()with a comma separated list of configuration predicates. It is true if at least one predicate is true. If there are no predicates, it is false. -
not()with a configuration predicate. It is true if its predicate is false and false if its predicate is true.
其中 Configuration options 可以是 name 或 key="value" 格式,编译器 根据 name 是否设置,或 key 是否值为 value ,进行条件编译:
- name 是单标识符,如 unix 或 windows
- key=value,key 是标识符,value 是字符串。
- key 可以重复,如
feature="xx", feature="yy"来表示根据是否启用 feature xx 和 yy 来进行条件编译. - key 和 value 前后可以有空格,如
foo="bar"等效于foo = "bar"
部分 name 或 key 是编译器自动设置的,另外一部分是在调用编译器时传入,如通过 rustc --cfg 参数来设置:
rustc --cfg "unix" program.rsrustc --cfg 'verbose' --cfg 'feature="serde"', 分别对应#[cfg(verbose)] 和 #[cfg(feature="serde")]
#[cfg(target_os = "macos")]
fn macos_only() {
// ...
}
// This function is only included when either foo or bar is defined
#[cfg(any(foo, bar))]
fn needs_foo_or_bar() {
// ...
}
#[cfg(all(unix, target_pointer_width = "32"))]
fn on_32bit_unix() {
// ...
}
// This function is only included when foo is not defined
#[cfg(not(foo))]
fn needs_not_foo() {
// ...
}
// This function is only included when the panic strategy is set to unwind
#[cfg(panic = "unwind")]
fn when_unwinding() {
// ...
}
# cfg_attr 是在条件有效时自动设置 attr
#[cfg_attr(feature = "magic", sparkles, crackles)]
fn bewitched() {}
// When the `magic` feature flag is enabled, the above will expand to:
#[sparkles]
#[crackles]
fn bewitched() {}
let machine_kind = if cfg!(unix) {
"unix"
} else if cfg!(windows) {
"windows"
} else {
"unknown"
};
println!("I'm running on a {} machine!", machine_kind);
key 可以是任意的,但是 rustc/cargo 定义了一些特定含义的 key:
- Cargo.toml 中定义的
features列表(默认都不开启):
default feature:表示未通过rustc --cfg 'feature="xx"'或cargo build --features "xx yy"启用 feature 时,默认启用的 feature 列表;feature:也被用于开启 option 的 dependencies;
- 各种预定义的
target_* key,如target_arch/target_os/target_family等;
使用 rustc --print cfg 查看条件编译宏 #[cfg] 可以使用的条件,如果要查看非 host target 的 cfg,可以指定 –target 参数。
$ rustup show |grep host
Default host: x86_64-apple-darwin
# 查看指定 target 的参数:--target=x86_64-win7-windows-msvc
# 当使用 debug、dev 构建,或指定 profile 的 opt-level=0 时,cfg(debug_assertions) 为 true。
$ rustc --print cfg
debug_assertions
overflow_checks
panic="unwind"
relocation_model="pic"
target_abi=""
target_arch="x86_64"
target_endian="little"
target_env=""
target_family="unix"
target_feature="cmpxchg16b"
target_feature="fxsr"
target_feature="lahfsahf"
target_has_atomic="128"
target_has_atomic_equal_alignment="128"
target_os="macos"
target_pointer_width="64"
target_thread_local
target_vendor="apple"
unix
$