Rust FFI 详解:安全地桥接 Rust 与其他语言
在现代软件开发中,单一语言往往难以满足所有需求。我们可能需要利用 C/C++ 编写的高性能库、与 Python/Ruby/Node.js 脚本进行交互、或者将 Rust 的安全性和性能优势引入现有的 Java/C# 系统。这时,外部函数接口 (Foreign Function Interface, FFI) 就成为了连接不同编程语言世界的关键桥梁。
Rust,作为一门以内存安全和并发安全著称的系统编程语言,提供了强大而灵活的 FFI 功能。然而,跨越语言边界本质上是 unsafe
的操作,因为它涉及到底层表示、内存管理、错误处理和 ABI(应用程序二进制接口)兼容性等一系列复杂问题。Rust 的 unsafe
关键字明确地标记了这些需要开发者承担额外安全责任的区域。
本文旨在深入探讨 Rust FFI 的机制、挑战和最佳实践,重点关注如何安全地利用 FFI 将 Rust 代码与其他语言(主要是 C,但也触及其他语言的 FFI 基础)集成。
FFI 的基础:为何以及如何工作?
FFI 允许一种语言编写的代码调用另一种语言编写的代码(函数、方法),并与之交换数据。这通常基于一个共同的、稳定的底层接口——C ABI。大多数语言都提供与 C 语言交互的能力,因此 C ABI 成为了事实上的通用语言交互标准。
为何需要 FFI?
- 代码复用:利用现有的、经过良好测试和优化的 C/C++ 库(如图形库、科学计算库、操作系统 API)。
- 性能优化:将 computationally intensive 的部分用 Rust 或 C/C++ 实现,供 Python、Ruby 等解释型语言调用。
- 渐进式迁移:将大型遗留系统(如 C++ 或 Java)的一部分逐步用 Rust 重写,以提高安全性和可维护性。
- 生态系统整合:让 Rust 库能够被其他语言生态系统所使用。
FFI 如何工作?
FFI 的核心在于双方语言都遵循一个共同的约定(通常是 C ABI),这个约定规定了:
- 函数调用约定 (Calling Convention):参数如何传递(寄存器、栈),返回值如何返回,调用者和被调用者谁负责清理栈。
- 数据类型表示 (Data Representation):基本类型(整数、浮点数、指针)在内存中的大小和布局。复杂类型(结构体、枚举)需要特别注意对齐和填充。
- 名称修饰 (Name Mangling):编译器可能会修改函数名以支持重载等特性。C ABI 通常要求使用原始的、未修饰的函数名。
- 内存管理:跨语言边界传递数据时,谁负责分配和释放内存?这是 FFI 中最容易出错的部分。
- 错误处理:不同的语言有不同的错误处理机制(返回值、异常、错误码)。需要统一的策略。
Rust FFI 的核心机制
Rust 提供了特定的属性和类型来处理 FFI。
-
extern
关键字:extern "C" { ... }
:用于声明外部(通常是 C 语言)库中定义的函数和全局变量。Rust 编译器不会查找这些项的定义,而是假设它们在链接阶段可用。这告诉 Rust 编译器使用 C ABI 来调用这些函数。extern "C" fn ...
:用于定义一个 Rust 函数,该函数将被其他语言(遵循 C ABI)调用。它确保 Rust 编译器生成的函数遵循 C 调用约定,并且通常与#[no_mangle]
结合使用。
-
#[no_mangle]
属性:
默认情况下,Rust 编译器会对函数名进行修饰(mangling)以支持泛型、重载等特性,并避免命名冲突。#[no_mangle]
属性告诉编译器不要修饰这个函数名,使其保持原始名称,以便 C 或其他语言能够通过这个确切的名称找到并调用它。 -
unsafe
关键字:
调用外部函数是unsafe
的,因为 Rust 编译器无法验证外部代码的安全性(内存安全、线程安全等)。任何extern
块中声明的函数调用都必须在unsafe
块或unsafe fn
中进行。同样,实现一个extern "C" fn
通常也需要处理裸指针等unsafe
操作。 -
C 类型表示:
libc
crate:提供了 C 标准库中定义的类型别名,如c_int
,c_char
,c_void
,size_t
等。强烈建议使用这些类型来明确 FFI 边界上的数据类型,而不是依赖 Rust 内置类型(如i32
,u8
)的大小可能因平台而异。std::os::raw
模块:也提供了类似的 C 类型别名。通常libc
更全面。
-
#[repr(C)]
属性:
用于 Rust 的struct
和enum
。它指示 Rust 编译器按照 C 语言的规则来布局结构体的字段(保证顺序,但仍可能有填充)或枚举的值。这对于在 FFI 边界传递结构体或 C 风格枚举至关重要。没有#[repr(C)]
,Rust 编译器可能会为了优化而重排字段顺序。
场景一:Rust 调用 C/C++ 代码
这是比较常见的场景,例如调用操作系统 API 或使用现有的 C 库。
步骤:
- 确定 C/C++ 函数签名:了解需要调用的 C 函数的名称、参数类型和返回类型。
- 在 Rust 中声明外部函数:使用
extern "C" { ... }
块声明这些函数,参数和返回类型使用libc
或std::os::raw
中的 C 兼容类型。 - 链接 C/C++ 库:
- 使用
build.rs
构建脚本:这是 Rust 项目中处理编译时任务(如链接外部库)的标准方式。build.rs
可以指示rustc
链接到特定的静态库 (.a
/.lib
) 或动态库 (.so
/.dylib
/.dll
)。 - 在
build.rs
中,通常使用println!("cargo:rustc-link-lib=...")
来指定库名,使用println!("cargo:rustc-link-search=native=...")
来指定库搜索路径。
- 使用
- 在
unsafe
块中调用:在 Rust 代码中,将对外部函数的调用包裹在unsafe
块中。
示例:调用 C 的 puts
函数
“`rust
// 使用 libc crate 获取 C 类型
extern crate libc;
use std::ffi::CString; // 用于创建 C 兼容的空终止字符串
// 声明外部 C 函数 puts
extern “C” {
fn puts(s: *const libc::c_char) -> libc::c_int;
}
fn main() {
// 创建一个 Rust String
let rust_string = “Hello from Rust using C’s puts!”;
// 将 Rust String 转换为 C 兼容的 CString (带空终止符)
// CString::new 可能失败(如果字符串包含内部空字节),因此需要处理 Result
match CString::new(rust_string) {
Ok(c_string) => {
// 调用外部函数必须在 unsafe 块中
// c_string.as_ptr() 获取指向 C 字符串数据的裸指针
unsafe {
puts(c_string.as_ptr());
}
// c_string 在这里超出作用域时会自动释放内存
}
Err(e) => {
eprintln!("Error creating CString: {}", e);
}
}
}
“`
安全注意事项 (Rust 调用 C/C++):
- 空指针 (Null Pointers):C API 经常使用空指针表示错误或可选值。Rust 代码必须检查从 C 返回的指针是否为空,并确保传递给 C 的指针是有效的(如果 C API 不允许 NULL)。
- 字符串处理:
- Rust
String
不是空终止的。需要使用CString::new()
创建 C 兼容的空终止字符串。CString
会管理内存。 - 从 C 接收字符串时,会得到
*const c_char
。需要使用CStr::from_ptr()
(在unsafe
块中) 将其包装,然后可以安全地转换为 Rust&str
(如果保证 UTF-8 有效性) 或String
。必须确保 C 端提供的指针有效且指向空终止的字符串。
- Rust
- 内存管理:如果 C 函数返回一个需要手动释放的指针(例如通过
malloc
分配),Rust 代码必须负责调用相应的 C 释放函数(例如free
)。这通常需要在extern "C"
块中声明 C 的free
函数。忘记释放会导致内存泄漏。 - 数据类型匹配:确保 Rust 中声明的类型 (
libc::c_int
,libc::c_double
等) 与 C 库期望的类型完全匹配。大小或符号不匹配可能导致数据损坏或崩溃。 - 线程安全:如果调用的 C 库不是线程安全的,需要确保 Rust 代码在调用时采取了适当的同步措施(如使用
Mutex
)。
场景二:C/C++ 调用 Rust 代码
这种场景允许你用 Rust 编写高性能或安全的模块,并将其集成到现有的 C/C++ 项目中。
步骤:
- 定义 C 兼容的 Rust 函数:
- 使用
extern "C"
标记函数,使其遵循 C 调用约定。 - 使用
#[no_mangle]
阻止名称修饰。 - 参数和返回类型应使用
libc
类型。 - 避免直接使用 Rust 特有的类型(如
String
,Vec
, 泛型,trait 对象)作为 FFI 边界的参数或返回值。需要将它们转换为 C 兼容的表示(通常是裸指针和长度)。
- 使用
- 处理 Rust 特有概念:
- 所有权和生命周期:Rust 的核心特性在 FFI 边界消失。必须手动管理内存。
- 传递所有权给 C:可以使用
Box::into_raw()
将 Rust 堆分配的对象(如Box<T>
)的所有权转移给 C 端,返回一个裸指针*mut T
。C 端现在负责在适当的时候将此指针传回给 Rust 进行释放。 - 从 C 接收所有权:C 端可以将之前由 Rust 分配的指针传回。Rust 代码可以使用
Box::from_raw()
(在unsafe
块中) 重新获得所有权,Rust 的内存管理系统将接管,并在Box
离开作用域时自动释放内存。 - 借用:可以将 Rust 数据的只读 (
*const T
) 或可变 (*mut T
) 裸指针传递给 C。必须确保在 C 代码使用指针期间,Rust 中的原始数据保持有效(不会被移动或释放)。这是非常危险的,因为 Rust 编译器无法追踪裸指针的生命周期。
- 传递所有权给 C:可以使用
- 错误处理:Rust 的
Result
和panic!
机制不能直接跨越 FFI 边界。- 返回错误码:最常见的方式是让 Rust 函数返回一个
libc::c_int
或类似的整数类型,用 0 表示成功,用负数或特定的正数值表示不同的错误。 - 输出参数 (Out Parameters):可以通过指针参数让调用者 (C 端) 提供一块内存,Rust 函数将结果或错误信息写入该内存。
- 线程局部存储 (Thread-Local Storage):可以设置一个线程局部的变量来存储最后一个错误信息。C 端在调用返回错误码后,可以调用另一个 Rust FFI 函数来获取详细的错误描述。
- 返回错误码:最常见的方式是让 Rust 函数返回一个
- Panic 处理:绝对不能让 panic! 跨越 FFI 边界! 这通常会导致未定义行为(很可能是程序崩溃)。因为 C/C++ 不知道如何展开 Rust 的栈。
- 使用
std::panic::catch_unwind
:在extern "C"
函数的顶层,用catch_unwind
包裹可能 panic 的代码。如果发生 panic,catch_unwind
会捕获它并返回一个Result
。你可以将 panic 转换为一个错误码返回给 C 端。这是极其重要的安全实践。
- 使用
- 所有权和生命周期:Rust 的核心特性在 FFI 边界消失。必须手动管理内存。
- 编译 Rust 代码为库:
- 在
Cargo.toml
中设置crate-type
,将其编译为 C 可以链接的库:crate-type = ["cdylib"]
:编译为 C 动态库 (.so
,.dylib
,.dll
)。这是最常用的方式,因为它易于分发和加载。crate-type = ["staticlib"]
:编译为 C 静态库 (.a
,.lib
)。这会将 Rust 代码和依赖项编译到一个单独的归档文件中,链接器会将其包含到最终的可执行文件中。
- 在
- 生成 C/C++ 头文件 (.h):
- 手动编写:对于简单的接口。
- 使用工具自动生成:
cbindgen
是一个非常流行的工具,它可以读取 Rust 源代码(特别是标记了#[no_mangle]
和#[repr(C)]
的部分)并自动生成 C/C++ 头文件。强烈推荐使用cbindgen
来保持 Rust 代码和 C/C++ 头文件的同步。
- 在 C/C++ 项目中链接和调用:
- 包含生成的头文件。
- 链接 Rust 生成的库文件(
.so
/.dll
/.lib
)。 - 像调用普通 C 函数一样调用 Rust 导出的函数。
示例:一个简单的 Rust 库供 C 调用
Rust (src/lib.rs
)
“`rust
extern crate libc;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// 定义一个 Rust 结构体,并使其 C 兼容
[repr(C)]
pub struct Point {
x: f64,
y: f64,
}
// 创建 Point 对象的函数 (返回裸指针,所有权转移给调用者)
[no_mangle]
pub extern “C” fn point_new(x: f64, y: f64) -> *mut Point {
let p = Box::new(Point { x, y });
Box::into_raw(p) // 转移所有权,返回裸指针
}
// 释放 Point 对象的函数 (接收裸指针,获取所有权并释放)
// 必须确保传入的 ptr 是之前由 point_new 创建且未被释放的
[no_mangle]
pub extern “C” fn point_free(ptr: *mut Point) {
if ptr.is_null() {
return; // 处理空指针
}
unsafe {
// 从裸指针重新构建 Box,获取所有权
// Box 在这里超出作用域时会自动调用 drop 来释放内存
let _ = Box::from_raw(ptr);
}
}
// 一个可能失败的操作,使用错误码和 panic 防护
[no_mangle]
pub extern “C” fn process_data(data_ptr: const c_char, out_len: mut usize) -> libc::c_int {
// 使用 catch_unwind 包裹核心逻辑,防止 panic 跨 FFI
let result = catch_unwind(AssertUnwindSafe(|| {
// 检查输入指针
if data_ptr.is_null() || out_len.is_null() {
return -1; // 返回错误码:无效参数
}
// 将 C 字符串指针转换为 Rust &str (unsafe 操作)
let c_str = unsafe { CStr::from_ptr(data_ptr) };
let rust_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return -2, // 返回错误码:非 UTF-8 字符串
};
println!("Rust received: {}", rust_str);
let processed_len = rust_str.len() * 2; // 示例处理
// 模拟一个可能 panic 的情况 (例如,除以零)
// if some_condition { panic!("Something went wrong!"); }
// 通过输出参数返回结果
unsafe {
*out_len = processed_len;
}
0 // 返回成功码
}));
// 处理 catch_unwind 的结果
match result {
Ok(return_code) => return_code, // 正常返回码
Err(_) => {
eprintln!("Panic caught in Rust FFI function!");
// 可选:设置线程局部错误状态
// 返回一个特定的错误码表示发生了 panic
-99 // 返回错误码:内部 panic
}
}
}
// 演示返回 C 字符串 (调用者负责释放)
[no_mangle]
pub extern “C” fn get_greeting() -> *mut c_char {
let greeting = “Hello from Rust!”;
match CString::new(greeting) {
Ok(c_string) => c_string.into_raw(), // 转移所有权给调用者
Err(_) => std::ptr::null_mut(), // 无法创建 CString,返回空指针
}
}
// 释放由 get_greeting 返回的字符串
[no_mangle]
pub extern “C” fn free_rust_string(s: *mut c_char) {
if s.is_null() { return; }
unsafe {
// 从裸指针重构 CString,然后它会立即被 drop
let _ = CString::from_raw(s);
}
}
“`
Cargo.toml
“`toml
[package]
name = “my_rust_lib”
version = “0.1.0”
edition = “2021”
[lib]
crate-type = [“cdylib”] # 或 [“staticlib”]
[dependencies]
libc = “0.2”
[build-dependencies]
cbindgen = “0.24” # 用于生成头文件
“`
build.rs
(可选,用于自动生成头文件)
“`rust
extern crate cbindgen;
use std::env;
use std::path::PathBuf;
fn main() {
let crate_dir = env::var(“CARGO_MANIFEST_DIR”).unwrap();
let package_name = env::var(“CARGO_PKG_NAME”).unwrap();
let output_file = target_dir()
.join(format!(“{}.h”, package_name))
.display()
.to_string();
let config = cbindgen::Config::from_file("cbindgen.toml")
.expect("Unable to find cbindgen.toml");
cbindgen::generate_with_config(&crate_dir, config)
.expect("Unable to generate bindings")
.write_to_file(&output_file);
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=cbindgen.toml");
}
/// Find the location of the target/
directory. Note that this may be
/// overridden by cmake
, so we also need to check the CARGO_TARGET_DIR
/// variable.
fn target_dir() -> PathBuf {
if let Ok(target) = env::var(“CARGO_TARGET_DIR”) {
PathBuf::from(target)
} else {
PathBuf::from(env::var(“CARGO_MANIFEST_DIR”).unwrap()).join(“target”)
}
}
“`
cbindgen.toml
(cbindgen 配置文件)
“`toml
language = “C” # 或 “C++”
include_guard = “MY_RUST_LIB_H”
header = “/ Generated by cbindgen /”
可以添加更多配置,如命名空间 (C++), include 等
“`
C 代码 (main.c
)
“`c
include
include // for free() if needed, though Rust handles its own memory here
include
include “my_rust_lib.h” // 包含生成的头文件
int main() {
// 使用 Rust 创建 Point 对象
Point p1 = point_new(10.5, 20.3);
if (!p1) {
fprintf(stderr, “Failed to create point.\n”);
return 1;
}
printf(“Created point in Rust: (%f, %f) at address %p\n”, p1->x, p1->y, (void)p1);
// 将所有权交还给 Rust 进行释放
point_free(p1);
p1 = NULL; // 好习惯:避免悬垂指针
printf("Point freed by Rust.\n");
// 调用可能失败的 Rust 函数
const char *input_data = "Some data to process";
size_t output_length = 0;
int result_code = process_data(input_data, &output_length);
if (result_code == 0) {
printf("Rust process_data succeeded. Output length: %zu\n", output_length);
} else {
fprintf(stderr, "Rust process_data failed with code: %d\n", result_code);
// 在实际应用中,可能需要调用另一个 Rust 函数获取详细错误信息
}
// 测试返回字符串
char *greeting = get_greeting();
if (greeting) {
printf("Greeting from Rust: %s\n", greeting);
// 必须调用 Rust 提供的函数来释放这个字符串
free_rust_string(greeting);
greeting = NULL;
} else {
fprintf(stderr, "Failed to get greeting from Rust.\n");
}
return 0;
}
“`
编译和运行 (Linux 示例)
“`bash
编译 Rust 库 (生成 libmy_rust_lib.so 和 my_rust_lib.h 到 target/debug/ 或 target/release/)
cargo build
编译 C 代码,链接 Rust 动态库
(假设头文件和库文件在 target/debug/ 目录下)
gcc main.c -o main_app -L./target/debug -lmy_rust_lib -I./target/debug
运行 (需要确保 .so 文件在链接器能找到的路径,例如当前目录)
export LD_LIBRARY_PATH=./target/debug:$LD_LIBRARY_PATH # Linux/macOS
或者在 Windows 上设置 PATH
./main_app
“`
安全注意事项 (C/C++ 调用 Rust):
- 内存管理(所有权):这是最关键的部分。必须明确哪个函数分配内存,哪个函数释放内存。
- 使用
Box::into_raw
和Box::from_raw
是管理 Rust 对象生命周期的常用模式。 - 为每个需要跨 FFI 边界管理的 Rust 类型提供
_new
和_free
函数是良好实践。 - 永远不要在 C 端
free()
一个由 Rust(通过Box
或其他分配器)分配的指针,反之亦然。内存分配器必须匹配!
- 使用
- Panic 安全:再次强调,使用
catch_unwind
保护所有可能 panic 的extern "C"
函数入口点。将 panic 转化为错误码或其他 C 兼容的错误信号。 - 空指针检查:Rust 代码应该对从 C 传入的指针进行严格的空指针检查。
- 字符串和字节切片:
- 当 C 传入
*const c_char
时,使用CStr::from_ptr
包装(unsafe)。确保 C 端保证了指针有效性和空终止。如果字符串可能是无效 UTF-8,使用to_bytes()
或to_string_lossy()
。 - 当 Rust 需要返回字符串给 C 时,使用
CString::into_raw
。调用者 (C) 获得所有权,并且必须调用 Rust 提供的free
函数来释放它。 - 对于非 UTF-8 的字节数据,使用
*const u8
/*mut u8
和usize
长度参数。
- 当 C 传入
- 数据表示 (
#[repr(C)]
):确保所有跨 FFI 边界传递的struct
和enum
都使用了#[repr(C)]
,并验证字段类型和对齐方式。 - 线程安全:如果 Rust 函数会访问共享的可变状态,必须使用 Rust 的同步原语(
Mutex
,RwLock
等)来确保线程安全。C 端调用者可能不知道这些内部细节。 - 回调 (Callbacks):如果 C 需要调用 Rust 函数作为回调,情况会更复杂。通常需要将 Rust 闭包(可能带有捕获的数据)包装在一个
Box
中,将其转换为*mut c_void
传递给 C,同时传递一个静态的extern "C"
跳板函数 (trampoline function) 的指针。跳板函数接收*mut c_void
,将其转换回Box<Closure>
,然后调用闭包。需要非常小心地管理闭包的生命周期和unsafe
类型转换。
桥接更高级的语言
虽然 C ABI 是 FFI 的基础,但通常我们不想直接在 Python、Java、Ruby 等语言中编写 C FFI 代码。幸运的是,存在许多库和工具可以简化这个过程:
- Python:
PyO3
:目前最流行和功能最丰富的库,允许你用 Rust 编写 Python 扩展模块,支持 Python 类型和 Rust 类型之间的自动转换,处理 GIL 等。rust-cpython
:较早的库,仍然可用。
- Node.js (JavaScript/TypeScript):
Neon
:提供了安全的抽象,用于编写原生 Node.js 模块,处理 V8 引擎的细节。NAPI-RS
:基于 Node.js 的 N-API,提供更稳定和跨 Node.js 版本的 ABI。
- Java/JVM:
- JNI (Java Native Interface):标准的 Java FFI 机制。需要编写 C “胶水代码” 或使用 Rust 的
jni
crate 来简化 JNI 的使用,处理 Java 和 Rust 之间的类型转换和对象生命周期。
- JNI (Java Native Interface):标准的 Java FFI 机制。需要编写 C “胶水代码” 或使用 Rust 的
- Ruby:
Helix
:允许用 Rust 编写 Ruby 类和模块。Rutie
:另一个用于集成 Rust 和 Ruby 的库。
- C#/.NET:
- P/Invoke (Platform Invoke):.NET 的标准机制,用于调用非托管代码(如 C 库)。可以将 Rust 代码编译为 C 动态库,然后使用 P/Invoke 从 C# 调用。需要仔细管理类型映射和内存。
这些库通常在底层仍然使用 C FFI,但它们提供了更高层次、更符合目标语言习惯的抽象,处理了很多繁琐和易错的细节(如类型转换、内存管理、错误处理映射)。
FFI 安全性最佳实践总结
- 最小化
unsafe
范围:将unsafe
块限制在绝对必要的地方(如调用extern
函数、解引用裸指针、调用unsafe fn
)。在unsafe
块内部进行尽可能多的检查(如空指针检查)。 - 明确内存管理契约:文档化并严格遵守谁分配、谁拥有、谁释放跨 FFI 边界传递的内存。为 Rust 对象提供 C 兼容的构造和析构函数。
- 使用
#[repr(C)]
:确保所有跨边界共享的struct
和enum
布局稳定且与 C 兼容。 - 使用
libc
类型:在 FFI 签名中使用libc::c_*
类型,而不是依赖 Rust 内置类型的大小。 - 处理字符串和切片:使用
CString
/CStr
处理 C 字符串,显式传递指针和长度用于字节切片。注意 UTF-8 有效性。 - 绝不让 Panic 跨 FFI 边界:使用
catch_unwind
包裹所有extern "C"
函数体。将 panic 转换为错误码或其他 C 可理解的错误指示。 - 严格的指针检查:始终检查从外部传入的指针是否为空,并确保传递给外部的指针是有效的。
- 使用
cbindgen
:自动生成 C/C++ 头文件,减少手动同步错误。 - 考虑使用更高层的绑定库:对于 Python, Node.js, Java 等语言,优先考虑使用 PyO3, Neon, jni crate 等库,它们能处理许多 FFI 的复杂性。
- 编写测试:编写集成测试,覆盖 FFI 边界的各种情况,包括正常路径、错误路径和边界条件。
结论
Rust FFI 是一个强大的工具,它使得 Rust 的安全性、性能和现代语言特性能够融入更广泛的软件生态系统。然而,FFI 本质上打破了 Rust 编译器的安全保证,要求开发者承担起维护跨语言边界安全的责任。
通过理解 C ABI 的基本原理,熟练运用 Rust 提供的 extern
, #[no_mangle]
, #[repr(C)]
, unsafe
等机制,并严格遵循内存管理、错误处理(特别是 panic 防护)和类型表示的最佳实践,我们可以构建出健壮、高效且相对安全的 FFI 接口。
虽然 FFI 带来了复杂性,但 Rust 的强类型系统、所有权模型(即使在 FFI 边界需要手动管理时,其思维方式也有助于设计清晰的契约)以及 catch_unwind
等工具,为构建可靠的跨语言解决方案提供了坚实的基础。谨慎地设计和实现 FFI,将能充分发挥 Rust 与其他语言协同工作的巨大潜力。