Rust FFI

翻译自安全 Rust 指南

外部函数接口(FFI)

Rust 与其他语言进行接口交互的方法依赖于与 C 语言的强大兼容性. 然而, 这种边界在其本质上是不安全的(参见 Rust 书籍: 不安全 Rust).

被标记为 extern 的函数在编译期间与 C 代码兼容. 它们可以从 C 代码调用, 并携带任何参数值.
具体的语法是 extern "<ABI>", 其中 ABI 是一个调用约定, 取决于目标平台. 默认的调用约定是 C, 对应于目标平台上的标准 C 调用约定.

1
2
3
4
5
// 导出一个与 C 兼容的函数
#[no_mangle]
unsafe extern "C" fn mylib_f(param: u32) -> i32 {
if param == 0xCAFEBABE { 0 } else { -1 }
}

要使函数 mylib_f 可以用相同的名称访问, 该函数还必须用 #[no_mangle] 属性进行注释.

相反地, 如果在 extern 块中声明了 C 函数, 则可以从 Rust 中调用它们:

1
2
3
4
5
6
7
8
9
10
use std::os::raw::c_int;
// 从 libc 中导入一个外部函数
extern "C" {
fn abs(args: c_int) -> c_int;
}

fn main() {
let x = -1;
println!("{} {}\n", x, unsafe { abs(x) });
}

注意

通过 extern 块在 Rust 中导入的任何外部函数都是自动不安全的. 这就是为什么对外部函数的任何调用都必须从一个 unsafe 上下文中进行的原因.

extern 块还可以包含以 static 关键字为前缀的外部全局变量声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//! 直接访问环境变量的一种方式(在 Unix 上).
//! 不应该使用!不是线程安全的, 请查看 `std::env`!

extern {
// Libc 全局变量
#[link_name = "environ"]
static libc_environ: *const *const std::os::raw::c_char;
}

fn main() {
let mut next = unsafe { libc_environ };
while !next.is_null() && !unsafe { *next }.is_null() {
let env = unsafe { std::ffi::CStr::from_ptr(*next) }
.to_str()
.unwrap_or("<invalid>");
println!("{}", env);
next = unsafe { next.offset(1) };
}
}

类型

类型是 Rust 确保内存安全的方式. 当与其他可能不提供相同保证的语言进行接口时, 绑定中类型的选择对于维护内存安全至关重要.

数据布局

Rust 在内存中布局数据的方式没有短期或长期的保证. 使数据与外部语言兼容的唯一方法是通过使用 repr 属性明确使用 C 兼容的数据布局(参见 Rust 参考: 类型布局). 例如, 以下 Rust 类型:

1
2
3
4
5
6
7
8
9
10
11
12
#[repr(C)]
struct Data {
a: u32,
b: u16,
c: u64,
}
#[repr(C, packed)]
struct PackedData {
a: u32,
b: u16,
c: u64,
}

与以下 C 类型兼容:

1
2
3
4
5
6
7
8
9
10
11
struct Data {
uint32_t a;
uint16_t b;
uint64_t c;
};
__attribute__((packed))
struct PackedData {
uint32_t a;
uint16_t b;
uint64_t c;
}

规则 在 FFI 中仅使用 C 兼容的类型

在安全的 Rust 开发中, 导入或导出函数的参数或返回类型以及导入或导出全局变量的类型必须仅使用 C 兼容的类型.

唯一的例外是在外部一侧被认为是不透明的类型.

以下类型被认为是 C 兼容的:

  • 整数或浮点原始类型,
  • 使用 repr(C) 注释的 struct,
  • 具有至少一个变体且只有无字段变体的 repr(C)repr(Int) 注释的 enum(其中 Int 是整数原始类型),
  • 指针.

以下类型不是 C 兼容的:

  • 动态大小类型,
  • Trait 对象,
  • 带有字段的枚举,
  • 元组(但 repr(C) 元组结构是可以的).

某些类型是兼容的, 但有一些注意事项:

  • 确实是零大小的零大小类型(在 C 中未指定, 并且与 C++ 规范相矛盾),
  • 带有字段的 repr(C)repr(C, Int)repr(Int) 注释的枚举(参见 RFC 2195).

类型一致性

规则 在 FFI 边界使用一致的类型

类型在 FFI 边界的每一侧必须保持一致.

尽管某些细节可能在一侧相对于另一侧被隐藏(通常是为了使类型不透明), 但两侧的类型必须具有相同的大小和相同的对齐要求.

特别是对于带有字段的枚举, C(或 C++)中的相应类型并不明显, 详见 RFC 2195.

自动生成绑定的自动化工具, 如 rust-bindgencbindgen, 可以帮助确保在 C 和 Rust 之间的类型一致.

建议 使用自动生成绑定的工具

在安全的 Rust 开发中, 应尽可能使用自动生成工具来生成绑定, 并持续地维护它们.

警告

对于将 C/C++ 绑定到 Rust, [rust-bindgen] 能够自动生成低级绑定. 强烈建议使用高级别安全绑定(参见建议 FFI-SAFEWRAPPING). 此外, rust-bindgen 的某些选项可能会导致危险的转换, 特别是 rustified_enum.

平台相关类型

当与外部语言(如 C 或 C++)进行接口时, 通常需要使用平台相关类型, 如 C 的 intlong 等.

除了 std::ffi(或 core::ffi)中的 c_void 代表 void 外, 标准库还在 std:os::raw(或 core::os::raw)中提供了可移植的类型别名:

  • c_char 代表 char(可以是 i8u8),
  • c_schar 代表 signed char(始终为 i8),
  • c_uchar 代表 unsigned char(始终为 u8),
  • c_short 代表 short,
  • c_ushort 代表 unsigned short,
  • c_int 代表 int,
  • c_uint 代表 unsigned int,
  • c_long 代表 long,
  • c_ulong 代表 unsigned long,
  • c_longlong 代表 long long,
  • c_ulonglong 代表 unsigned long long,
  • c_float 代表 float(始终为 f32),
  • c_double 代表 double(始终为 f64).

[libc] crate 提供了更多与 C 兼容的类型, 几乎完全涵盖了 C 标准库.

规则 绑定到平台相关类型时使用可移植别名 c_*

在安全的 Rust 开发中, 当与使用平台相关类型(如 C 的 intlong)的外部代码进行接口时, Rust 代码必须使用可移植类型别名, 如标准库或 [libc] crate 提供的, 而不是平台特定类型, 除非对每个平台自动生成了绑定(见下面的注意).

注意

自动绑定生成工具(如 [cbindgen]、[rust-bindgen])能够确保特定平台上的类型一致性. 在构建过程中, 应针对每个目标使用这些工具, 以确保生成对特定目标平台有效的代码.

非健壮类型: 引用、函数指针、枚举

特定类型的 陷阱表示 是符合该类型表示约束(如大小和对齐)但不代表该类型有效值的表示(位模式), 并导致未定义行为.

简而言之, 如果将 Rust 变量设置为这样的无效值, 可能会发生从简单的程序崩溃到任意代码执行的任何情况. 在编写安全的 Rust 代码时, 这是不可能的(除非 Rust 编译器中存在错误). 但是, 在编写不安全的 Rust 代码, 特别是在 FFI 中, 情况就不一样了.

在下面, 非健壮类型 是具有这种陷阱表示的类型(至少有一个). 许多 Rust 类型都是非健壮的, 即使在 C 兼容类型中也是如此:

  • bool(1 字节, 256 种表示, 只有 2 种有效的),
  • 引用,
  • 函数指针,
  • 枚举,
  • 浮点数(即使几乎每种语言对于什么是有效的浮点数都有相同的理解),
  • 包含非健壮类型字段的复合类型.

另一方面, 整数类型(u*/i*)、不包含非健壮字段的紧凑复合类型, 它们的实例都是健壮类型.

非健壮类型在两种语言之间进行接口时是一个困难. 它涉及决定两种语言中哪一种负责断言跨界值的有效性以及如何做到这一点.

建议 在 Rust 中检查外部值

在安全的 Rust 开发中, 应尽可能在 Rust 中对外部值的有效性进行检查.

这些通用规则应根据特定的外部语言或相关风险进行调整. 就语言而言, C 特别不适合提供关于有效性的保证. 然而, Rust 并不是唯一提供强大保证的语言. 例如, 某些 C++ 子集(不包括重新解释)允许开发人员进行大量类型检查. 因为 Rust 本身将安全和不安全的部分分开, 所以推荐始终在可能的情况下使用 Rust 进行检查. 至于风险, 最危险的类型是引用、函数引用和枚举, 下面将对其进行讨论.

警告

Rust 的 bool 已经被定义为等同于 C99 的 _Bool(在 <stdbool.h> 中别名为 bool)和 C++ 的 bool. 但是, 在 _Bool/bool 中加载除 0 和 1 以外的值是 在两侧 都是未定义行为的.
安全的 Rust 确保了这一点. 符合标准的 C 和 C++ 编译器确保除 0 和 1 以外的值不会 存储_Bool/bool 值中, 但不能保证不存在 不正确的重新解释(例如, 联合类型、指针转换). 为了检测这种错误的重新解释, 可以使用诸如 LLVM 的 -fsanitize=bool 等工具.

引用和指针

尽管 Rust 编译器允许使用引用, 但在 FFI 中使用 Rust 引用可能会破坏 Rust 的内存安全性. 因为它们的“不安全性”更加显式, 所以在绑定到其他语言时, 指针优先于 Rust 引用.

一方面, 引用类型非常不健壮: 它们只允许指向有效内存对象的指针. 任何偏差都会导致未定义的行为.

在与 C 进行绑定时, 问题尤其严重, 因为 C 没有引用(指向有效指针的意义), 而且编译器不提供任何安全保证.

在与 C++ 进行绑定时, 实际上可能将 Rust 引用绑定到 C++ 引用, 尽管具有引用的 extern "C" 函数的实际 ABI 是“实现定义的”. 此外, 应该对 C++ 代码进行检查, 以防止指针/引用混淆.

Rust 引用可能与其他与 C 兼容的语言合理地结合使用, 包括允许非空类型检查的 C 变种, 例如 Microsoft SAL 注释的代码.

另一方面, Rust 的 指针类型 也可能导致未定义的行为, 但更容易验证, 主要针对 std/core::ptr::null()(C 的 (void*)0)以及某些情况下针对已知有效内存范围(特别是在嵌入式系统或内核级编程中). 在 FFI 中使用 Rust 指针的另一个优势是, 指向值的任何加载都明确标记在 unsafe 块或函数内.

建议 不要使用引用类型, 而是使用指针类型

在安全的 Rust 开发中, Rust 代码不应使用引用类型, 而应使用指针类型.

例外情况包括:

  • 在外部语言中不透明的 Rust 引用, 并且仅在 Rust 一侧进行操作,
  • Option 封装的引用(见下面的注意),
  • 绑定到外部安全引用的引用, 例如某些增强的 C 变体或在 extern "C" 引用被编码为指针的环境中编译的 C++.

规则 不要使用未经检查的外部引用

在安全的 Rust 开发中, 通过 FFI 传递到 Rust 的每个外部引用必须在 外部一侧 进行检查, 可以是自动的(例如, 通过编译器)也可以是手动的.

例外情况包括仅在 Rust 一侧创建和操作的具有不透明封装的 Rust 引用和 Option 封装的引用(见下面的注意).

规则 检查外部指针

在安全的 Rust 开发中, 任何在 Rust 中解引用外部指针的代码都必须在使用之前检查其有效性.
特别是, 在使用之前必须检查指针是否为非空.

在可能的情况下, 建议采用更强的方法. 这包括针对已知有效内存范围或对指针进行标记(或签名)的检查(如果指向的值仅从 Rust 中操作, 则特别适用).

下面的代码是一个导出的 Rust 函数中使用外部指针的简单示例:

1
2
3
4
5
6
7
8
9
/// 就地相加
#[no_mangle]
pub unsafe extern fn add_in_place(a: *mut u32, b: u32) {
// 检查 `a` 是否为空
// 如果非空, 则获取可变引用
if let Some(a) = a.as_mut() {
*a += b
}
}

请注意, as_ref 方法和 as_mut 方法(对于可变指针)允许以非常 Rust 的方式轻松访问引用, 同时确保进行了空指针检查. 另一方面, 在 C 代码中, 可以如下使用:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdint.h>
#include <inttypes.h>

//! 就地相加
void add_in_place(uint32_t *a, uint32_t b);

int main() {
uint32_t x = 25;
add_in_place(&x, 17);
printf("%" PRIu32 " == 42", x);
return 0;
}

注意

在 FFI 中, Option<&T>Option<&mut T> 代替具有显式空值检查的指针是允许的. 由于 Rust 保证的“可空指针优化”, C 中的可空指针在 Rust 中是可以接受的. 在 Rust 中, C 的 NULL 被理解为 None, 而非空指针则封装在 Some 中. 尽管非常符合人体工程学, 但此功能不允许更强的验证, 例如内存范围检查.

函数指针

跨 FFI 边界的函数指针最终可能导致任意代码执行, 并且代表着真正的安全风险.

规则 将 FFI 中的函数指针类型标记为 externunsafe

在安全的 Rust 开发中, FFI 边界处的任何函数指针类型必须标记为 extern(可能具有特定的 ABI)和 unsafe.

Rust 中的函数指针更类似于引用而不是普通指针. 特别地, 函数指针的有效性不能直接在 Rust 一侧检查. 但是, Rust 提供了两种替代可能性:

  • 使用 Option 封装的函数指针, 并针对 null 进行检查:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #[no_mangle]
    pub unsafe extern "C" fn repeat(start: u32, n: u32, f: Option<unsafe extern "C" fn(u32) -> u32>) -> u32 {
    if let Some(f) = f {
    let mut value = start;
    for _ in 0..n {
    value = f(value);
    }
    value
    } else {
    start
    }
    }

在 C 一侧:

1
uint32_t repeat(uint32_t start, uint32_t n, uint32_t (*f)(uint32_t));
  • 使用原始指针, 并对其进行 unsafe 转换为函数指针类型, 以更大的成本换取更强大的检查.

规则 检查外部函数指针

在安全的 Rust 开发中, 任何外部函数指针都必须在 FFI 边界处进行检查.

与 C 或甚至 C++ 进行绑定时, 很难保证函数指针的有效性. C++ 仿函数不兼容 C.

枚举

通常, 有效 enum 值的可能位模式与相同大小的可能位模式的数量相比非常小. 错误处理外部代码提供的 enum 值可能导致类型混淆, 并对软件安全性产生严重后果. 不幸的是, 在 FFI 边界处检查 enum 值在两侧都不简单.

在 Rust 一侧, 它涉及到在 extern 块声明中实际使用整数类型, 即 健壮 类型, 然后执行经过检查的转换为枚举类型.

在外部一侧, 只有在其他语言允许进行比纯 C 更严格的检查时才可能. 例如, 在 C++ 中, enum class 就是可以接受的. 但是请注意, 就像引用一样, extern "C" ABI 的实际 enum class 应由每个环境进行验证.

建议 不要在 FFI 边界处使用传入的 Rust enum

在安全的 Rust 开发中, 当与外部语言进行交互时, Rust 代码不应接受任何 Rust enum 类型的传入值.

例外情况包括 Rust enum 类型是:

  • 在外部语言中不透明且仅从 Rust 一侧操作的,
  • 绑定到外部安全枚举的 Rust 枚举类型, 例如 C++ 中的 enum class 类型.

关于无字段枚举, 诸如 [num_derive] 或 [num_enum] 这样的 crate 允许开发人员轻松地从整数转换为枚举, 可以用于将整数(由 C enum 提供)安全地转换为 Rust 枚举.

不透明类型

使类型不透明是增加软件开发中模块化的一种良好方法. 在进行多语言开发时, 这是非常常见的.

建议 对外部不透明类型使用专用的 Rust 类型

在安全的 Rust 开发中, 绑定外部不透明类型时, 应使用指向专用不透明类型的指针, 而不是 c_void 指针.

目前, 制作外部不透明类型的推荐方法如下:

1
2
3
4
5
#[repr(C)]
pub struct Foo {_private: [u8; 0]}
extern "C" {
fn foo(arg: *mut Foo);
}

尚未实现的 RFC 1861 提议通过允许在 extern 块中声明不透明类型来简化编码.

建议 使用不完整的 C/C++

struct 指针使类型不透明

在安全的 Rust 开发中, 当与 C 或 C++ 进行交互时, Rust 类型在 C/C++ 中应被视为不透明应该被翻译为不完整的 struct 类型(即, 没有定义声明)并且应该提供专用的构造函数和析构函数.

不透明 Rust 类型的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# use std::panic::catch_unwind;
#
struct Opaque {
// (...) 隐藏的细节
}

#[no_mangle]
pub unsafe extern "C" fn new_opaque() -> *mut Opaque {
catch_unwind(|| // 捕获 panic, 见下文
Box::into_raw(Box::new(Opaque {
// (...) 实际构造
}))
).unwrap_or(std::ptr::null_mut())
}

#[no_mangle]
pub unsafe extern "C" fn destroy_opaque(o: *mut Opaque) {
catch_unwind(||
if !o.is_null() {
drop(Box::from_raw(o))
}
); // 仅在 Opaque 或其子字段是 Drop 时需要
}

内存和资源管理

编程语言以各种方式处理内存. 因此, 在 Rust 和另一种语言之间传输数据时, 了解哪种语言负责为这些数据回收内存空间非常重要. 对于其他类型的资源, 例如套接字或文件, 情况也是如此.

Rust 跟踪变量所有权和生命周期, 以确定在编译时是否以及何时应该释放内存. 借助 Drop trait, 可以利用这个系统来回收其他类型的资源, 例如文件或网络访问. 将某些数据从 Rust 移动到另一种语言意味着也放弃了与之关联的可能的回收.

规则 在 FFI 边界不要使用实现 Drop 的类型

在安全的 Rust 开发中, Rust 代码不得为直接传输到外部代码的任何类型实现 Drop(即不通过指针或引用).

实际上, 建议只使用 Copy 类型. 请注意, 即使 T 不是 Copy 类型, *const T 也是 Copy 类型.

然而, 如果不释放内存和资源是不好的, 那么使用已释放的内存或多次释放一些资源就更糟糕了, 从安全性的角度来看. 为了正确释放资源仅一次, 必须知道哪种语言负责分配和释放内存.

规则 在 FFI 中确保明确的数据所有权

在安全的 Rust 开发中, 当某种类型的数据在 FFI 边界上无需复制地传递时, 必须确保:

  • 一个语言负责数据的分配和释放.
  • 另一种语言不能直接分配或释放数据, 而是使用所选语言提供的专用外部函数.

拥有权还不够. 仍然需要确保正确的生命周期, 主要是在回收后不会发生使用. 这是一个更具挑战性的任务. 当另一种语言负责内存时, 最好的方法是在外部类型周围提供一个安全的包装器:

建议 在内存释放包装器中封装外部数据

在安全的 Rust 开发中, 应该将在外部语言中分配和释放的任何非敏感外部数据封装在 Drop 类型中, 以便通过自动调用外部语言的释放例程自动在 Rust 中释放它们.

警告

因为恐慌可能导致未运行 Drop::drop 方法, 所以该解决方案对于敏感的资源释放(例如擦除敏感数据)并不足够, 除非可以保证代码永远不会发生恐慌.

对于擦除敏感数据的情况, 可以通过专用的恐慌处理程序来解决该问题.

当外部语言是利用 Rust 分配的资源时, 要提供任何保证就要困难得多.

例如, 在 C 中, 没有一种简单的方法可以检查是否已检查了适当的析构函数. 一种可能的方法是利用回调来确保回收是安全的.

以下 Rust 代码是一个线程不安全的示例, 展示了一个与 C 兼容的 API, 提供回调以确保安全的资源回收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
use std::ops::Drop;

pub struct XtraResource {/*fields */}

impl XtraResource {
pub fn new() -> Self {
XtraResource { /* ... */}
}
pub fn dosthg(&mut self) {
/*...*/
}
}

impl Drop for XtraResource {
fn drop(&mut self) {
println!("xtra drop");
}
}

pub mod c_api {
use super::XtraResource;
use std::panic::catch_unwind;

const INVALID_TAG: u32 = 0;
const VALID_TAG: u32 = 0xDEAD_BEEF;
const ERR_TAG: u32 = 0xDEAF_CAFE;

static mut COUNTER: u32 = 0;

pub struct CXtraResource {
tag: u32, // to detect accidental reuse
id: u32,
inner: XtraResource,
}

#[no_mangle]
pub unsafe extern "C" fn xtra_with(cb: extern "C" fn(*mut CXtraResource) -> ()) {
let inner = if let Ok(res) = catch_unwind(XtraResource::new) {
res
} else {
# println!("cannot allocate resource");
return;
};
let id = COUNTER;
let tag = VALID_TAG;

COUNTER = COUNTER.wrapping_add(1);
// Use heap memory and do not provide pointer to stack to C code!
let mut boxed = Box::new(CXtraResource { tag, id, inner });

# println!("running the callback on {:p}", boxed.as_ref());
cb(boxed.as_mut() as *mut CXtraResource);

if boxed.id == id && (boxed.tag == VALID_TAG || boxed.tag == ERR_TAG) {
# println!("freeing {:p}", boxed.as_ref());
boxed.tag = INVALID_TAG; // prevent accidental reuse
// implicit boxed drop
} else {
# println!("forgetting {:p}", boxed.as_ref());
// (...) error handling (should be fatal)
boxed.tag = INVALID_TAG; // prevent reuse
std::mem::forget(boxed); // boxed is corrupted it should not be
}
}

#[no_mangle]
pub unsafe extern "C" fn xtra_dosthg(cxtra: *mut CXtraResource) {
let do_it = || {
if let Some(cxtra) = cxtra.as_mut() {
if cxtra.tag == VALID_TAG {
# println!("doing something with {:p}", cxtra);
cxtra.inner.dosthg();
return;
}
}
println!("doing nothing with {:p}", cxtra);
};
if catch_unwind(do_it).is_err() {
if let Some(cxtra) = cxtra.as_mut() {
# println!("panicking with {:p}", cxtra);
cxtra.tag = ERR_TAG;
}
};
}
}

兼容的 C 调用:

1
2
3
4
5
6
7
8
9
10
11
12
struct XtraResource;
void xtra_with(void (*cb)(XtraResource* xtra));
void xtra_sthg(XtraResource* xtra);

void cb(XtraResource* xtra) {
// ()...) do anything with the proposed C API for XtraResource
xtra_sthg(xtra);
}

int main() {
xtra_with(cb);
}

处理 FFI 绑定的 Rust 代码中的 panic 对于保持稳定性并防止未定义行为至关重要. 当 Rust 代码从另一种语言(如 C)调用时, 任何 panic 必须得到适当处理, 以避免将 unwinding 传播到外部代码中. Rust 中的 panic! 宏是一个强大的错误信号工具, 但在 FFI 场景中必须控制其使用.

在提供的 Rust 代码片段中, may_panic 函数根据某些条件随机发生 panic. 为了确保可以从外部代码安全调用此函数, 它被包装在一个 catch_unwind 块中. 该块尝试捕获 may_panic 中发生的任何 panic, 防止它们传播到外部代码中. 相反, 如果发生 panic, 则返回错误代码(-1).

对于 #![no_std] 程序, 其中标准库不可用, 定义自定义 panic 处理程序是必需的. 此 panic 处理程序应以确保程序安全性和安全性的方式处理 panic. 或者, 可以使用类似 panic-never 的库来静态地确保代码中不会发生 panic.

在 Rust 中绑定外部库时, 建议提供两个部分: 一个密切反映原始 C API 的低级绑定和一个更高级的安全包装模块. 低级绑定应使用 extern 块忠实地转换 C API, 而安全包装模块应确保 Rust 级别的内存安全性和安全性不变量. 这种分离有助于在 Rust 和外部代码之间保持清晰度和安全性.

使用诸如 rust-bindgen 之类的工具可以自动生成从 C 头文件到低级绑定的代码, 简化与外部库交互的过程. 遵循这些最佳实践, 开发人员可以确保其 FFI 绑定的 Rust 代码保持强大、稳定和安全.

在另一种语言中绑定 Rust 库

推荐 仅公开专用的 C 兼容 API

在安全的 Rust 开发中, 将 Rust 库暴露给另一种语言应该只通过专用的 C 兼容 API进行.

可以使用 cbindgen 工具自动生成 Rust 库的 C 或 C++ 绑定, 以便与 Rust 的 C 兼容 API 交互.

一个导出为 C 的 Rust 库的最简示例

src/lib.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/// 不透明的计数器
pub struct Counter(u32);

impl Counter {
/// 创建一个计数器(初始值为 0)
fn new() -> Self {
Self(0)
}
/// 获取计数器当前值
fn get(&self) -> u32 {
self.0
}
/// 如果没有溢出, 则增加计数器的值
fn incr(&mut self) -> bool {
if let Some(n) = self.0.checked_add(1) {
self.0 = n;
true
} else {
false
}
}
}

// C 兼容 API

#[no_mangle]
pub unsafe extern "C" fn counter_create() -> *mut Counter {
Box::into_raw(Box::new(Counter::new()))
}

#[no_mangle]
pub unsafe extern "C" fn counter_incr(counter: *mut Counter) -> std::os::raw::c_int {
if let Some(counter) = counter.as_mut() {
if counter.incr() {
0
} else {
-1
}
} else {
-2
}
}

#[no_mangle]
pub unsafe extern "C" fn counter_get(counter: *const Counter) -> u32 {
if let Some(counter) = counter.as_ref() {
return counter.get();
}
return 0;
}

#[no_mangle]
pub unsafe extern fn counter_destroy(counter: *mut Counter) -> std::os::raw::c_int {
if !counter.is_null() {
let _ = Box::from_raw(counter); // 取出 box 并释放
return 0;
}
return -1;
}

使用 cbindgen([cbindgen] -l c > counter.h), 可以生成一个一致的 C 头文件 counter.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct Counter Counter;

Counter *counter_create(void);

int counter_destroy(Counter *counter);

uint32_t counter_get(const Counter *counter);

int counter_incr(Counter *counter);

counter_main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

#include "counter.h"

int main(int argc, const char** argv) {
if (argc < 2) {
return -1;
}
size_t n = (size_t)strtoull(argv[1], NULL, 10);

Counter* c = counter_create();
for (size_t i=0; i < n; i++) {
if (counter_incr(c) != 0) {
printf("overflow\n");
counter_destroy(c);
return -1;
}
}

printf("%" PRIu32 "\n", counter_get(c));
counter_destroy(c);

return 0;
}