跳过正文

目录

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

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

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

  1. 源文件集成:将 C/C++ 源码集成到 Rust 项目中,在编译 Rust 代码前先编译 C/C++ 源码生成库文件,然后让 Rust 程序链接;
  2. 库文件集成:在编译 Rust 程序时链接:
  3. 库文件位于 Rust 程序目录中,即 Rust 源码自包含;
  4. 库文件位于系统库文件目录中;
  5. 混合方式:1 和 2 混合的方式。

Rust crate 链接外部库有 4 种方式:

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

参考:

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

  1. C/C++ 源代码文件;
  2. C/++ 二进制库文件(静态库或动态库);
  3. C/C++ 接口的 Rust 封装声明(入口位于 src/lib.rs),如使用 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++ 外部库一般建议源码集成,而非二进制库集成。

  • 但对于默认安装到系统标准未指的 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() 函数的可执行程序,可以执行任何业务逻辑,但它比较特殊的地方在于:

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

  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 指令:https://doc.rust-lang.org/cargo/reference/build-scripts.html

重新运行条件:

  1. cargo::rerun-if-changed=PATH — Tells Cargo when to re-run the script.
  2. cargo::rerun-if-env-changed=VAR — Tells Cargo when to re-run the script.

链接器参数:

  1. 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
  1. 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 有效;
  1. cargo::rustc-link-arg-bins=FLAG — Passes custom flags to a linker for binaries.
  • 对应 rustc 的 -C link-arg=FLAG option,对所有 bin crate type 有效;
  1. cargo::rustc-link-arg-tests=FLAG — Passes custom flags to a linker for tests.

  2. cargo::rustc-link-arg-examples=FLAG — Passes custom flags to a linker for examples.

  3. cargo::rustc-link-arg-benches=FLAG — Passes custom flags to a linker for benchmarks.

  4. cargo::rustc-link-lib=LIB — Adds a library to link. # 为 Rust 程序指定要链接的外部库文件名称和类型

  • 对应 rustc 的 -l flag, 一般用于使用 FFI 链接外部库的场景。
  • LIB 的格式和 rustc 的 -l 参数值格式 一致: [KIND[:MODIFIERS]=]NAME[:RENAME]
  1. cargo::rustc-link-search=[KIND=]PATH — Adds to the library search path. # 外部库文件搜索路径
  • 对应 rustc 的 -L flag

编译器参数:

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

环境变量参数:

  1. cargo::rustc-env=VAR=VALUE — Sets an environment variable.
  2. cargo::rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates.

错误提示参数:

  1. cargo::error=MESSAGE — Displays an error on the terminal.
  • 在 build script 执行结束后打印一条 error 信息,并失败退出。
  1. cargo::warning=MESSAGE — Displays a warning on the terminal.

元数据参数:

  1. cargo::metadata=KEY=VALUE — Metadata, used by links scripts.

cc crate
#

上面 build script 问题:

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

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

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

基本步骤:

  1. 创建一个 xx-sys crate;
  2. 拷贝依赖的 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
  1. 在 xx-sys crate 的 build.rs 中:
  2. 调用 cc crate 来编译包含的 C/C++ 源文件,生成静态库和对应的 cargo:: 编译链接指令;
  3. 创建一个 wrapper.h 头文件,将所有的头文件都 include 进来,该 wrapper.h 头文件作为 bindgen 输入头文件; - 示例:https://github.com/rusqlite/rusqlite/blob/master/libsqlite3-sys/wrapper.h
  4. 调用 bindgen SDK,为头文件生成对应的 Rust extern block 封装文件,一般为 buildgen.rs; - 示例:https://github.com/rusqlite/rusqlite/blob/master/libsqlite3-sys/build.rs#L544
  5. 在 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 可以是 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:

  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 参数。

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$

参考
#

相关文章

程序的编译和链接:gcc、clang、glibc、musl 和 rustc
·
Rust Cargo
系统总结了使用 gcc、clang、rustc 编译器进行程序的编译和链接过程,以及使用 musl 进行静态链接的方案。
gdb
·
Gnu Tool Gdb
gdb 个人速查手册。
gas X86_64 汇编 - 个人参考手册
·
Gnu Asm Gcc Manual
GCC 编译器 - 个人参考手册
·
Gnu Gcc Manual
gcc 编译器个人参考手册。