外部函数接口

简介

本指南将使用 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_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 代码

#[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>) 使用不可为空的指针作为指向所包含对象的句柄。但是,不应手动创建它们,因为它们由内部分配器管理。可以安全地假设引用是指向类型的不可为空的指针。但是,破坏借用检查或可变性规则并不能保证是安全的,因此如果需要,最好使用原始指针 (*),因为编译器无法对它们进行太多假设。

向量和字符串共享相同的基本内存布局,并且 vecstr 模块中提供了用于处理 C API 的实用程序。但是,字符串不会以 \0 结尾。如果需要以 NUL 结尾的字符串与 C 进行互操作,则应使用 std::ffi 模块中的 CString 类型。

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

可变参数函数

在 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 标记为 SendSyncUnpin 不适用于该 struct。(*mut u8 不是 SendSyncPhantomPinned 不是 Unpin

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

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

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