Rust 编译和链接三方 C/C++ 源码&库 #
Rust crate 可以链接外部 C/C++ 库,使用其中的变量、类型和函数等内容。
对于外部的 C/C++ 程序,Rust 有三种集成方式:
- 源文件集成:将 C/C++ 源码集成到 Rust 项目中,在编译 Rust 代码前先编译 C/C++ 源码生成库文件,然后让 Rust 程序链接;
- 库文件集成:在编译 Rust 程序时链接:
- 库文件位于 Rust 程序目录中,即 Rust 源码自包含;
- 库文件位于系统库文件目录中;
- 混合方式:1 和 2 混合的方式。
Rust crate 链接外部库有 4 种方式:
- rustc -L/-l 参数;
#[link]
属性宏;- build.rs 在 stdout 打印 cargo:: 编译和链接指令;
- rustc 的 -Clink-arg 或 -Clink-args 参数;(适合链接系统标准库)
前 1-3 方式都是 rustc 自己链接,而第四种是 rustc 调用外部的链接器(如 ld)来链接。
-Clink-arg 或 -Clink-args 链接方式例子:
# 文件 hello.rs 由 hello.h 生成
bindgen hello.h -o src/hello.rs
# 编译并归档生成 libhello.a 静态链接文件
gcc -c -o hello.o hello.c
ar -cr libhello.a hello.o
# 文件 .cargo/config.toml 配置链接参数
[build]
rustflags = ["-C", "link-args=-L. -lhello"]
# 文件 main.rs
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: 指定编译 crate Rust 源码时链接的外部库文件参数 #
在 FFI 场景,如 crate Rust 程序调用外部 C/C++ 库中的函数,在编译该 crate 时,rustc 会调用系统连接器来链接该外部库。
rustc 的 -L 和 -l 参数,指定在编译 crate 时(bin、lib、proc-macro 等类型)链接的外部库(native libray)的参数。
- -L
:指定外部库文件的搜索路径; - -l
: 指定链接特定外部库的参数;
Syntax: -l [KIND[:MODIFIERS]=]NAME[:RENAME]
KIND
指定链接的某一个外部库的类型:
- dylib — A native dynamic library. 外部动态库格式 *.so
- static — A native static library (such as a .a archive). 外部静态库格式 *.a
- framework — A macOS framework.
MODIFIERS 是逗号分割的 MODIFIER 列表,每个 MODIFIER 以 + 或 - 开头,表示启用或关闭对应特性。例如: -l static:+whole-archive=mylib
KIND 默认为 dylib(系统动态库格式),即动态链接外部库。但如果编译静态可执行程序,则默认为 static
。
- 注:KIND 的 dylib 为外部动态库格式(native dynamic lib),一般为 *.so。而后面的 crate type 的 dylib 为 Rust 动态库格式,cdylib 为系统动态库格式;
NAME:RENAME 用于指定链接到外部库名称和实际名称。
- NAME 是
#[link]
中使用的 ATTR_NAME; - RENAME 是真实的库文件名称;
支持的 MODIFIER 如下:
- whole-archive:
- 只对 static KIND 有效
- 表示完整链接该外部 static 库(而不是只链接依赖的部分),默认关闭(-whole-archive)
- bundle:
- 只对 static KIND 有效(默认对 static KIND 开启)
- 只对 crate type 为 rlib 或 staticlib 的 Rust 项目有效;
- +bundle 表示将外部库打包到生成的 Rust rlib 或 staticlib 库中;
- 例如,各种 xx-sys crate 默认是 rlib 类型,cc crate 会生成 static KIND 链接的 cargo:: 指令,这样编译生成的 xx-sys 库文件 rlib 中就会打包 cc 生成的 C/C++ 静态库。
- 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
// 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, ...);
}
如果链接的是系统的库文件,则第 1 部分内容是可选的,在第 2 部分的 extern block 中使用 #[link(name="mylib]
来指定链接到系统库,还可以在项目的 .cargo/config.toml 或 build.rs 或命令行中指定
Rust 程序要链接的三方库名称。
如果库文件不在系统标准搜索路径中,可以使用 build script 来指定搜索路径:
fn main() {
println!(r"cargo:rustc-link-search=native=/home/alizj/libgit2-0.25.1/build");
}
xx-sys crate #
一般使用专门的 crate 来封装 C/C++ 源码和库文件,该 crate 的命名惯例是 xx-sys
, 如 libduckdb-sys
。
该 xx-sys crate 一般包含如下内容:
- C/C++ 源代码文件;
- C/++ 二进制库文件(静态库或动态库);
- C/C++ 接口的 Rust 封装声明(入口位于 src/lib.rs),如使用
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++ 外部库一般建议源码集成,而非二进制库集成。
- 但对于默认安装到系统标准未指的 OS 系统库,可以在编译 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();
// Note that there are a number of downsides to this approach, the comments
// below detail how to improve the portability of these commands.
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-dependencies]
中声明的依赖,而不使用 [dependencies] 和 [dev-dependencies]
。
buid.rs 是一个带 main() 函数的可执行程序,可以执行任何业务逻辑,但它比较特殊的地方在于:
- 由 cargo build 等触发编译和自动执行,只能从 cargo 传递的环境变量来获取输入参数;
- 可以在 stdout 输出 cargo metadata 指令(以 cargo:: 开头),这些指令用于向 cargo 传递编译、链接参数等信息。
- 一般用于链接 build.rs 生成的外部库。
- crate 的 Rust 代码不需要在通过
#[link]
来手动指定链接参数。
build script 结果是有缓存的,当再次编译时,只有当任一 src 目录下的源文件(或者 Cargo.toml 中配置的 exclude 和 include 列表文件)或依赖包发生变化时,build script 才会被重新执行。
- 可以使用
cargo::rerun-if-changed
指令来自定义 change detection 算法,只有当指定的文件发生变化时才会重新运行 build script。
build script 输入:只能通过环境变量进行配置,一般由 cargo 调用时自动设置。脚本程序的退出码为 0 时表示正常退出。
- build script 也可以返回 Result,然后由调用方来决定是否失败退出。
- 环境变量列表:https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts
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 指令:https://doc.rust-lang.org/cargo/reference/build-scripts.html
重新运行条件:
- cargo::rerun-if-changed=PATH — Tells Cargo when to re-run the script.
- cargo::rerun-if-env-changed=VAR — Tells Cargo when to re-run the script.
链接器参数:
cargo::rustc-link-arg=FLAG
— Passes custom flags to a linker for benchmarks, binaries, cdylib crates, examples, and tests.
- 对应 rustc 的
-C link-arg=FLAG option
cargo::rustc-link-arg-bin=BIN=FLAG
— Passes custom flags to a linker for the binary BIN.
- 对应 rustc 的
-C link-arg=FLAG option
,但只对名为 BIN 的 bin 有效;
cargo::rustc-link-arg-bins=FLAG
— Passes custom flags to a linker for binaries.
- 对应 rustc 的
-C link-arg=FLAG option
,对所有 bin crate type 有效;
-
cargo::rustc-link-arg-tests=FLAG
— Passes custom flags to a linker for tests. -
cargo::rustc-link-arg-examples=FLAG
— Passes custom flags to a linker for examples. -
cargo::rustc-link-arg-benches=FLAG
— Passes custom flags to a linker for benchmarks. -
cargo::rustc-link-lib=LIB
— Adds a library to link. # 为 Rust 程序指定要链接的外部库文件名称和类型
- 对应 rustc 的
-l flag
, 一般用于使用 FFI 链接外部库的场景。 - LIB 的格式和 rustc 的 -l 参数值格式 一致:
[KIND[:MODIFIERS]=]NAME[:RENAME]
cargo::rustc-link-search=[KIND=]PATH
— Adds to the library search path. # 外部库文件搜索路径
- 对应 rustc 的
-L flag
编译器参数:
cargo::rustc-flags=FLAGS
— Passes certain flags to the compiler.
- 只能指定 rustc 的 -l 和 -L 参数,使用空格分割。等效于 rustc-link-lib 和 rustc-link-search;
cargo::rustc-cfg=KEY[="VALUE"]
— Enables compile-time cfg settings.
- 指定 rustc 的
--cfg flag
,可用于条件编译;
cargo::rustc-check-cfg=CHECK_CFG
– Register custom cfgs as expected for compile-time checking of configs.
- 指定 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 — Sets an environment variable.
- cargo::rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates.
错误提示参数:
- cargo::error=MESSAGE — Displays an error on the terminal.
- 在 build script 执行结束后打印一条 error 信息,并失败退出。
- cargo::warning=MESSAGE — Displays a warning on the terminal.
元数据参数:
- cargo::metadata=KEY=VALUE — Metadata, used by links scripts.
cc crate #
上面 build script 问题:
- 写死了 gcc 编译命令,会有跨平台的问题;
- 不支持交叉编译;
使用 cc crate
来重写 build.rs,可以更好的解决上面的问题:
// Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
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 环境变量对应的目录下保存文件。
- 环境变量列表:https://docs.rs/cc/latest/cc/#external-configuration-via-environment-variables
- 自动向 stdout 发送 cargo:: 开头的 metadata 指令,用于指示 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
- When target is MSVC, the ATL-MFC libs are added via rustc-link-search=native=
- When C++ is enabled, the C++ stdlib is added via rustc-link-lib
- If emit_rerun_if_env_changed is not false,
rerun-if-env-changed=env
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")], 因为 buidl.rs 中的 cc 会自动生成链接 libfoo.a 的 cargo:: meta 指令。
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 中使用 SDK 的方式来使用 bindgen,这是以为 C、C++ 头文件有很多平台相关特性,在 build.rs 中使用 bindgen 时,会自动感知 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.rs
rustc --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 参数。
zj@a:~/docs$ 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。
zj@a:~/docs$ 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
zj@a:~/docs$