外部函数接口(FFI)

简介

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

关于 libc 的说明

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

[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 需要被包装起来,以提供内存安全,并使用更高级别的概念,如向量。一个库可以选择只公开安全的高级接口而隐藏不安全的内部细节。

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

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函数可以用来分配一个最大容量的 Vec,以容纳压缩后的输出,然后该向量可以作为输出参数传递给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 代码

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

Rust 代码侧

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

#[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类型,不过这会需要我们修改一些链接的参数。)

接下来,执行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(); // 触发回调
    }
}

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); // 在 Rust 中会调用回调函数 callback(7)
}

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

针对 Rust 对象的回调

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

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

Rust 代码:

struct RustObject {
    a: i32,
    // 其余的成员...
}

extern "C" fn callback(target: *mut RustObject, a: i32) {
    println!("I'm called from C with value {0}", a);
    unsafe {
        // 在回调函数中更新 RustObject 的内容
        (*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() {
    // 创建一个会被在回调函数中引用的 RustObject
    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); // 这会调用 Rust 代码中的 callback(&rustObject, 7)
}

异步回调

在之前给出的例子中,回调是作为对外部 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 上,框架的行为与动态库的语义相同。

不安全块

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

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

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

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

访问外部的全局变量

外部的 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();
    }
}

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

外部调用规约

大多数外部代码都暴露了一个 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
  • 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 中可以通过在外部函数声明的参数列表中指定“...”来实现:

extern {
    fn foo(x: i32, ...);
}

fn main() {
    unsafe {
        foo(10, 20, 30, 40, 50);
    }
}

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

#![allow(unused)]
fn main() {
// 这不会编译通过

fn foo(x: i32, ...) {}
}

"空指针优化"

某些 Rust 类型被定义为永不为“空”。这包括引用(&T, &mut T), Box(Box<T>), 和函数指针(extern "abi" fn())。当与 C 语言对接时,经常使用可能为“空”的指针,这似乎需要一些混乱的transmute和/或不安全的代码来处理与 Rust 类型的转换。然而,尝试构造或者使用这些无效的值是 undefined behavior,所以你应当使用如下的变通方法。

作为一种特殊情况,如果一个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" {
    /// 注册回调函数
    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>)
{}

// 这个函数其实没什么实际的用处,
// 它从C代码接受一个函数指针和一个整数,
// 用整数做参数,调用指针指向的函数,并返回函数的返回值,
// 如果没有指定函数,那默认就返回整数的平方
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 和 unwinding

在使用 FFI 时,必须注意 unwinding。大多数 ABI 的名称有两种变体,一种带有 -unwind 后缀而另一种不带。Rust 的 ABI 总是允许 unwinding,所以不存在 Rust-unwind ABI。

如果你希望 Rust panics 或是外部(例如:C++)的异常能够穿越 FFI 的边界,则必须使用正确的 -unwind ABI。相反,如果你不希望 unwinding 可以穿越 FFI 边界,使用非 unwind 的 ABI。

注意:编译时指定 panic=abort 会导致 panic! 立即终止进程,无论发生 panic 的函数指定了何种 ABI。

如果一个 unwinding 操作遇到了不允许 unwind 的 ABI 边界,具体行为会由 unwinding 的源头决定(Rust panic 或是外部异常):

  • panic 会导致进程安全终止。
  • 外部异常会导致未定义行为。

注意 catch_unwind 和外部异常的交互行为是未定义的,同样,panic 和外部异常处理机制的交互也是一样(尤其是 C++ 的 try/catch)。

Rust panic"C-unwind"

#[no_mangle]
extern "C-unwind" fn example() {
    panic!("Uh oh");
}

该函数(当编译时指定 panic=unwind 时)可以 unwind C++ 的栈帧。

[通过 `catch_unwind` 停止 unwinding 的 Rust 函数 ]
      |
     ...
      |
   [C++ 栈]
      |                           ^
      | (调用)                     | (向上 unwinding)
      v                           |
[Rust 函数 `example`]              |
      |                           |
      +----- rust 函数 panics -----+

如果 C++ 的栈上包含对象,它们将会被析构。

C++ throw"C-unwind"

#[link(...)]
extern "C-unwind" {
    // 一个可能会抛出异常的 C++ 函数
    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 抛出的异常。

[在 `try` 语句块中调用 `rust_passthrough`的 C++ 函数]
      |
     ...
      |
[Rust 函数 `rust_passthrough`]
      |                            ^
      | (调用)                      | (向上 unwinding)
      v                            |
[C++ 函数 `may_throw`]              |
      |                            |
      +------ C++ 函数抛出异常 ------+

如果 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只捕捉 unwind 的 panic,而不是那些中止进程的 panic。更多信息请参见catch_unwind的文档。

表示不透明(opaque)的结构

有时,一个 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() {}

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

struct Foo; /* Foo 是一个接口,但它的内容不属于公共接口 */
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)]。该标记确保编译器不会将该结构标记为SendSync,并且Unpin也不会应用于该结构(*mut u8不是Send或者SyncPhantomPinned也不是Unpin)。

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

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

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