外国函数接口

引言

本指南将使用 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")]
unsafe extern "C" {
    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 的,因此调用它们需要用 unsafe {} 包裹,以此向编译器保证其中包含的所有内容确实是安全的。C 库通常会暴露非线程安全的接口,并且几乎任何接受指针参数的函数对于所有可能的输入都是无效的,因为指针可能是悬垂指针,而且裸指针超出了 Rust 的安全内存模型。

声明外部函数的参数类型时,Rust 编译器无法检查声明是否正确,因此正确指定它是保持运行时绑定正确的一部分。

extern 块可以扩展以涵盖整个 snappy API

use libc::{c_int, size_t};

#[link(name = "snappy")]
unsafe 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_compresssnappy_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 代码:

#[unsafe(no_mangle)]
pub extern "C" fn hello_from_rust() {
    println!("Hello from Rust!");
}
fn main() {}

extern "C" 使此函数遵循 C 调用约定,如下文 "外部调用约定" 中所述。no_mangle 属性关闭了 Rust 的名称修饰(name mangling),以便它有一个明确的符号可以链接到。

然后,要将 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")]
unsafe 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...
}

unsafe 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")]
unsafe extern {
   fn register_callback(target: *mut RustObject,
                        cb: unsafe 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 属性有两种可接受的形式:

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

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

  • 动态库 (Dynamic) - #[link(name = "readline")]
  • 静态库 (Static) - #[link(name = "my_build_dependency", kind = "static")]
  • 框架 (Frameworks) - #[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 上,框架的行为与动态库的语义相同。

Unsafe 块

某些操作,例如解引用裸指针或调用标记为 unsafe 的函数,只允许在 unsafe 块内执行。unsafe 块隔离了不安全性,并向编译器承诺不安全性不会从块中泄露出去。

另一方面,unsafe 函数则向外部宣称其不安全性。unsafe 函数是这样编写的:

#![allow(unused)]
fn main() {
unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }
}

此函数只能从 unsafe 块或另一个 unsafe 函数中调用。

访问外部全局变量

外部 API 通常会导出全局变量,这些变量可以用于跟踪全局状态等。为了访问这些变量,你需要在 extern 块中使用 static 关键字声明它们:

#[link(name = "readline")]
unsafe 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")]
unsafe 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)]
unsafe 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 gate 后面,可能会发生变化。
  • Rust
  • rust-intrinsic
  • system
  • C
  • win64
  • sysv64

此列表中的大多数 abi 都是不言自明的,但 system abi 可能看起来有点奇怪。此约束选择与目标库交互的适当 ABI。例如,在 x86 架构的 win32 上,这意味着使用的 abi 将是 stdcall。然而,在 x86_64 上,Windows 使用 C 调用约定,因此将使用 C。这意味着在之前的示例中,我们可以使用 extern "system" { ... } 来定义一个适用于所有 Windows 系统(而不仅仅是 x86 系统)的块。

与外部代码的互操作性

Rust 保证只有当 #[repr(C)] 属性应用于 struct 时,其内存布局才与平台在 C 中的表示兼容。#[repr(C, packed)] 可用于在没有填充的情况下排列结构体成员。#[repr(C)] 也可以应用于 enum。

Rust 的自有盒子 (Box<T>) 使用非空指针作为句柄,指向包含的对象。但是,不应手动创建它们,因为它们由内部分配器管理。引用可以安全地假定为直接指向类型的非空指针。但是,违反借用检查或可变性规则不能保证是安全的,因此如果需要,请优先使用裸指针 (*),因为编译器对它们不能做太多假设。

向量和字符串共享相同的基本内存布局,并且 vecstr 模块中提供了与 C API 协作的工具。但是,字符串不是以 \0 结尾的。如果需要一个以 NUL 结尾的字符串以便与 C 互操作,你应该使用 std::ffi 模块中的 CString 类型。

The crates.io 上的 libc cratelibc 模块中包含了 C 标准库的类型别名和函数定义,并且 Rust 默认链接 libclibm

可变参数函数

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

unsafe 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 和/或 unsafe 代码来处理与 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)]
unsafe 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 函数的 ABI,使用 panic=abort 编译仍然会导致 panic! 立即中止进程。

如果栈展开操作确实遇到了不允许栈展开的 ABI 边界,其行为取决于栈展开的来源(Rust 的 panic 或外部异常):

  • panic 将导致进程安全中止。
  • 外部异常进入 Rust 将导致未定义行为。

注意,catch_unwind 与外部异常的交互是未定义的,同样,panic 与外部异常捕获机制(特别是 C++ 的 try/catch)的交互也是未定义的。

Rust panic"C-unwind"

#[unsafe(no_mangle)]
unsafe 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(...)]
unsafe extern "C-unwind" {
    // A C++ function that may throw an exception
    fn may_throw();
}

#[unsafe(no_mangle)]
unsafe extern "C-unwind" fn rust_passthrough() {
    let b = Box::new(5);
    unsafe { may_throw(); }
    println!("{:?}", &b);
}

带有 try 块的 C++ 函数可能会调用 rust_passthroughcatch 捕获由 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 将被丢弃(drop)。否则,将打印 5

panic 可以在 ABI 边界停止

#![allow(unused)]
fn main() {
#[unsafe(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;

#[unsafe(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);

我们可以在 Rust 中使用 c_void 类型来表示这一点:

unsafe extern "C" {
    pub fn foo(arg: *mut libc::c_void);
    pub fn bar(arg: *mut libc::c_void);
}
fn main() {}

这是一种完全有效的方法来处理这种情况。然而,我们可以做得更好一些。为了解决这个问题,一些 C 库会转而创建一个 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: (),
    _marker:
        core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
#[repr(C)]
pub struct Bar {
    _data: (),
    _marker:
        core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}

unsafe extern "C" {
    pub fn foo(arg: *mut Foo);
    pub fn bar(arg: *mut Bar);
}
fn main() {}

通过包含至少一个私有字段且没有构造函数,我们创建了一个在这个模块之外无法实例化的不透明类型。(没有字段的结构体任何人都可以实例化。)我们还希望在 FFI 中使用这种类型,所以必须添加 #[repr(C)]。标记确保编译器不会将结构体标记为 SendSync,并且 Unpin 不应用于此结构体。(*mut u8 不是 SendSyncPhantomPinned 不是 Unpin)。

但由于我们的 FooBar 类型不同,它们之间将获得类型安全,因此我们不会意外地将指向 Foo 的指针传递给 bar()

注意,将空枚举用作 FFI 类型是一个非常糟糕的主意。编译器依赖于空枚举是不可实例化的(uninhabited),因此处理类型为 &Empty 的值是一个巨大的陷阱(footgun),可能导致程序出现错误行为(通过触发未定义行为)。

注意:最简单的方法是使用“外部类型”(extern types)。但它目前(截至 2021 年 6 月)是不稳定的,并且存在一些未解决的问题,详情请参见 RFC 页面跟踪 issue