外部函数接口
简介
本指南将使用 snappy 压缩/解压缩库作为编写外部代码绑定的入门。Rust 目前无法直接调用 C++ 库,但 snappy 包括一个 C 接口(文档位于 snappy-c.h
中)。
关于 libc 的说明
许多示例使用 libc
crate,它提供了 C 类型的各种类型定义等。如果您自己尝试这些示例,您需要将 libc
添加到您的 Cargo.toml
中
[dependencies]
libc = "0.2.0"
调用外部函数
以下是一个调用外部函数的最小示例,如果安装了 snappy,它将可以编译
use libc::size_t;
#[link(name = "snappy")]
extern {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("max compressed length of a 100 byte buffer: {}", x);
}
extern
代码块是外部库中的函数签名列表,在本例中,它使用平台的 C ABI。 #[link(...)]
属性用于指示链接器链接 snappy 库,以便解析符号。
外部函数被假定为是不安全的,因此需要使用 unsafe {}
将对它们的调用包装起来,以向编译器保证其中包含的所有内容都是真正安全的。C 库通常会公开非线程安全的接口,并且几乎任何采用指针参数的函数对于所有可能的输入都无效,因为指针可能悬空,并且原始指针超出了 Rust 的安全内存模型。
在声明外部函数的参数类型时,Rust 编译器无法检查声明是否正确,因此正确指定它是保持绑定在运行时正确的必要部分。
可以扩展 extern
代码块以覆盖整个 snappy API
use libc::{c_int, size_t};
#[link(name = "snappy")]
extern {
fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t) -> c_int;
fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t) -> c_int;
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t) -> c_int;
fn snappy_validate_compressed_buffer(compressed: *const u8,
compressed_length: size_t) -> c_int;
}
fn main() {}
创建安全接口
需要包装原始 C API,以提供内存安全性并使用更高级的概念(如向量)。库可以选择仅公开安全的高级接口并隐藏不安全的内部细节。
包装需要缓冲区的函数需要使用 slice::raw
模块将 Rust 向量作为内存指针进行操作。Rust 的向量保证是连续的内存块。长度是当前包含的元素数量,容量是已分配内存中元素总大小。长度小于或等于容量。
use libc::{c_int, size_t};
unsafe fn snappy_validate_compressed_buffer(_: *const u8, _: size_t) -> c_int { 0 }
fn main() {}
pub fn validate_compressed_buffer(src: &[u8]) -> bool {
unsafe {
snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
}
}
上面的 validate_compressed_buffer
包装器使用了 unsafe
代码块,但它通过从函数签名中省略 unsafe
来保证调用它对于所有输入都是安全的。
snappy_compress
和 snappy_uncompress
函数更复杂,因为也必须分配一个缓冲区来保存输出。
可以使用 snappy_max_compressed_length
函数来分配一个具有最大所需容量的向量,以保存压缩的输出。然后可以将该向量作为输出参数传递给 snappy_compress
函数。还会传递一个输出参数以在压缩后检索真实长度以设置长度。
use libc::{size_t, c_int};
unsafe fn snappy_compress(a: *const u8, b: size_t, c: *mut u8,
d: *mut size_t) -> c_int { 0 }
unsafe fn snappy_max_compressed_length(a: size_t) -> size_t { a }
fn main() {}
pub fn compress(src: &[u8]) -> Vec<u8> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen = snappy_max_compressed_length(srclen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
snappy_compress(psrc, srclen, pdst, &mut dstlen);
dst.set_len(dstlen as usize);
dst
}
}
解压缩类似,因为 snappy 将未压缩的大小存储为压缩格式的一部分,并且 snappy_uncompressed_length
将检索所需的确切缓冲区大小。
use libc::{size_t, c_int};
unsafe fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t) -> c_int { 0 }
unsafe fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t) -> c_int { 0 }
fn main() {}
pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen: size_t = 0;
snappy_uncompressed_length(psrc, srclen, &mut dstlen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {
dst.set_len(dstlen as usize);
Some(dst)
} else {
None // SNAPPY_INVALID_INPUT
}
}
}
然后,我们可以添加一些测试来展示如何使用它们。
use libc::{c_int, size_t};
unsafe fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t)
-> c_int { 0 }
unsafe fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t)
-> c_int { 0 }
unsafe fn snappy_max_compressed_length(source_length: size_t) -> size_t { 0 }
unsafe fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t)
-> c_int { 0 }
unsafe fn snappy_validate_compressed_buffer(compressed: *const u8,
compressed_length: size_t)
-> c_int { 0 }
fn main() { }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid() {
let d = vec![0xde, 0xad, 0xd0, 0x0d];
let c: &[u8] = &compress(&d);
assert!(validate_compressed_buffer(c));
assert!(uncompress(c) == Some(d));
}
#[test]
fn invalid() {
let d = vec![0, 0, 0, 0];
assert!(!validate_compressed_buffer(&d));
assert!(uncompress(&d).is_none());
}
#[test]
fn empty() {
let d = vec![];
assert!(!validate_compressed_buffer(&d));
assert!(uncompress(&d).is_none());
let c = compress(&d);
assert!(validate_compressed_buffer(&c));
assert!(uncompress(&c) == Some(d));
}
}
析构函数
外部库通常会将资源的所有权移交给调用代码。发生这种情况时,我们必须使用 Rust 的析构函数来提供安全性并保证这些资源的释放(尤其是在发生 panic 的情况下)。
有关析构函数的更多信息,请参阅 Drop trait。
从 C 调用 Rust 代码
您可能希望以一种可以从 C 调用的方式编译 Rust 代码。这相当容易,但需要一些事情。
Rust 端
首先,我们假设您有一个名为 rust_from_c
的 lib crate。lib.rs
应该有如下的 Rust 代码
#[no_mangle] pub extern "C" fn hello_from_rust() { println!("Hello from Rust!"); } fn main() {}
extern "C"
使此函数遵循 C 调用约定,如下文 “外部调用约定” 中所述。no_mangle
属性关闭了 Rust 的名称修饰,因此它具有一个明确定义的链接符号。
然后,要将 Rust 代码编译为可以从 C 调用的共享库,请将以下内容添加到您的 Cargo.toml
中
[lib]
crate-type = ["cdylib"]
(注意:我们也可以使用 staticlib
crate 类型,但它需要调整一些链接标志。)
运行 cargo build
,您就可以在 Rust 端开始了。
C 端
我们将创建一个 C 文件来调用 hello_from_rust
函数,并使用 gcc
编译它。
C 文件应如下所示
extern void hello_from_rust();
int main(void) {
hello_from_rust();
return 0;
}
我们将文件命名为 call_rust.c
并将其放置在 crate 的根目录中。运行以下命令进行编译
gcc call_rust.c -o call_rust -lrust_from_c -L./target/debug
-l
和 -L
告诉 gcc 查找我们的 Rust 库。
最后,我们可以使用指定的 LD_LIBRARY_PATH
从 C 调用 Rust 代码
$ LD_LIBRARY_PATH=./target/debug ./call_rust
Hello from Rust!
就是这样!有关更真实的示例,请查看 cbindgen
。
从 C 代码到 Rust 函数的回调
一些外部库需要使用回调来向调用者报告其当前状态或中间数据。可以将 Rust 中定义的函数传递给外部库。此要求是回调函数被标记为具有正确调用约定的 extern
,以使其可以从 C 代码调用。
然后,可以通过注册调用将回调函数发送到 C 库,然后从那里调用。
一个基本示例是
Rust 代码
extern fn callback(a: i32) { println!("I'm called from C with value {0}", a); } #[link(name = "extlib")] extern { fn register_callback(cb: extern fn(i32)) -> i32; fn trigger_callback(); } fn main() { unsafe { register_callback(callback); trigger_callback(); // Triggers the callback. } }
C 代码
typedef void (*rust_callback)(int32_t);
rust_callback cb;
int32_t register_callback(rust_callback callback) {
cb = callback;
return 1;
}
void trigger_callback() {
cb(7); // Will call callback(7) in Rust.
}
在此示例中,Rust 的 main()
将调用 C 中的 trigger_callback()
,后者又会回调 Rust 中的 callback()
。
将回调定向到 Rust 对象
前面的示例演示了如何从 C 代码调用全局函数。但是,通常希望将回调定向到特定的 Rust 对象。这可以是表示相应 C 对象包装器的对象。
这可以通过将指向该对象的原始指针传递给 C 库来实现。然后,C 库可以将指向 Rust 对象的指针包含在通知中。这将允许回调不安全地访问引用的 Rust 对象。
Rust 代码
struct RustObject { a: i32, // Other members... } extern "C" fn callback(target: *mut RustObject, a: i32) { println!("I'm called from C with value {0}", a); unsafe { // Update the value in RustObject with the value received from the callback: (*target).a = a; } } #[link(name = "extlib")] extern { fn register_callback(target: *mut RustObject, cb: extern fn(*mut RustObject, i32)) -> i32; fn trigger_callback(); } fn main() { // Create the object that will be referenced in the callback: let mut rust_object = Box::new(RustObject { a: 5 }); unsafe { register_callback(&mut *rust_object, callback); trigger_callback(); } }
C 代码
typedef void (*rust_callback)(void*, int32_t);
void* cb_target;
rust_callback cb;
int32_t register_callback(void* callback_target, rust_callback callback) {
cb_target = callback_target;
cb = callback;
return 1;
}
void trigger_callback() {
cb(cb_target, 7); // Will call callback(&rustObject, 7) in Rust.
}
异步回调
在前面给出的示例中,回调是对外部 C 库的函数调用直接反应而调用的。对当前线程的控制从 Rust 切换到 C,再切换到 Rust,以执行回调,但最终回调是在调用触发回调的函数的同一线程上执行的。
当外部库生成自己的线程并从那里调用回调时,事情会变得更加复杂。在这些情况下,在回调内部访问 Rust 数据结构尤其不安全,必须使用适当的同步机制。除了像互斥锁这样的经典同步机制之外,Rust 中的一种可能性是使用通道(在 std::sync::mpsc
中)将来自调用回调的 C 线程的数据转发到 Rust 线程。
如果异步回调的目标是 Rust 地址空间中的一个特定对象,那么绝对有必要的是,在相应的 Rust 对象被销毁后,C 库不再执行回调。这可以通过在对象的析构函数中取消注册回调,并以保证在取消注册后不会执行任何回调的方式来设计库来实现。
链接
extern
代码块上的 link
属性为指示 rustc 如何链接到本机库提供了基本构建块。今天有两种可接受的链接属性形式
#[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 上,框架的行为与动态库的语义相同。
不安全代码块
一些操作(如解引用原始指针或调用已标记为不安全的函数)仅允许在不安全代码块内进行。不安全代码块会隔离不安全行为,并向编译器保证不安全行为不会泄漏到代码块之外。
另一方面,不安全的函数会向世界宣传它。不安全的函数编写如下
#![allow(unused)] fn main() { unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr } }
此函数只能从 unsafe
代码块或其他 unsafe
函数中调用。
访问外部全局变量
外部 API 通常会导出一个全局变量,该变量可能会执行诸如跟踪全局状态之类的操作。为了访问这些变量,你需要使用 static
关键字在 extern
代码块中声明它们。
#[link(name = "readline")]
extern {
static rl_readline_version: libc::c_int;
}
fn main() {
println!("You have readline version {} installed.",
unsafe { rl_readline_version as i32 });
}
或者,你可能需要更改由外部接口提供的全局状态。为此,可以使用 mut
声明静态变量,以便我们修改它们。
use std::ffi::CString;
use std::ptr;
#[link(name = "readline")]
extern {
static mut rl_prompt: *const libc::c_char;
}
fn main() {
let prompt = CString::new("[my-awesome-shell] $").unwrap();
unsafe {
rl_prompt = prompt.as_ptr();
println!("{:?}", rl_prompt);
rl_prompt = ptr::null();
}
}
请注意,与 static mut
的所有交互都是不安全的,包括读取和写入。处理全局可变状态需要非常谨慎。
外部调用约定
大多数外部代码都公开了 C ABI,并且在调用外部函数时,Rust 默认使用平台的 C 调用约定。某些外部函数(最值得注意的是 Windows API)使用其他调用约定。Rust 提供了一种告诉编译器使用哪个约定的方法。
#[cfg(all(target_os = "win32", target_arch = "x86"))]
#[link(name = "kernel32")]
#[allow(non_snake_case)]
extern "stdcall" {
fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> libc::c_int;
}
fn main() { }
这适用于整个 extern
代码块。受支持的 ABI 约束列表如下:
stdcall
aapcs
cdecl
fastcall
thiscall
vectorcall
目前隐藏在abi_vectorcall
特性门后,可能会发生变化。锈
rust-intrinsic
system
C
win64
sysv64
此列表中的大多数 abi 都是不言自明的,但是 system
abi 可能会显得有些奇怪。此约束选择与目标库进行互操作的适当 ABI。例如,在 x86 架构的 win32 上,这意味着使用的 abi 将是 stdcall
。但是,在 x86_64 上,windows 使用 C
调用约定,因此将使用 C
。这意味着在我们之前的示例中,我们可以使用 extern "system" { ... }
来定义适用于所有 windows 系统(而不仅仅是 x86 系统)的代码块。
与外部代码的互操作性
只有在 struct
应用了 #[repr(C)]
属性时,Rust 才能保证 struct
的布局与 C 中的平台表示兼容。#[repr(C, packed)]
可用于在不使用填充的情况下布置结构成员。#[repr(C)]
也可以应用于枚举。
Rust 的所有权盒子 (Box<T>
) 使用不可为空的指针作为指向所包含对象的句柄。但是,不应手动创建它们,因为它们由内部分配器管理。可以安全地假设引用是指向类型的不可为空的指针。但是,破坏借用检查或可变性规则并不能保证是安全的,因此如果需要,最好使用原始指针 (*
),因为编译器无法对它们进行太多假设。
向量和字符串共享相同的基本内存布局,并且 vec
和 str
模块中提供了用于处理 C API 的实用程序。但是,字符串不会以 \0
结尾。如果需要以 NUL 结尾的字符串与 C 进行互操作,则应使用 std::ffi
模块中的 CString
类型。
crates.io 上的 libc
crate 在 libc
模块中包含 C 标准库的类型别名和函数定义,并且 Rust 默认链接到 libc
和 libm
。
可变参数函数
在 C 中,函数可以是“可变参数的”,这意味着它们接受可变数量的参数。这可以通过在外部函数声明的参数列表中指定 ...
在 Rust 中实现。
extern {
fn foo(x: i32, ...);
}
fn main() {
unsafe {
foo(10, 20, 30, 40, 50);
}
}
普通的 Rust 函数不能是可变参数的
#![allow(unused)] fn main() { // This will not compile fn foo(x: i32, ...) {} }
“可空指针优化”
某些 Rust 类型被定义为永远不会为 null
。这包括引用 (&T
, &mut T
)、盒子 (Box<T>
) 和函数指针 (extern "abi" fn()
)。在与 C 接口时,通常使用可能为 null
的指针,这似乎需要一些混乱的 transmute
和/或不安全的代码来处理与 Rust 类型之间的转换。但是,尝试构造/使用这些无效值是未定义行为,因此您应该改为使用以下解决方法。
作为一种特殊情况,如果 enum
包含正好两个变体,其中一个不包含任何数据,另一个包含上面列出的不可为空类型之一的字段,则该 enum
符合“可空指针优化”的条件。这意味着不需要额外的空间来存储判别符;相反,空变体通过将 null
值放入不可为空的字段中来表示。这被称为“优化”,但与其他优化不同,它保证应用于符合条件的类型。
利用可空指针优化最常见的类型是 Option<T>
,其中 None
对应于 null
。因此,Option<extern "C" fn(c_int) -> c_int>
是使用 C ABI 表示可空函数指针(对应于 C 类型 int (*)(int)
)的正确方法。
这是一个人为的例子。假设某个 C 库具有注册回调的功能,该回调会在特定情况下被调用。回调传递一个函数指针和一个整数,并且应该使用该整数作为参数来运行该函数。因此,我们有函数指针在 FFI 边界之间双向传递。
use libc::c_int;
#[cfg(hidden)]
extern "C" {
/// Registers the callback.
fn register(cb: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>, c_int) -> c_int>);
}
unsafe fn register(_: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>,
c_int) -> c_int>)
{}
/// This fairly useless function receives a function pointer and an integer
/// from C, and returns the result of calling the function with the integer.
/// In case no function is provided, it squares the integer by default.
extern "C" fn apply(process: Option<extern "C" fn(c_int) -> c_int>, int: c_int) -> c_int {
match process {
Some(f) => f(int),
None => int * int
}
}
fn main() {
unsafe {
register(Some(apply));
}
}
C 端代码如下所示
void register(int (*f)(int (*)(int), int)) {
...
}
无需 transmute
!
FFI 和展开
在使用 FFI 时,注意展开很重要。大多数 ABI 字符串有两种变体,一种带有 -unwind
后缀,另一种没有。Rust
ABI 始终允许展开,因此没有 Rust-unwind
ABI。
如果你希望 Rust panic
或外部(例如 C++)异常跨越 FFI 边界,则该边界必须使用适当的 -unwind
ABI 字符串。相反,如果你不希望展开跨越 ABI 边界,请使用非 unwind
ABI 字符串之一。
注意:使用
panic=abort
进行编译仍然会导致panic!
立即中止进程,无论panic
的函数指定了哪个 ABI。
如果展开操作遇到不允许展开的 ABI 边界,则该行为取决于展开的来源(Rust panic
或外部异常)
panic
将导致进程安全中止。- 进入 Rust 的外部异常将导致未定义的行为。
请注意,catch_unwind
与外部异常的交互是未定义的,panic
与外部异常捕获机制(特别是 C++ 的 try
/catch
)的交互也是未定义的。
Rust panic
与 "C-unwind"
#[no_mangle]
extern "C-unwind" fn example() {
panic!("Uh oh");
}
此函数(使用 panic=unwind
编译时)允许展开 C++ 堆栈帧。
[Rust function with `catch_unwind`, which stops the unwinding]
|
...
|
[C++ frames]
| ^
| (calls) | (unwinding
v | goes this
[Rust function `example`] | way)
| |
+--- rust function panics --+
如果 C++ 帧具有对象,则将调用其析构函数。
C++ throw
与 "C-unwind"
#[link(...)]
extern "C-unwind" {
// A C++ function that may throw an exception
fn may_throw();
}
#[no_mangle]
extern "C-unwind" fn rust_passthrough() {
let b = Box::new(5);
unsafe { may_throw(); }
println!("{:?}", &b);
}
具有 try
代码块的 C++ 函数可以调用 rust_passthrough
并捕获 may_throw
引发的异常。
[C++ function with `try` block that invokes `rust_passthrough`]
|
...
|
[Rust function `rust_passthrough`]
| ^
| (calls) | (unwinding
v | goes this
[C++ function `may_throw`] | way)
| |
+--- C++ function throws ----+
如果 may_throw
引发异常,则 b
将被丢弃。否则,将打印 5
。
panic
可以在 ABI 边界处停止
#![allow(unused)] fn main() { #[no_mangle] extern "C" fn assert_nonzero(input: u32) { assert!(input != 0) } }
如果使用参数 0
调用 assert_nonzero
,则无论是否使用 panic=abort
编译,运行时都保证(安全地)中止进程。
抢先捕获 panic
如果你正在编写可能出现 panic 的 Rust 代码,并且你不希望在出现 panic 时中止进程,则必须使用 catch_unwind
use std::panic::catch_unwind; #[no_mangle] pub extern "C" fn oh_no() -> i32 { let result = catch_unwind(|| { panic!("Oops!"); }); match result { Ok(_) => 0, Err(_) => 1, } } fn main() {}
请注意,catch_unwind
只会捕获展开 panic,而不会捕获那些中止进程的 panic。有关更多信息,请参阅 catch_unwind
的文档。
表示不透明结构体
有时,C 库想要提供指向某些内容的指针,但不让你知道它想要的内容的内部细节。一种稳定且简单的方法是使用 void *
参数
void foo(void *arg);
void bar(void *arg);
我们可以使用 c_void
类型在 Rust 中表示这一点
extern "C" {
pub fn foo(arg: *mut libc::c_void);
pub fn bar(arg: *mut libc::c_void);
}
fn main() {}
这是一种处理这种情况的完全有效的方法。但是,我们可以做得更好。为了解决这个问题,一些 C 库会创建一个 struct
,其中该 struct 的细节和内存布局是私有的。这提供了一定程度的类型安全性。这些结构被称为“不透明”。这是一个 C 中的示例
struct Foo; /* Foo is a structure, but its contents are not part of the public interface */
struct Bar;
void foo(struct Foo *arg);
void bar(struct Bar *arg);
为了在 Rust 中做到这一点,让我们创建自己的不透明类型
#[repr(C)] pub struct Foo { _data: [u8; 0], _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, } #[repr(C)] pub struct Bar { _data: [u8; 0], _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, } extern "C" { pub fn foo(arg: *mut Foo); pub fn bar(arg: *mut Bar); } fn main() {}
通过包含至少一个私有字段且没有构造函数,我们创建了一个不透明类型,我们无法在此模块之外实例化该类型。(没有任何字段的 struct 可以由任何人实例化。)我们还希望在 FFI 中使用此类型,因此我们必须添加 #[repr(C)]
。该标记确保编译器不会将该 struct 标记为 Send
,Sync
和 Unpin
不适用于该 struct。(*mut u8
不是 Send
或 Sync
,PhantomPinned
不是 Unpin
)
但是,由于我们的 Foo
和 Bar
类型不同,我们将在它们之间获得类型安全性,因此我们不会意外地将指向 Foo
的指针传递给 bar()
。
请注意,将空枚举用作 FFI 类型是一个非常糟糕的主意。编译器依赖于空枚举是未居住的,因此处理 &Empty
类型的值是一个巨大的陷阱,并可能导致程序行为错误(通过触发未定义的行为)。
注意:最简单的方法是使用“外部类型”。但是它目前(截至 2021 年 6 月)是不稳定的,并且有一些未解决的问题,有关更多详细信息,请参见 RFC 页面和 跟踪问题。