安全 Rust 的边界
摘要
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。
<p><a href="https://lobste.rs/s/hkwyrc/edge_safe_rust">评论</a></p>
查看缓存全文
缓存时间: 2026/04/22 14:15
# 安全 Rust 的边界
来源:https://kyju.org/blog/tokioconf-2026/
*疯狂滥用 Rust 特性,为“指针汤”提供可证明的内存安全与追踪式垃圾回收。*
**2026-04-22**
- 引言 (https://kyju.org/blog/tokioconf-2026/#introduction)
- 背景 (https://kyju.org/blog/tokioconf-2026/#background)
- Rust 里“所有权不明”的指针非常难搞 (https://kyju.org/blog/tokioconf-2026/#pointers-in-rust-without-clear-ownership-is-very-hard)
- 其实 Rust 最擅长的是 Vec (https://kyju.org/blog/tokioconf-2026/#rust-is-actually-just-really-good-at-vec)
- 但 Rust 对安全循环引用相当不友好 (https://kyju.org/blog/tokioconf-2026/#but-rust-is-pretty-bad-at-safe-circular-references)
- 生成性,或者说 for<'a> 才是 Rust 最酷的特性 (https://kyju.org/blog/tokioconf-2026/#generativity-or-for-a-is-the-coolest-feature-in-rust)
- 通过“追踪”解决“可达性” (https://kyju.org/blog/tokioconf-2026/#dealing-with-reachability-via-tracing)
- 一个真正基于裸指针的 GC 草图 (https://kyju.org/blog/tokioconf-2026/#a-sketch-of-a-real-raw-pointer-based-gc)
- 收束——更大的图景 (https://kyju.org/blog/tokioconf-2026/#wrapping-up-the-bigger-picture)
---
演讲幻灯片在此! (https://kyju.org/tokioconf-2026-slides/index.html)
## 引言
这是我在首届 TokioConf(https://www.tokioconf.com/)4 月 22 日演讲的文字版。我先把它写成一篇普通博客,因为把内容先落成文字对我准备演讲最有帮助。幻灯片链接已放在上面,等视频放出后我也会补上。
每次选题我都在两条路之间纠结:
1. 写我熟、大家又感兴趣的东西。
2. 写我独有体会、别人很难讲出来的东西。
这次几乎 100% 是第二条路。我最近泡在 Rust 一个极冷门的角落,于是决定把这段经历讲出来。表面看,话题似乎跟 *Tokio* 关系不大——我要讲的是如何在安全 Rust 里做垃圾回收和虚拟机。
说实话,我最近真没写多少正经网络代码(¯\\(ツ)/¯,而且写的也都是游戏相关,跟 Tokio 的用法不是一回事)。但过去几个月,我在帮 Kong(TokioConf 的赞助商之一)设计小型、隔离、*安全* 的 Rust 规则引擎 VM——正是这个话题!所以,*应该* 算切题吧。
总之,这就是我在折腾的东西,也是我最可能讲出点新意的方向。希望你觉得有趣。
> 我默认读者没看过我以前的博客/演讲,所以部分内容会重复。
## 背景
很久以前,我痴迷于把“追踪式 GC”塞进安全 Rust。远非只有我一人尝试(见 Manish 的综述),但我的目标格外激进:
1. 零成本指针:
`Gc<T>` 必须是 `*const T` 的透明包装,且是 `Copy`,开销与裸指针完全一致(分配/回收另算)。
2. 完全安全 Rust 可用。
最后我认定:通用方案几乎*必须*语言级支持,而我“失败”了。但也没全败——我的最佳成果 [gc-arena](https://github.com/kyren/gc-arena)(注:Ruffle 项目后来贡献了大量核心代码,我不敢独占)如今跑在两大用户量过百万的项目里:
- [Ruffle](https://github.com/ruffle-rs/ruffle)(浏览器 Flash 模拟器)用它跑 ActionScript VM。
- 下一版 [Fields of Mistria](https://www.fieldsofmistria.com/) 将使用我的 [fabricator](https://github.com/kyren/fabricator)(GameMaker 兼容运行时),底层同样用 gc-arena。
其实我解决的是“*Rust 代码在 GC 上下文里不活跃时* 才回收”这一子问题——对游戏来说刚好够用:帧内从不回收,脚本一帧就跑完。
> “既然我解决不了你的问题,那要不要换个我能解决的问题?”——成功案例。
如此自夸之余,我意识到:包括我在内,全球大概只有寥寥几人活在这个“奇怪生态位”。想在安全 Rust 里做 GC,几乎一定得玩出 lifetime 黑魔法,代码外人看来像疯子。
给你看一段真实商业项目(Fields of Mistria,强烈推荐, cozy 农场游戏,Steam 有售)里的代码:
```rust
// `ctx` 是啥?
let methods = Gc::new(&ctx, DsGridMethods);
// `unsize!` 又是啥?还有自定义语法?
let methods_ptr = gc_arena::unsize!(methods => dyn vm::UserDataMethods<'gc>);
```
```rust
// `Rootable![_]` 宏居然返回一个**类型**?
// 宏里写 `'_` 是什么鬼?
let methods = ctx.singleton::<Rootable![dyn vm::UserDataMethods<'gc>]>>().0;
let ud = vm::UserData::new::<Rootable![DsList<'gc>]>(&ctx, self);
ud.set_methods(&ctx, Some(methods));
```
```rust
// `downcast_write` 听起来像 `downcast_mut`?
// 不,它返回 `&'gc barrier::Write`,完全看不懂!
let ds_list = DsList::downcast_write(&ctx, ud).unwrap();
// `barrier` 又是啥?为啥非得宏?这语法是人看的?
let inner = barrier::field!(ds_list, DsList, inner);
// 最后这句我大概懂,但“unlock”是什么操作?
let mut vec = inner.unlock().borrow_mut();
```
这种“聪明过头”的代码,正常情况下能让你*被开除*——或者,如果你理由足够硬,被请去 TokioConf 演讲。
然而,这段几乎看不懂的代码,就在两个中等规模的真实项目里跑着。这说明:
- 对某类程序,安全 Rust 目前*应该*更简单,却一点也不;
- 就算得用一门“长得像 Rust 的奇怪方言”,它仍可能是*最佳可行方案*。
> *如果能把更多人拉进我的个人地狱,也许就不那么痛苦了?*
## Rust 里“所有权不明”的指针非常难搞
别慌,本文真正讲的*不是* gc-arena 的设计(感兴趣请移步[这篇](https://kyju.org/blog/rust-safe-garbage-collection/))。我只想让你明白:**想在安全 Rust 里做 GC,难到变态。**
本文主题甚至不是 GC,而是一个所有 Rustacean 迟早会发现的简单事实:
***Rust 对循环指针极其不友好。***
我知道这是废话,但它是本次演讲的核心:盘点当下 Rust 应对这一现实的几种办法。
先定位:我抱怨的是“*完全安全* 接口”能做的事;而在“带循环指针”这一领域,Rust *基本上是唯一*能提供内存安全证明的语言。
广义上,语言对“指针安全”分三派:
1. 所有权 + Drop 派:安全 Rust、Swift、理论上的 C++。
2. “可达性”派,即 GC 语言:JS、Go、Python、Ruby、Lua、OCaml、Java、Scheme……
它们默认允许循环,因为通常只有一种引用类型,没有“所有权”概念。
3. “信我兄弟”派:Unsafe Rust、C、实际中的 C++…… 关键词“use after free”。
看到 #2 你会想“GC 语言呗”。但先听一句可能反直觉的定义:
***所有权 + Drop 也是垃圾回收的一种。***
只要语言不让程序员手动 free,就必须自动回收,而“自动回收”就叫 GC。Drop 只是*确定性*的 GC,牺牲了通过“可达性”处理循环的能力——换来的是时序确定、零运行时、可在编译期知晓的析构行为。
于是悲剧来了:
- 安全 Rust 必须证明引用不悬垂;
- 它的唯一 GC 系统基于所有权;
- 循环指针按定义没有清晰所有权;
- 因此编译器无法自动回收它们!
结局:要么所有可能循环的指针都活成 `'static`,要么退化成裸指针手动 free——于是人人“与借用检查器搏斗”。
我最近的项目之所以踩坑,是因为:
**我在用 Rust 安全地实现一门“可达性”语言。**
“Rust 写链表难”已是梗。看 Lua 版循环链表多简单:
```lua
-- Lua 循环链表
local a, b, c = {}, {}, {}
a.next = b
b.next = c
c.next = a
-- 完事,随便环。
```
真实 Lua(Python、Ruby、JS、甚至 Java)代码里,这种循环无处不在。
可在 Rust 解释器里,你*必须*用某种内部数据结构表示它。
而前面说了:想用普通 `&'a T` 安全地*删除*不可达节点,基本不可能。
当然,crates.io 上大把能处理循环图的 Rust 库,绝大多数既没玩 lifetime 黑魔法,也没用 [bacon-rajan](https://github.com/fitzgen/bacon-rajan-cc) 循环 `Rc`。最常见方案是——
## 其实 Rust 最擅长的是 Vec
初学 Rust 时,你总会遇到一个看似需要循环引用的问题,然后被借用检查器教做人。
多数情况下,问题*可以*换种方式解决(有时更好,有时只是不同,有时略烦),你就此升级。
直到某天,你遇到*非得*循环不可的场景:反向指针无法静态推断,或对象组成无结构循环图,毫无所有权可言。
于是你学会“Vec + 下标”这套终极解法:
- 所有节点塞进同一个 Vec;
- 用下标当“指针”;
- 传个“上下文”就能随便图遍历。
如果节点能统一成单类型,又不需要频繁增删,这方案简直完美——零 lifetime 烦恼,缓存友好,还能安全地删除节点(swap-pop 即可)。
(后面还会讲更花哨的裸指针 GC,但先把 Vec 大法讲透。)
相似文章
无 Unsafe 代码的垃圾回收
safe-gc 是一个全新的 Rust 库,它完全不用 unsafe 代码就实现了垃圾回收器,通过“堆索引”而非直接解引用指针来保证内存安全。
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。
面向 Morello 的 Rust:始终在线内存安全,即使在非安全代码中
本文介绍了一种针对 Morello 能力硬件架构修改的 Rust 编译器,通过利用硬件能力,实现即使在非安全代码中也始终在线内存安全。
Rust与C/C++在内存安全CVE上的差异
分析Rust与C/C++在内存安全CVE报告方式上的不同,论证即使存在错误,Rust的设计也能降低某些类型漏洞的发生。
内存安全绝对主义者
本文批评了编程语言辩论中的内存安全绝对主义,认为像 Fil-C 这样的新方法也有权衡,而将 Rust 视为不安全忽略了实际好处。