Diplomat:面向 Rust 库的多语言 FFI
摘要
Diplomat 是一个多语言单向 FFI 工具,用于封装 Rust 库,旨在将 Rust API 暴露给 C++、JS、Dart 和 JVM 等语言,而无需 FFI 专业知识,填补了 Rust 工具生态系统中的空白。
<p><a href="https://lobste.rs/s/mgbtd6/diplomat_multi_language_ffi_for_rust">评论</a></p>
查看缓存全文
缓存时间: 2026/06/15 09:05
# Diplomat: 为 Rust 库提供多语言 FFI 支持
来源:https://manishearth.github.io/blog/2026/06/14/diplomat-multi-language-ffi-for-rust-libraries/
*这是一篇我多年来一直想写并发表的文章,直到最近才付诸实施。我希望能重新开始多写一些!*
在过去的几年里,作为我在 [ICU4X](https://github.com/unicode-org/icu4x) 项目工作的一部分,我一直在开发 [Diplomat](https://github.com/rust-diplomat/diplomat),这是一个用于封装 Rust 库的多语言单向 FFI 工具。我最初在 2021 年 [设计](https://github.com/rust-diplomat/diplomat/blob/main/docs/design_doc.md) 了 Diplomat,以回答“将 ICU4X(一个 Rust 库)暴露给其他编程语言的最佳方式是什么?”这个问题。需要说明一下,虽然 ICU4X 是用 Rust 编写的,但其核心设计目标之一就是让任何编程语言都能使用它,从核心语言集合开始,并随着时间的推移逐步扩展。这与现有的 Unicode 库 [ICU4C](https://unicode-org.github.io/icu/userguide/icu4c/) 和 [ICU4J](https://unicode-org.github.io/icu/userguide/icu4j/) 形成对比,它们分别服务于 C/C++ 和 Java。
从长远来看,对于这样的项目,工具化是必不可少的。如果 ICU4X 只暴露给单一语言,那么手动处理是可行的:有人为每一个用 Rust 编写的新 API 手动编写 FFI,并且你需要让团队中至少一部分人熟悉某种特定语言的 FFI 编写。然而,随着你想要支持的语言数量增加,这种方式变得越来越不可持续。期望大多数工程团队成员都成为 C++、JS、Dart、JVM 等语言的 FFI 专家是不合理的。
在项目启动时,我对当时可用的工具进行了 [调研](https://docs.google.com/document/d/1Y1mNFAGbGNvK_I64dd0fRWOxx9xqi12dXeLivnxRWvA/edit?usp=sharing&resourcekey=0-l9QvvqXW7cC-TrfLWt7nZw),得出的结论是,现有的工具都无法满足我们的用例:一个用 Rust 编写的库希望向多种语言暴露 API。其中一些工具只能解决部分问题,需要与其他工具拼凑使用。我还为我的“空中楼阁 FFI 工具”写了一份设计,当时觉得实现起来太麻烦(得先解决无数琐碎问题),但它能填补我长期以来在 Rust FFI 工具生态中感受到的空白。在那之前,我们一直坚持手动编写 C 绑定,因为我们还在摸索各种问题。
现有 FFI 工具无法工作的核心原因之一是:它们不是“单向”的,而是“双向”的,或者是“单向”但方向相反[^1]。
> 困惑粒子发言:在 FFI 工具的语境下,什么是“单向”和“双向”?
>
> 嗯,这可能是我某天自己发明的术语[^2],但我发现这个分类在很多很多场合都很有用,所以我觉得值得介绍一下。
通常,在进行 FFI 时,大致有两种不同的目标,各有其特点。一种用例由 [bindgen](https://github.com/rust-lang/rust-bindgen)、[cbindgen](https://github.com/mozilla/cbindgen)、[wasm-bindgen](https://github.com/rustwasm/wasm-bindgen)、[uniffi](https://github.com/mozilla/uniffi-rs) 和 [PyO3](https://pyo3.rs/) 等工具服务,即你有一个用某种语言编写的库,并希望从另一种语言中使用它。这就是“单向”FFI,因为被封装的库不需要知道调用它的代码库的任何信息。请注意,在“单向”FFI 中,*调用*仍然可以是双向的;一个单向 FFI 工具可能支持回调,允许调用代码库向库传递一个闭包,并由库来调用它。这仍然是单向的,因为 API 定义在封装的库内部。
另一种用例由 [cxx](https://github.com/dtolnay/cxx)、[autocxx](https://github.com/google/autocxx)、[crubit](https://github.com/google/crubit) 和 [swift-bridge](https://github.com/chinedufn/swift-bridge) 等工具服务,即你正在处理一个由两种语言组成的混合代码库,需要“双向”互操作,例如,Rust 需要能够访问 C++ 的 API,C++ 也需要能够访问 Rust 的 API。这就是我在 [Stylo](https://bholley.net/blog/2017/stylo.html) 项目中所熟悉的那种互操作情况,该项目旨在将 [Servo](https://github.com/servo/servo/) 的样式系统用于 Firefox。即使 Servo 相对模块化,这也不是“像调用库一样调用 Servo”的情况,而是将两个代码库在某种参差不齐的 API 边界上进行集成。当时没有多少工具,我们设法让 [bindgen 能够用于这个目的](https://manishearth.github.io/blog/2021/02/22/integrating-rust-and-c-plus-plus-in-firefox/),但这显然是一个“双向”用例。
双向工具通常也可以用于单向用例,但它们通常也是针对这两种特定语言设计的,这限制了生成的绑定与其他语言一起使用的效用。你无法将这些绑定作为许多语言围绕的“枢纽”。
在设计 Diplomat 时,我考虑了以下几点,这些点可能与其他 FFI 工具的选择有所不同:
### 无远距离作用
对常规 Rust 库代码的修改永远不应静默地改变你的 FFI 层。我不希望 Diplomat 解析完整的依赖图:应该能非常清楚地知道对代码的编辑是否会改变 FFI 层,通过将 Diplomat 处理的内容限制为经过特殊标记的“桥接”[^3] 代码。在 ICU4X 中,FFI 层只有在人们更新位于 `ffi/capi` 下的 Diplomat “桥接” 代码时才会改变。
> 困惑粒子发言:为什么工具具备这个属性会很有用?
>
> 首先,当一个工具不需要解析 Rust 支持的全部范围时,设计起来就*更容易*。由于 Diplomat 的“桥接”代码仅供 Diplomat 使用,我们可以禁止在那边使用奇怪的 Rust 语法。
>
> 消极粒子发言:就是说*你*,`for<'a\>`。
其次,FFI 工具不应过度限制暴露给常规 Rust 用户的 API;应该能够根据 Rust 用户的需求定制 API,而不必考虑其他语言。
最后,如果库的每一部分都被一个可能需要回避或安抚的工具监视,这对库开发者来说是非常烦人的。ICU4X 开发者绝对需要知道如何使用 Diplomat,以便为他们设计的每个 ICU4X API 编写 FFI,但在仅设计主要 Rust 代码时,他们*不应该*一直想着这个事情。
### 生成可直接使用的库
Diplomat 应生成可直接使用的库,而非低级绑定。因此,它应生成在目标语言中符合惯用法的 API,并提供一定程度的按语言可配置性,允许开发者选择如何精确地暴露各种功能。
### 无需 IDL
理想情况下,接口在 Rust 代码中流畅地指定,而不是使用某种接口描述语言。这是一个美学上的选择;IDL 也能很好地工作,并且在 [uniffi](https://github.com/mozilla/uniffi-rs) 中提供了这种选项。
### 可扩展以支持更多语言
扩展 Diplomat 以生成更多语言的绑定不应该过于困难。愿景是,如果有人要求 ICU4X 提供 Dart API,我们可以为 Dart 编写一个 Diplomat “后端”,并在现有的 ICU4X Diplomat 桥接代码上运行它。
> 积极粒子发言:事实上,这正是发生的事情,ICU4X 现在有了 [Dart API](https://github.com/unicode-org/icu4x/tree/main/ffi/capi/bindings/dart)。
这意味着 Diplomat 的约束和设计从一开始就要考虑到它最终可能支持的各种语言:如果某个特性对特定语言没有意义,可能需要重新设计或使其成为条件性的。
> 积极粒子发言:这也意味着第三方可以根据需要构建自己的 Diplomat 后端,既可以通过将 Diplomat 作为库使用,也可以向上游贡献。这种情况已经发生多次:Kotlin 和 Python 后端并非由 ICU4X 团队编写,尽管 ICU4X 现在正在使用 Kotlin 后端,并考虑使用 Python 后端!
可扩展性的另一个方面是,Diplomat *特性*本身不需要在所有后端中得到支持。如果开发 Kotlin 后端的人想要回调支持,他们不必弄清楚如何将其添加到所有其他后端,而其他后端也大多不需要担心回调——他们只需将其标记为不支持。这一特性导致了 Diplomat 中特性的爆炸式增长:目前我们有多个用户,各自关心不同的后端子集,他们每个人都可以构建自己需要的功能,而不必过多担心为用户增加复杂性。然后,当其他用户想要这些功能时,他们采用起来就容易多了。
## 使用 Diplomat
Diplomat 的核心工作流程是,你编写一个单一的“桥接 crate”来封装你的 Rust API,该 crate 通过一个过程宏生成一个公共的底层 `extern "C"` API。然后你可以在这个桥接 crate 上运行 `diplomat-tool`,调用各个按语言区分的“后端”,以生成符合惯用法的语言绑定,这些绑定在底层调用相同的 `extern "C"` API。这种枢纽-分支模型意味着一个桥接 crate 支持你针对的每种语言。
例如,你可以编写如下代码:
```rust
#[diplomat::bridge]
mod ffi {
pub struct Settings {
pub something: u8,
pub something_else: bool,
}
#[diplomat::opaque]
pub struct MyObject(my_library::MyObject);
impl MyObject {
#[diplomat::attr(auto, constructor)]
pub fn create(settings: Settings) -> Box<MyObject> {
Box::new(MyObject::new(settings))
}
pub fn do_thing(&self) {
self.0.do_thing();
}
}
}
```
这将(通过过程宏)生成类似如下的 `extern "C"` API:
```rust
extern "C" fn MyObject_new(settings: Settings) -> *mut MyObject {...}
extern "C" fn MyObject_do_thing(this: &MyObject) {...}
```
并且还会为 `Settings` 添加 `repr(C)`。然后你可以选择一种支持的语言,运行 `diplomat-tool <backend> <path>` 来生成绑定到指定路径。
目前,我们有 `c`、`cpp`、`js`(包括 TypeScript)、`dart`、`kotlin` 和 `python-nanobind` 后端。还有一个 [正在单独仓库开发](https://github.com/rust-diplomat/diplomat-java) 的 `java` 后端。我们始终欢迎更多的后端!
在 C++ 中,这可能会生成一个结构体 `Settings` 和一个类 `MyObject`,具有方法 `create()` 和 `do_thing()`。在 JS 中,它会有一个类似的类,但 `create()` 会是一个构造函数,而 `do_thing()` 会重命名为 `doThing()`。为了进一步提高惯用性,`new MyObject()` 也可以接受与 `Settings` 具有相同字段的未类型化对象。在这两种情况下,构造函数/方法都会在底层调用 `MyObject_new`/`MyObject_do_thing`。
Diplomat 支持三种“自定义”用户定义类型:类 C 枚举、结构体和“不透明对象”。结构体通过 FFI 边界进行复制,而“不透明对象”则封装了一个底层对外部语言不透明的 Rust 对象,该对象位于分配的堆内存之后,只能通过所有权或借用指针传递。一些 Diplomat 后端还支持将 trait 作为第四种自定义类型,允许用户插入他们自己的接口实现。
Diplomat 还支持 `Option`、`Result` 和切片,将它们映射到目标语言的惯用可空性、错误和列表模型。例如,Rust 的 `Result` 在 JS、Dart 和 Python 中会抛出异常,但映射到 Kotlin 的 `Result` 类型。有关 Diplomat 支持跨 FFI 边界传递的类型的完整列表,请参阅 [Diplomat 书籍中的类型章节](https://rust-diplomat.github.io/diplomat/types.html)。
### 自定义
Diplomat 支持相当多的自定义。在示例代码中,你可以看到 `#[diplomat::attr(auto, constructor)]`,这意味着对于支持构造函数的后端,`create()` 被视为构造函数。`attr` 的第一个参数是一种类似于 `cfg` 的语法,用于选择后端,而 `auto` 大致意思是“选择支持该属性的后端”。对于构造函数,Dart、JS 和 Kotlin 支持它们,但 C++ 和 C 后端不支持。
> 困惑粒子发言:为什么 C++ 后端不支持构造函数?C++ 不是有构造函数吗?
>
> C++ 中的不透明类型位于 `unique_ptr` 之后,而 C++ 不允许你拥有返回其他类型的构造函数。我们可能会添加一些在 C++ 中实现类似构造函数功能的方法,但目前必须编写 `MyObject::create()` 也是可以的。
Diplomat [通过属性支持大量自定义](https://rust-diplomat.github.io/diplomat/attrs.html),所有这些属性都可以根据特定后端或特性可用性进行条件设置:
- `disable`:禁用 API。如果某个后端不支持所需的特性,或者该 API 是后端特定的优化,这很有用。你也可以使用 `#[diplomat::cfg(cpp)]` 作为 `#[diplomat::attr(not(cpp), disable)]` 的快捷方式。
- `rename`:重命名 API。可用于重载!
- `namespace`:用于将代码组织到命名空间/子模块中
- `constructor` 和 `named_constructor`:用于将方法标记为构造函数
- `iterator`、`iterable`:用于连接到内置语言迭代机制,启用 `for i in obj` 等功能
- `getter`、`setter`:用于将方法标记为访问器
- `indexer`、`add`、`sub`、`comparison` 等:用于重载大多数内置运算符
### 演示生成
通常,在谈论库的功能时,我希望人们能够随意使用其 API 并尝试不同的数值。例如,在 ICU4X 中,能够展示一个递进过程就很好:“看,它可以格式化日期!” → “这是用更简洁的格式表示的日期” → “这是法语的日期” → “这是法语、中国日历下的日期” → “这是法语、中国日历、泰语数字下的日期”,并且在每一步都能让人们摆弄参数。但 ICU4X 是一个 Rust 库,在 Rust 中进行这种演示需要拿出笔记本电脑并让人们修改代码。
不久前我意识到,Diplomat 已经知道如何为你的库生成 JS-Wasm 封装器,并且它已经在类型层面很好地理解了 API——这意味着 Diplomat 可以为大多数暴露的 API 生成基于 Web 的“演示”,方法是通过追踪构造函数,直到找到它可以向用户请求的原始/枚举类型。你可以在 [ICU4X 自动生成的演示页面](https://icu4x.unicode.org/2_2/demo) 上看到它的实际效果(尝试使用 `DateFormatter.formatIso` 来测试上述日期时间格式化的例子)。演示生成已被证明对我们非常有价值;上面链接的演示可以在手机上运行,并且在很短的时间内就能展示 ICU4X 的能力。Diplomat 书籍记录了 [如何设置这个功能](https://rust-diplomat.github.io/diplomat/demo_gen/intro.html)。
## 设计笔记
我喜欢编译器设计,而 Diplomat 本质上就是一个编译器。它接收(用 `syn` 解析的)Rust 代码,并通过一系列中间表示将其转换为绑定。与 `rustc` 一样,Diplomat 也有两层抽象语法树风格的 IR。
[^1]: 原文注释 1:`fn:opposite` — 例如,将这些工具用于单向 FFI,但方向相反意味着它们是为从另一种语言调用 Rust 而设计的,但我需要从 Rust 调用另一种语言;或者它们是为另一种语言调用 Rust 而设计的,但我需要相反的方向。
[^2]: 原文注释 2:`fn:1` — 可能不是,但感觉是这样的。
[^3]: 原文注释 3:`fn:2` — 在编译器中,这被称为“门控式解析”。
相似文章
Rust编译器的大语言模型政策
本文介绍了Rust Forge,这是一个Rust编程语言的补充文档仓库,包含构建、贡献和维护文档的说明。
我们如何(及为何)将生产环境的C++前端基础设施重写为Rust
NearlyFreeSpeech.NET 将其生产环境的C++前端基础设施(nfsncore)重写为Rust,该系统负责所有传入请求的路由、缓存和访问控制。迁移的动机是Rust的安全性保证、性能、生态系统优势以及老化的C++代码库的局限性。
Show HN: Hsrs – 用于 Rust 的类型安全 Haskell 绑定生成器
Hsrs 是一个类型安全的 FFI 绑定生成器,允许从 Haskell 调用 Rust 代码,具有自动内存管理、类型转换和 Borsh 序列化功能。它在 Rust 中提供注解,并生成符合语言习惯的 Haskell 包装器。
将React编译器移植到Rust
一项将React编译器移植到Rust的计划,旨在提高性能并与Rust生态系统集成。
将OCaml运行时从C语言逐行翻译为Rust
该项目详细介绍了将OCaml运行时从C语言逐行翻译为Rust的过程,旨在提高安全性和性能。