外部函数接口

介绍

本指南将使用 snappy 压缩/解压缩库作为编写外部代码绑定的入门。Rust 目前无法直接调用 C++ 库,但 snappy 包含一个 C 接口(在 snappy-c.h 中有文档说明)。

关于 libc 的说明

许多示例都使用了 libc,该库提供了 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 的析构函数来提供安全保障并保证释放这些资源(尤其是在出现恐慌的情况下)。

有关析构函数的更多信息,请参阅 Drop 特征

从 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 属性有两种可接受的形式

  • #[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 中时,部分目标(如 rlib)不会链接到该库,但当 rlib 包含在最终目标(如二进制文件)中时,原生库将被链接进来。

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

不安全块

某些操作,例如解引用原始指针或调用已标记为不安全的函数,只允许在不安全块内进行。不安全块隔离了不安全性,并向编译器承诺不安全性不会泄漏到块外。

另一方面,不安全函数会向外界公布其不安全性。不安全函数的编写方式如下

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

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

访问外部全局变量

外部 API 通常会导出一个全局变量,该变量可以执行跟踪全局状态等操作。为了访问这些变量,您需要在 extern 块中使用 static 关键字声明它们

#[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 gate 后面,可能会发生变化。
  • 锈色
  • 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)] 也可以应用于枚举。

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))。

这是一个 contrived 示例。假设某个 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)的交互也是如此。

使用 "C-unwind" 进行 Rust panic

#[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-unwind" 进行 C++ throw

#[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_passthroughcatchmay_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 进行编译。

预emptively 捕获 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);

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

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

这是一种 perfectly 处理这种情况的有效方法。但是,我们可以做得更好。为了解决这个问题,一些 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: [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() {}

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

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

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

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