外部函数接口(FFI)

简介

本指南将使用snappy压缩/解压缩库作为为外部代码编写绑定的示例。Rust 目前无法直接调用 C++ 库,但 snappy 包括一个 C 接口(在snappy-c.h)。

关于 libc 的说明

这些例子中有许多使用了the libc crate,它为 C 类型提供了各种类型定义,以及其他东西。如果你要自己尝试这些例子, 你需要在你的Cargo.toml中加入libc

  1. [dependencies]
  2. libc = "0.2.0"

调用外部函数

下面是一个调用外部函数的最小例子,如果你安装了 snappy,它就可以被编译:

  1. use libc::size_t;
  2. #[link(name = "snappy")]
  3. extern {
  4. fn snappy_max_compressed_length(source_length: size_t) -> size_t;
  5. }
  6. fn main() {
  7. let x = unsafe { snappy_max_compressed_length(100) };
  8. println!("max compressed length of a 100 byte buffer: {}", x);
  9. }

extern块是一个外部库中的函数签名列表,在本例中是平台的 C ABI。#[link(...)]属性用来指示链接器与 snappy 库进行链接,以便解析这些符号。

外部函数被认为是不安全的,所以对它们的调用需要用unsafe {}来包装,作为对编译器的承诺,其中包含的所有内容都是安全的。C 库经常暴露出不是线程安全的接口,而且几乎所有接受指针参数的函数都对一些输入是无效的,因为指针可能是悬空的,而原始指针不在 Rust 的安全内存模型之内。

当声明一个外部函数的参数类型时,Rust 编译器不能检查声明是否正确,所以正确指定它是在运行时保持绑定正确的一部分。

extern块可以被扩展到覆盖整个 snappy API:

  1. use libc::{c_int, size_t};
  2. #[link(name = "snappy")]
  3. extern {
  4. fn snappy_compress(input: *const u8,
  5. input_length: size_t,
  6. compressed: *mut u8,
  7. compressed_length: *mut size_t) -> c_int;
  8. fn snappy_uncompress(compressed: *const u8,
  9. compressed_length: size_t,
  10. uncompressed: *mut u8,
  11. uncompressed_length: *mut size_t) -> c_int;
  12. fn snappy_max_compressed_length(source_length: size_t) -> size_t;
  13. fn snappy_uncompressed_length(compressed: *const u8,
  14. compressed_length: size_t,
  15. result: *mut size_t) -> c_int;
  16. fn snappy_validate_compressed_buffer(compressed: *const u8,
  17. compressed_length: size_t) -> c_int;
  18. }
  19. # fn main() {}

创建一个安全的接口

原始的 C 语言 API 需要被包装起来,以提供内存安全,并使用更高级别的概念,如向量。一个库可以选择只公开安全的高级接口而隐藏不安全的内部细节。

封装一个需要内存 buffer 参数的函数需要使用slice::raw模块来操作 Rust Vec 作为内存的指针。Rust 的 Vec 被保证为一个连续的内存块,长度是当前包含的元素数,容量是分配的内存的总大小(元素),其中长度必定小于或等于容量:

  1. # use libc::{c_int, size_t};
  2. # unsafe fn snappy_validate_compressed_buffer(_: *const u8, _: size_t) -> c_int { 0 }
  3. # fn main() {}
  4. pub fn validate_compressed_buffer(src: &[u8]) -> bool {
  5. unsafe {
  6. snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
  7. }
  8. }

上面的“validate_compressed_buffer”包装器使用了一个“unsafe”块,但它通过在函数签名中去掉“unsafe”来保证调用它对所有输入都是安全的。

snappy_compresssnappy_uncompress函数更复杂,因为还需要分配一个缓冲区来容纳输出。

snappy_max_compressed_length函数可以用来分配一个最大容量的 Vec,以容纳压缩后的输出,然后该向量可以作为输出参数传递给snappy_compress函数。还会传递一个输出参数来检索压缩后的真实长度,以便设置长度:

  1. # use libc::{size_t, c_int};
  2. # unsafe fn snappy_compress(a: *const u8, b: size_t, c: *mut u8,
  3. # d: *mut size_t) -> c_int { 0 }
  4. # unsafe fn snappy_max_compressed_length(a: size_t) -> size_t { a }
  5. # fn main() {}
  6. pub fn compress(src: &[u8]) -> Vec<u8> {
  7. unsafe {
  8. let srclen = src.len() as size_t;
  9. let psrc = src.as_ptr();
  10. let mut dstlen = snappy_max_compressed_length(srclen);
  11. let mut dst = Vec::with_capacity(dstlen as usize);
  12. let pdst = dst.as_mut_ptr();
  13. snappy_compress(psrc, srclen, pdst, &mut dstlen);
  14. dst.set_len(dstlen as usize);
  15. dst
  16. }
  17. }

解压缩也是类似的,因为 snappy 将未压缩的大小作为压缩格式的一部分来存储,snappy_uncompressed_length将检索出所需的确切缓冲区大小:

  1. # use libc::{size_t, c_int};
  2. # unsafe fn snappy_uncompress(compressed: *const u8,
  3. # compressed_length: size_t,
  4. # uncompressed: *mut u8,
  5. # uncompressed_length: *mut size_t) -> c_int { 0 }
  6. # unsafe fn snappy_uncompressed_length(compressed: *const u8,
  7. # compressed_length: size_t,
  8. # result: *mut size_t) -> c_int { 0 }
  9. # fn main() {}
  10. pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {
  11. unsafe {
  12. let srclen = src.len() as size_t;
  13. let psrc = src.as_ptr();
  14. let mut dstlen: size_t = 0;
  15. snappy_uncompressed_length(psrc, srclen, &mut dstlen);
  16. let mut dst = Vec::with_capacity(dstlen as usize);
  17. let pdst = dst.as_mut_ptr();
  18. if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {
  19. dst.set_len(dstlen as usize);
  20. Some(dst)
  21. } else {
  22. None // SNAPPY_INVALID_INPUT
  23. }
  24. }
  25. }

然后,我们可以添加一些测试来展示如何使用它们:

  1. # use libc::{c_int, size_t};
  2. # unsafe fn snappy_compress(input: *const u8,
  3. # input_length: size_t,
  4. # compressed: *mut u8,
  5. # compressed_length: *mut size_t)
  6. # -> c_int { 0 }
  7. # unsafe fn snappy_uncompress(compressed: *const u8,
  8. # compressed_length: size_t,
  9. # uncompressed: *mut u8,
  10. # uncompressed_length: *mut size_t)
  11. # -> c_int { 0 }
  12. # unsafe fn snappy_max_compressed_length(source_length: size_t) -> size_t { 0 }
  13. # unsafe fn snappy_uncompressed_length(compressed: *const u8,
  14. # compressed_length: size_t,
  15. # result: *mut size_t)
  16. # -> c_int { 0 }
  17. # unsafe fn snappy_validate_compressed_buffer(compressed: *const u8,
  18. # compressed_length: size_t)
  19. # -> c_int { 0 }
  20. # fn main() { }
  21. #
  22. #[cfg(test)]
  23. mod tests {
  24. use super::*;
  25. #[test]
  26. fn valid() {
  27. let d = vec![0xde, 0xad, 0xd0, 0x0d];
  28. let c: &[u8] = &compress(&d);
  29. assert!(validate_compressed_buffer(c));
  30. assert!(uncompress(c) == Some(d));
  31. }
  32. #[test]
  33. fn invalid() {
  34. let d = vec![0, 0, 0, 0];
  35. assert!(!validate_compressed_buffer(&d));
  36. assert!(uncompress(&d).is_none());
  37. }
  38. #[test]
  39. fn empty() {
  40. let d = vec![];
  41. assert!(!validate_compressed_buffer(&d));
  42. assert!(uncompress(&d).is_none());
  43. let c = compress(&d);
  44. assert!(validate_compressed_buffer(&c));
  45. assert!(uncompress(&c) == Some(d));
  46. }
  47. }

析构器

外部的库经常把资源的所有权交给调用代码,当这种情况发生时,我们必须使用 Rust 的析构器来提供安全并保证这些资源的释放(尤其是在 panic 的情况下)。

关于析构器的更多信息,请参见 Drop trait

从 C 调用 Rust 代码

你可能想要把 Rust 代码编译成某种形式,以便在 C 中调用。这个并不难,不过需要一些额外的步骤。

Rust 代码侧

首先,我们假设你有一个 lib 库名字叫rust_from_c,其中的lib.rs应该包含类似这样的代码:

  1. #[no_mangle]
  2. pub extern "C" fn hello_from_rust() {
  3. println!("Hello from Rust!");
  4. }
  5. # fn main() {}

extern "C"使得这个函数使用 C 的调用规约,正如下文外部调用规约一章所述。 no_mangle属性关闭了 Rust 的 name mangling 特性,这使得我们在链接时有个明确定义的符号名。

接下来,为了把我们的 Rust 代码编译成一个可以直接从 C 调用的共享库,我们需要加这些到Cargo.toml中:

  1. [lib]
  2. crate-type = ["cdylib"]

(注意:我们也可以用staticlib类型,不过这会需要我们修改一些链接的参数。)

接下来,执行cargo build,Rust 侧就搞定啦!

C 代码侧

我们将写一段 C 代码来调用hello_from_rust并用gcc来编译。

C 代码大致是这样:

  1. extern void hello_from_rust();
  2. int main(void) {
  3. hello_from_rust();
  4. return 0;
  5. }

我们把这个文件命名为call_rust.c,并且把它放到我们 crate 的根目录下,然后编译:

  1. gcc call_rust.c -o call_rust -lrust_from_c -L./target/debug

-l-L告诉 gcc 去找我们的 Rust 库。

最后,我们可以通过指定LD_LIBRARY_PATH来从 C 调用 Rust:

  1. $ LD_LIBRARY_PATH=./target/debug ./call_rust
  2. Hello from Rust!

搞定! 如果需要更多实际的例子,可以参考cbindgen

从 C 代码到 Rust 函数的回调

一些外部库需要使用回调来向调用者报告其当前状态或中间数据,我们可以将 Rust 中定义的函数传递给外部库。这方面的要求是,回调函数被标记为“extern”,并有正确的调用约定,使其可以从 C 代码中调用。

然后,回调函数可以通过注册调用发送到 C 库中,之后再从那里调用。

一个基本的例子是:

Rust 代码:

  1. extern fn callback(a: i32) {
  2. println!("I'm called from C with value {0}", a);
  3. }
  4. #[link(name = "extlib")]
  5. extern {
  6. fn register_callback(cb: extern fn(i32)) -> i32;
  7. fn trigger_callback();
  8. }
  9. fn main() {
  10. unsafe {
  11. register_callback(callback);
  12. trigger_callback(); // 触发回调
  13. }
  14. }

C 代码:

  1. typedef void (*rust_callback)(int32_t);
  2. rust_callback cb;
  3. int32_t register_callback(rust_callback callback) {
  4. cb = callback;
  5. return 1;
  6. }
  7. void trigger_callback() {
  8. cb(7); // 在 Rust 中会调用回调函数 callback(7)
  9. }

在这个例子中,Rust 的main()将调用 C 语言中的trigger_callback(),而这又会回调 Rust 中的callback()

针对 Rust 对象的回调

前面的例子展示了如何从 C 代码中调用一个全局函数,然而,人们通常希望回调是针对一个特殊的 Rust 对象,这可能是代表相应的 C 对象的封装器的对象。

这可以通过向 C 库传递一个指向该对象的原始指针来实现,然后,C 库可以在通知中包含指向 Rust 对象的指针,这将使回调能够不安全地访问引用的 Rust 对象。

Rust 代码:

  1. struct RustObject {
  2. a: i32,
  3. // 其余的成员...
  4. }
  5. extern "C" fn callback(target: *mut RustObject, a: i32) {
  6. println!("I'm called from C with value {0}", a);
  7. unsafe {
  8. // 在回调函数中更新 RustObject 的内容
  9. (*target).a = a;
  10. }
  11. }
  12. #[link(name = "extlib")]
  13. extern {
  14. fn register_callback(target: *mut RustObject,
  15. cb: extern fn(*mut RustObject, i32)) -> i32;
  16. fn trigger_callback();
  17. }
  18. fn main() {
  19. // 创建一个会被在回调函数中引用的 RustObject
  20. let mut rust_object = Box::new(RustObject { a: 5 });
  21. unsafe {
  22. register_callback(&mut *rust_object, callback);
  23. trigger_callback();
  24. }
  25. }

C 代码:

  1. typedef void (*rust_callback)(void*, int32_t);
  2. void* cb_target;
  3. rust_callback cb;
  4. int32_t register_callback(void* callback_target, rust_callback callback) {
  5. cb_target = callback_target;
  6. cb = callback;
  7. return 1;
  8. }
  9. void trigger_callback() {
  10. cb(cb_target, 7); // 这会调用 Rust 代码中的 callback(&rustObject, 7)
  11. }

异步回调

在之前给出的例子中,回调是作为对外部 C 库的函数调用的同步调用的。为了执行回调,对当前线程的控制从 Rust 切换到 C,再切换到 Rust,但最终回调是在调用触发回调的函数的同一线程上执行。

当外部库生成自己的线程并从那里调用回调时,事情会变得更加复杂。在这种情况下,对回调中的 Rust 数据结构的访问特别不安全,必须使用适当的同步机制。除了像 mutex 这样的经典同步机制,Rust 中的一种可能性是使用通道(在std::sync::mpsc中),将数据从调用回调的 C 线程转发到 Rust 线程。

如果一个异步回调的目标是 Rust 地址空间中的一个特殊对象,那么在相应的 Rust 对象被销毁后,C 库也绝对不能再进行回调。这可以通过在对象的析构器中取消对回调的注册来实现,并以保证在取消注册后不执行回调的方式设计库。

链接

extern块上的link属性提供了基本的构建模块,用于指示 rustc 如何链接到本地库。现在有两种可接受的 link 属性的形式:

  • #[link(name = "foo")]
  • #[link(name = "foo", kind = "bar")]

在这两种情况下,foo是我们要链接的本地库的名称,在第二种情况下,bar是编译器要链接的本地库的类型。目前已知有三种类型的本地库:

  • 动态 - #[link(name = "readline")]
  • 静态 - #[link(name = "my_build_dependency", kind = "static")]
  • 框架 - #[link(name = "CoreFoundation", kind = "framework")]

注意,框架只在 macOS 上可用。

不同的kind值是为了区分本地库如何参与链接。从链接的角度来看,Rust 编译器创建了两种类型的工件:部分(rlib/staticlib)和最终(dylib/binary)。原生的动态库和框架依赖被传播到最终的可执行文件中,而静态库的依赖则完全不被传播,因为静态库被直接集成到后续的可执行文件中的。

来看几个这个模型如何使用的例子:

  • 一个本地构建依赖。有时在编写一些 Rust 代码时需要一些 C/C++ 胶水,但以库的形式分发 C/C++ 代码是一种负担。在这种情况下,代码将被归档到libfoo.a,然后 Rust crate 将通过#[link(name = "foo", kind = "static")]声明一个依赖关系。

    无论 crate 的输出是什么,本地静态库都会被包含在输出中,这意味着本地静态库的分发是没有必要的。

  • 一个正常的动态依赖。常见的系统库(如readline)在大量的系统上可用,而这些库的静态副本往往找不到。当这种依赖被包含在 Rust crate 中时,部分目标(如 rlibs)将不会链接到该库,但当 rlib 被包含在最终目标(如二进制)中时,本地库将被链接进来。

在 macOS 上,框架的行为与动态库的语义相同。

不安全块

一些操作,如取消引用原始指针或调用被标记为不安全的函数,只允许在不安全块中进行。不安全块隔离了不安全因素,并向编译器承诺不安全因素不会从块中泄露出去。

另一方面,不安全的函数则向世界公布了它。一个不安全的函数是这样写的:

  1. unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }

这个函数只能从一个“不安全”块或另一个“不安全”函数中调用。

访问外部的全局变量

外部的 API 经常输出一个全局变量,它可以做一些类似于跟踪全局状态的事情。为了访问这些变量,你可以在extern块中用static关键字来声明它们:

  1. #[link(name = "readline")]
  2. extern {
  3. static rl_readline_version: libc::c_int;
  4. }
  5. fn main() {
  6. println!("You have readline version {} installed.",
  7. unsafe { rl_readline_version as i32 });
  8. }

另外,你可能需要改变由外部接口提供的全局状态。要做到这一点,可以用mut声明全局变量,这样我们就可以改变它们:

  1. use std::ffi::CString;
  2. use std::ptr;
  3. #[link(name = "readline")]
  4. extern {
  5. static mut rl_prompt: *const libc::c_char;
  6. }
  7. fn main() {
  8. let prompt = CString::new("[my-awesome-shell] $").unwrap();
  9. unsafe {
  10. rl_prompt = prompt.as_ptr();
  11. println!("{:?}", rl_prompt);
  12. rl_prompt = ptr::null();
  13. }
  14. }

注意,所有“可变全局变量”的交互都是不安全的,包括读和写。处理全局可变状态需要非常小心。

外部调用规约

大多数外部代码都暴露了一个 C ABI,Rust 在调用外部函数时默认使用平台的 C 调用约定。一些外部函数,最明显的是 Windows API,使用了其他的调用约定。Rust 提供了一种方法来告诉编译器应该使用哪种约定:

  1. #[cfg(all(target_os = "win32", target_arch = "x86"))]
  2. #[link(name = "kernel32")]
  3. #[allow(non_snake_case)]
  4. extern "stdcall" {
  5. fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> libc::c_int;
  6. }
  7. # fn main() { }

这适用于整个extern块。支持的 ABI 约束列表如下:

  • stdcall
  • aapcs
  • cdecl
  • fastcall
  • vectorcall 这是目前隐藏在abi_vectorcall特性开关后面的,可能会有变化
  • Rust
  • rust-intrinsic
  • system
  • C
  • win64
  • sysv64

这个列表中的大多数 ABI 是不言自明的,但是system ABI 可能看起来有点奇怪。这个约束条件选择了任何合适的 ABI 来与目标库进行交互操作。例如,在 x86 架构的 win32 上,这意味着使用的 ABI 是stdcall。然而,在 x86_64 上,windows 使用C调用惯例,所以将使用C。这意味着在我们之前的例子中,我们可以使用extern "system" { ... }来为所有的 windows 系统定义一个块,而不仅仅是 x86 系统。

与外部代码的互操作性

只有当#[repr(C)]属性应用于一个struct时,Rust 才能保证该结构的布局与平台的 C 语言表示兼容。#[repr(C, packed)]可以用来布局结构成员而不需要填充。#[repr(C)]也可以应用于枚举。

Rust 的 Box 类型(Box<T>)使用不可为空的指针作为句柄,指向所包含的对象。然而,它们不应该被手动创建,因为它们是由内部分配器管理的。引用可以安全地被认为是直接指向该类型的不可归零的指针。然而,打破借用检查或可变性规则是不安全的,所以如果需要的话,最好使用原始指针(*),因为编译器不能对它们做出那么多假设。

向量和字符串共享相同的基本内存布局,并且在vecstr模块中提供了与 C API 工作的实用程序。然而,字符串不是以\0结束的。如果你需要一个以 NUL 结尾的字符串与 C 语言互通,你应该使用std::ffi模块中的CString类型。

crates.io 上的libc crate包括libc模块中的 C 标准库的类型别名和函数定义,Rust 默认与libclibm链接。

Variadic 函数

在 C 语言中,函数可以是“variadic”,这意味着它们接受可变数量的参数。这在 Rust 中可以通过在外部函数声明的参数列表中指定“…”来实现:

  1. extern {
  2. fn foo(x: i32, ...);
  3. }
  4. fn main() {
  5. unsafe {
  6. foo(10, 20, 30, 40, 50);
  7. }
  8. }

正常的 Rust 函数不能是可变参数的:

  1. // 这不会编译通过
  2. fn foo(x: i32, ...) {}

“空指针优化”

某些 Rust 类型被定义为永不为“空”。这包括引用(&T, &mut T), Box(Box<T>), 和函数指针(extern "abi" fn())。当与 C 语言对接时,经常使用可能为“空”的指针,这似乎需要一些混乱的transmute和/或不安全的代码来处理与 Rust 类型的转换。然而,该语言提供了一个变通办法。

作为一种特殊情况,如果一个“enum”正好包含两个变体,其中一个不包含数据,另一个包含上面列出的非空类型的字段,那么它就有资格获得“空指针优化”。这意味着不需要额外的空间来进行判别;相反,空的变体是通过将一个null的值放入不可空的字段来表示。这被称为“优化”,但与其他优化不同,它保证适用于符合条件的类型。

最常见的利用空指针优化的类型是Option<T>,其中None对应于null。所以Option<extern "C" fn(c_int) -> c_int>是使用 C ABI(对应于 C 类型int (*)(int))来表示可空函数指针的一种正确方式。

这里有一个臆造的例子:假设某个 C 库有一个用于注册回调的工具,在某些情况下会被调用。回调被传递给一个函数指针和一个整数,它应该以整数为参数运行该函数。所以我们有函数指针在 FFI 边界上双向飞行。

  1. use libc::c_int;
  2. # #[cfg(hidden)]
  3. extern "C" {
  4. /// 注册回调函数
  5. fn register(cb: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>, c_int) -> c_int>);
  6. }
  7. # unsafe fn register(_: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>,
  8. # c_int) -> c_int>)
  9. # {}
  10. // 这个函数其实没什么实际的用处,
  11. // 它从C代码接受一个函数指针和一个整数,
  12. // 用整数做参数,调用指针指向的函数,并返回函数的返回值,
  13. // 如果没有指定函数,那默认就返回整数的平方
  14. extern "C" fn apply(process: Option<extern "C" fn(c_int) -> c_int>, int: c_int) -> c_int {
  15. match process {
  16. Some(f) => f(int),
  17. None => int * int
  18. }
  19. }
  20. fn main() {
  21. unsafe {
  22. register(Some(apply));
  23. }
  24. }

而 C 语言方面的代码看起来是这样的:

  1. void register(int (*f)(int (*)(int), int)) {
  2. ...
  3. }

实际上,不需要transmute!

FFI 和 panic

在使用 FFI 时,必须注意panic!。一个跨越 FFI 边界的“panic!”是未定义的行为。如果你写的代码可能会出现恐慌,你应该用catch_unwind在闭包中运行它。

  1. use std::panic::catch_unwind;
  2. #[no_mangle]
  3. pub extern fn oh_no() -> i32 {
  4. let result = catch_unwind(|| {
  5. panic!("Oops!");
  6. });
  7. match result {
  8. Ok(_) => 0,
  9. Err(_) => 1,
  10. }
  11. }
  12. fn main() {}

请注意,catch_unwind只捕捉 unwind 的 panic,而不是那些中止进程的恐慌。更多信息请参见catch_unwind的文档。

表示不透明(opaque)的结构

有时,一个 C 语言库想提供一个指向某东西的指针,但又不想让你知道它想要的东西的内部细节。一个稳定而简单的方法是使用一个void *参数。

  1. void foo(void *arg);
  2. void bar(void *arg);

我们可以在 Rust 中用c_void类型来表示。

  1. extern "C" {
  2. pub fn foo(arg: *mut libc::c_void);
  3. pub fn bar(arg: *mut libc::c_void);
  4. }
  5. # fn main() {}

这是一种完全有效的处理方式。然而,我们可以做得更好一点。为了解决这个问题,一些 C 库会创建一个struct,其中结构的细节和内存布局是私有的,这提供了某种程度的类型安全。这些结构被称为“不透明的”。下面是一个例子,在 C 语言中:

  1. struct Foo; /* Foo 是一个接口,但它的内容不属于公共接口 */
  2. struct Bar;
  3. void foo(struct Foo *arg);
  4. void bar(struct Bar *arg);

为了在 Rust 中做到这一点,让我们创建我们自己的不透明类型:

  1. #[repr(C)]
  2. pub struct Foo {
  3. _data: [u8; 0],
  4. _marker:
  5. core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
  6. }
  7. #[repr(C)]
  8. pub struct Bar {
  9. _data: [u8; 0],
  10. _marker:
  11. core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
  12. }
  13. extern "C" {
  14. pub fn foo(arg: *mut Foo);
  15. pub fn bar(arg: *mut Bar);
  16. }
  17. # fn main() {}

通过包括至少一个私有字段和没有构造函数,我们创建了一个不透明的类型,我们不能在这个模块之外实例化(否则,一个没有字段的结构可以被任何人实例化)。我们也想在 FFI 中使用这个类型,所以我们必须添加#[repr(C)]。该标记确保编译器不会将该结构标记为SendSync,并且Unpin也不会应用于该结构(*mut u8不是Send或者SyncPhantomPinned也不是Unpin)。

但是因为我们的FooBar类型不同,我们将在它们两个之间获得类型安全,所以我们不能意外地将Foo的指针传递给bar()

注意,使用空枚举作为 FFI 类型是一个非常糟糕的主意。编译器假设空枚举是无法使用的,所以处理&Empty类型的值会是意料之外的,并可能导致错误的程序行为(通过触发未定义行为)。

注意: 最简单的方法还是使用“extern 类型”。但它目前(截至 2021 年 10 月)还不稳定,而且还有一些未解决的问题,更多细节请参见RFC 页面跟踪 Issue