跳过正文

Rust 编译和链接三方 C/C++ 源码&库

·
目录

Rust 程序可以链接外部 C/C++ 库,使用其中的变量、类型和函数等内容。

本文介绍 Rust 程序编译和链接 C/C++ 程序源码或库的方式。

对于外部的 C/C++ 程序,Rust 有三种集成方式:

  1. 源文件集成:将 C/C++ 源码集成到 Rust 项目中,在编译 Rust 代码前先自动编译 C/C++ 源码,然后让 Rust 程序自动链接生成的 C/C++ 库;
  2. 库文件集成:在编译 Rust 程序时链接外部的 C/C++ 库文件,库文件可以位于 Rust 项目目录中,或者所在操作系统文件目录中;
  3. 混合方式:混合 1 和 2;

Rust 程序链接外部 C/C++ 库的4 种方式:

  1. 通过 rustc -L/-l 参数;
  2. 通过 #[link] 属性宏;
  3. 通过 build.rs 在 stdout 打印编译和链接指令(cargo:: 开头的字符串);
  4. 通过 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

参考:

  1. https://doc.rust-lang.org/rustc/command-line-arguments.html?highlight=bundle#-l-link-the-generated-crate-to-a-native-library

#[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 一般包含如下内容:

  1. C/C++ 源代码文件;
  2. C/++ 二进制库文件(静态库或动态库);
  3. C/C++ 类型、函数、全局变量和常量的 Rust 封装声明,如使用 extern "ABI" {} 来声明 C/C++ 库中变量或函数;

xx-sys crate 一般还使用 build script(build.rs) 机制来实现如下功能:

  1. 从源码编译生成外部 C/C++ 静态库文件:一般使用 cc crate 来生成静态的外部库文件;
  2. 调用 bingen 命令或 SDK,从 C/C++ 头文件自动生成 Rust 封装声明(如 bindgen.rs,然后被导入到 src/lib.rs 中);
  3. 打印 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() 函数的可执行程序,可以执行任何业务逻辑:

  1. 由 cargo build 等触发编译和自动执行;
  2. 只能通过 cargo 传递的环境变量来获取输入参数;
  3. 在 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 输出:

  1. 生成的文件或中间数据:统一保存到 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");
  1. cargo 交互:需要打印到 stdout,cargo 逐行检查 script 输出,将 cargo:: 开头的行解释为 cargo 指令,cargo 再转换为调用的 rustc 的编译链接参数。
  • 示例:println!("cargo:rustc-link-lib=framework=Security");
  • 注意 cargo:: 指令的顺序影响传递给 rustc 的参数顺序。
  1. build script 的输出默认是隐藏的,可以为 cargo 指定 -vv 参数来打印输出。
  • 脚本的 stdout 和 stderr 保存位置:target/debug/build//output

cargo 指令
#

常见的 cargo 指令:https://doc.rust-lang.org/cargo/reference/build-scripts.html

重新运行条件:

  1. cargo::rerun-if-changed=PATH
  2. cargo::rerun-if-env-changed=VAR

链接器参数:

  1. cargo::rustc-link-arg=FLAG:对应 rustc 的 -C link-arg=FLAG
  2. cargo::rustc-link-arg-bin=BIN=FLAG:对应 rustc 的 -C link-arg=FLAG,但只对名为 BIN 的 bin 有效;
  3. cargo::rustc-link-arg-bins=FLAG:对应 rustc 的 -C link-arg=FLAG,对所有 bin crate type 有效;
  4. cargo::rustc-link-arg-tests=FLAG: 编译 tests 时的链接器参数;
  5. cargo::rustc-link-arg-examples=FLAG:编译 examples 时的链接器参数;
  6. cargo::rustc-link-arg-benches=FLAG:编译 benchmark 时的链接器参数;
  7. cargo::rustc-link-lib=LIB:为 Rust 程序指定要链接的外部库文件名称和类型,对应 rustc 的 -l flag, 一般用于使用 FFI 链接外部库的场景。
  • LIB 的格式和 rustc 的 -l 参数值格式一致: [KIND[:MODIFIERS]=]NAME[:RENAME]
  1. cargo::rustc-link-search=[KIND=]PATH:外部库文件搜索路径,对应 rustc 的 -L flag

编译器参数:

  1. cargo::rustc-flags=FLAGS:只能指定 rustc 的 -l 和 -L 参数,使用空格分割。等效于 rustc-link-lib 和 rustc-link-search;
  2. cargo::rustc-cfg=KEY[="VALUE"]:指定 rustc 的 --cfg flag,可用于条件编译;
  3. 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\"");
}

环境变量参数:

  1. cargo::rustc-env=VAR=VALUE:设置环境变量;
  2. cargo::rustc-cdylib-link-arg=FLAG:为 cdylib crate 设置链接器参数;

错误提示参数:

  1. cargo::error=MESSAGE:在 build script 执行结束后打印一条 error 信息,并失败退出;
  2. cargo::warning=MESSAGE:打印 warning 消息;

元数据参数:

  1. cargo::metadata=KEY=VALUE:设置链接脚本使用的 Metadata;

cc crate
#

上面 build script 的问题:

  1. 写死了 gcc 编译命令,会有跨平台的问题;
  2. 不支持交叉编译;

使用 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 能很好的协作:

  1. 自动使用系统缺省的编译器;
  2. 考虑 HOST、TARGET 环境变量,给编译器传递合适的编译参数,从而 支持交叉编译
  3. 自动处理 build.rs 环境变量,如 OPT_LEVEL, DEBUG, HOST, TARGET,自动在 stdout 生成 cargo:: 指令,自动在 OUT_DIR 环境变量对应的目录下保存文件。
  4. 自动向 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。

基本步骤:

  1. 创建一个 xx-sys crate;
  2. 拷贝依赖的 C/C++ 源文件(头文件、C 文件等)到 crate 目录中,示例:
  1. 在 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
  1. 在 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 来决定是否编译,包含如下类型:

  1. A configuration option. It is true if the option is set and false if it is unset.

  2. 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.

  3. 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.

  4. not() with a configuration predicate. It is true if its predicate is false and false if its predicate is true.

其中 Configuration options 可以是 namekey="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:

  1. Cargo.toml 中定义的 features 列表(默认都不开启):
  • default feature:表示未通过 rustc --cfg 'feature="xx"'cargo build --features "xx yy" 启用 feature 时,默认启用的 feature 列表;
  • feature:也被用于开启 option 的 dependencies;
  1. 各种预定义的 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
$

参考
#

相关文章

程序的编译和链接:gcc、clang、glibc、musl 和 rustc
·
系统总结了使用 gcc、clang、rustc 编译器进行程序的编译和链接过程,以及使用 musl 进行静态链接的方案。
async-trait
·
async-trait 解决了 Rust Trait 中 async fn 函数的一系列问题
Rust 工具链、项目布局、程序的编译链接和 Cargo 配置
·
介绍使用 rustup 管理 Rust 工具链,Rust 程序的目录布局,编译链接和 Cargo 配置。
sqlx
·
sqlx 是异步的 SQL mapper。