Ante:融合借用检查与引用计数的新方式

Lobsters Hottest 工具

摘要

Ante 提出了一种新颖的方法,将借用检查和引用计数无缝结合,且不会引发运行时崩溃,使开发者能够在系统编程语言中同时使用这两种范式。

<p><a href="https://lobste.rs/s/vv4fhi/ante_new_way_blend_borrow_checking">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/29 02:22

# Ante:融合借用检查与引用计数的新方式 来源:https://verdagon.dev/blog/ante-blending-borrowing-rc 融合借用检查与引用计数,且无运行时崩溃! 2026年6月28日—— Ante 迈出了第一步,实现了**我们曾以为不可能的事情:**将引用计数与借用检查融合,且无运行时崩溃。0(https://verdagon.dev/blog/ante-blending-borrowing-rc#note0)这*非常*令人振奋,因为这意味着有一天,我们可以更轻松地在**合适的场景使用各自的方式**,而无需冒崩溃的风险。如果存在这样的语言,我就能先用引用计数灵活地制作游戏原型,然后逐步迁移到更快的借用检查代码。1(https://verdagon.dev/blog/ante-blending-borrowing-rc#note1) 目前还没有主流语言找到无缝融合两者的方法,尽管许多语言尝试过。Rust 尝试了,但当我们使用 Rust 的引用计数 `Rc` 类型时,通常需要 `RefCell`(例如 `Rc<RefCell<T>>`),若使用不当,会在运行时崩溃。2(https://verdagon.dev/blog/ante-blending-borrowing-rc#note3)我**希望 `rustc`** 能在编译时检查 `Rc` 的正确使用。45(https://verdagon.dev/blog/ante-blending-borrowing-rc#note5) Swift 也尝试过,推出了新的借用系统(https://github.com/swiftlang/swift/blob/main/docs/OwnershipManifesto.md),但其运行时检查代价高昂(https://forums.swift.org/t/huge-performance-hit-from-exclusive-memory-access-checks/54374/12),若使用不当就会崩溃(https://github.com/swiftlang/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md)。事实证明,融合引用计数与借用检查*极其困难*,原因下文会阐述。 那么,我们来聊聊 Ante! **Ante 仍在开发中!**本文有些部分已实现,有些仍处于理论阶段,设计也在不断变化。目前该项目正在积极开发中,请关注 Ante 的官方网站(https://antelang.org/)和 Discord(https://discord.gg/BN97fKnEH2)了解最新进展! ### Ante:融合借用检查与引用计数的新方式 0 更具体地说,Ante 找到了一种方法,在可变对象上融合引用计数与借用检查,且无需 Rust 的 `RefCell` 或 Swift 的独占性检查所带来的运行时 panic 或开销。 1 这与我致力于 Vale 的原因相近!但 Vale 使用世代引用,这种引用常常会引发程序停顿。 2 例如,两个人同时获取同一数据的唯一(读写)引用就会导致崩溃。 3 它还提供了 `try_borrow()` 和 `try_borrow_mut()` 方法,这些方法不会 panic,但只是把问题转移到了别处。 4 当然,GhostCell / QCell 不算,它们有其他限制,且无法直接替代 `RefCell`。 5 还有 `Cell`,但它不允许你获取其内部数据的引用。例如,你无法从 `&Cell<(T, U)>` 得到 `&Cell<T>`。Rust 在内部可变性方面通常也充满摩擦,导致许多用户避而远之。 ### Ante Ante 旨在成为一款更简单的 Rust,一种兼具内存安全和线程安全的系统编程语言。它采用单一所有权和借用检查,因此值可以是内联的(在栈上,或在包含它的结构体/数组中)。当用户希望优先考虑简洁性时,他们可以通过在类型定义前加上 `shared` 关键字来选择引用计数。例如,以下代码片段实现了红黑树的平衡: ``` // Color 可以是 R 或 B shared type Color = | R | B // RbTree 可以是 Empty 或 Tree shared type RbTree t = | Empty | Tree Color (RbTree t) t (RbTree t) // 适用于任意元素类型 t 的 RbTree 的平衡函数 balance (tree: RbTree t) {Copy t}: RbTree t = match tree | Tree B (Tree R (Tree R a x b) y c) z d | Tree B (Tree R a x (Tree R b y c)) z d | Tree B a x (Tree R (Tree R b y c) z d) | Tree B a x (Tree R b y (Tree R c z d)) -> Tree R (Tree B a x b) y (Tree B c z d) | other -> other ``` 我通常偏爱 C 风格语法,但即使是我也不得不承认,这段代码非常优美。而且它也很简洁,与 Python 版本(https://github.com/Verdagon/AntePost/blob/main/python-rbtree/rbtree.py)相当,比 C++ 版本(https://github.com/Verdagon/AntePost/blob/main/cpp-rbtree/main.cpp)和 Rust 版本(https://github.com/Verdagon/AntePost/blob/main/rust-rbtree/src/main.rs)都要短小。 但最令我感兴趣的是 Ante 在内存安全方面的创新。Ante 拥有**共享可变性的超能力:**如果你想可变地借用引用计数的数据,无需冒运行时错误的风险。目前没有主流语言能做到这一点,即使是 Rust 或 Swift 也不行。 在讨论共享可变性的超能力之前,我们先从基础开始。看看它是如何进行借用检查的,然后再把引用计数加入其中。 ### 形状稳定性 Ante 有一个**形状稳定性**的概念,这意味着“对形状稳定对象的引用始终有效 6(https://verdagon.dev/blog/ante-blending-borrowing-rc#note6),无论其他地方发生何种修改。”正因为如此,Ante 代码可以安全地**同时拥有多个指向同一结构体的可变借用引用。** 为了铺垫,这里有一个 `heal` 函数,它接受两个指向 `Entity` 的可变引用: ``` type Entity = energy: I32 health: I32 heal (healer: mut Entity) (target: mut Entity) = healer.energy -= 10 target.health += 10 ``` 在 Ante 中,我们可以对两个参数传入同一个 `Entity` 来调用 `heal`——例如,当某个实体自我治疗时: ``` self_heal (entity: mut Entity) = heal entity entity ``` 修改 `healer` 不会以任何方式使指向 `Entity` 的共享引用失效,7(https://verdagon.dev/blog/ante-blending-borrowing-rc#note7)因此编译器接受这段代码为有效。换句话说,即使 `healer` 和 `target` 可能指向同一个 `Entity`,这也是内存安全的:这里没有任何操作能销毁 `Entity`,所以两个引用都保持有效。 现在让我们稍微复杂一些。Ante 代码实际上可以同时拥有多个指向同一结构体的可变借用引用,**或者指向其任意结构体字段,或者这些字段的任意字段,同时进行。**例如,下面我们同时拥有一个指向飞船的可变借用引用,以及另一个指向飞船引擎的可变借用引用。 ``` type Engine = fuel: I32 type Spaceship = engine: Engine name: String refuel (ship: mut Spaceship) = engine_alias: mut Engine = ship.engine // 仍然可以使用原始 `ship` ship.engine.fuel := 200 engine_alias.fuel := 100 ``` Ante 知道这完全是内存安全的,因为在函数执行期间,没有人能销毁 `ship`,因此也没人能销毁它的 `engine` 或 `fuel`。换句话说,这些引用指向的是**形状稳定**的数据。 熟悉 Rust 和 Swift 的人会对这一点感到惊讶: - 在 Rust 中,我们**不能**(https://github.com/Verdagon/AntePost/blob/main/rust-twomuts/src/main.rs)拥有多个指向同一数据的 `&mut` 引用。8(https://verdagon.dev/blog/ante-blending-borrowing-rc#note8) - 在 Swift 中,我们同样**不能**(https://github.com/Verdagon/AntePost/blob/main/swift-twomuts/Sources/SwiftTwoMuts/main.swift)拥有多个指向同一数据的 `&mut` 引用。 现在,让我们加入引用计数! 6 此处“有效”指的是可解引用;你可以解引用该引用,而不会面临任何内存不安全或未定义行为的风险。 7 类似于在 Rust 中,你不能使用 `&mut Spaceship` 来销毁它所指向的 `Spaceship`。即使在 Rust 中,让多个 `&mut Spaceship` 引用指向同一个 `Spaceship` 局部变量也是完全安全的。 8 我们有时可以用 `Cell` 接近这个效果(https://github.com/Verdagon/AntePost/blob/main/rust-cell/src/main.rs),但附录部分会解释为什么 Ante 比 Rust 的 `Cell` 更进一步。 ### 引用计数 上述能力与**引用计数完美契合**,如下文所示。在 Ante 中,如果在一个类型定义前加上 `shared`,意味着该类型会自动进行引用计数。9(https://verdagon.dev/blog/ante-blending-borrowing-rc#note9)当你拥有一个 `shared mut` 类型时,可以无需锁定直接修改其字段,就像 `set_fuel` 对 `Spaceship` 的 `engine: Engine` 字段所做的那样: ``` type Engine = fuel: I32 shared mut type Spaceship = engine: Engine name: String launch (var ship: Spaceship) = set_fuel (mut ship.engine) set_fuel (engine: mut Engine) = engine.fuel := 100 ``` 我下面会进一步解释这段代码,但目前来说:这与 Rust 版本(https://github.com/Verdagon/AntePost/blob/main/rust-spaceship/src/main.rs)和 Swift 版本(https://github.com/Verdagon/AntePost/blob/main/swift-spaceship/Sources/SwiftSpaceship/main.swift)类似,但没有 Rust 的 `.borrow_mut()` 或 Swift 的自动运行时独占性检查所带来的崩溃风险。 `launch` 可以创建一个 `mut Engine` 借用引用,因为 `launch` 知道 `engine` 会保持存活,因为 `launch` 一直持有包含它的 `Spaceship`。更一般地说:**你总是可以对 shared mut 类型的字段创建 mut 借用引用**,即使它们是共享的。10(https://verdagon.dev/blog/ante-blending-borrowing-rc#note10) 9 至少目前如此。最终,应用程序(而非代码)将能够配置其内存管理方式,例如通过 RC、GC 或自定义机制。 10 不过,你并不总是能对字段内部的对象创建 mut 借用引用,我们稍后会看到。针对不同情况有不同机制。 ### 更显式的语法 从现在开始,我将使用更显式的 `Rc Spaceship` 语法,而不是 `shared mut type` 语法糖(稍后会说明原因)。使用更显式的 `Rc` 语法,上述代码片段会变成:11(https://verdagon.dev/blog/ante-blending-borrowing-rc#note11) ``` type Engine = fuel: I32 type Spaceship = engine: Engine name: String launch (var ship: Rc Spaceship) = set_fuel (mut ship.engine) set_fuel (engine: mut Engine) = engine.fuel := 100 ``` 不同之处: - `shared mut type Spaceship` 变成了 `type Spaceship` - `var ship: Spaceship` 变成了 `var ship: Rc Spaceship` 11 我们假设 Ante 语法允许直接访问 Rc 内容的字段。如果不行,可能会写成类似 `set_fuel (ship.as_mut()).engine` 的形式。 ### 联合体 联合体**在速度上非常出色。**12(https://verdagon.dev/blog/ante-blending-borrowing-rc#note12)我们喜欢联合体。不幸的是,联合体以其不安全性而闻名,内存安全语言很难很好地支持它们。为了说明原因,请看下面的例子,其中 `Engine` 是一个联合体,我们试图对其进行不安全的操作。 ``` type Engine = | StringTheoryEngine (str: String) | ImpulseEngine (fuel: I32) type Spaceship = engine: Engine name: String launch (var ship: Rc Spaceship) (var other_ship: Rc Spaceship) = match uniq ship.engine | StringTheoryEngine str -> other_ship.engine := ImpulseEngine 0x42 str.[0] := 'z' | ImpulseEngine fuel -> () ``` 如果我们对 `ship` 和 `other_ship` 传入同一个 `Spaceship`,这个程序实际上会段错误!因此,**Ante 需要拒绝编译上述代码。** Ante 有这样的规则:如果你有一个指向联合体的 mut 借用引用,你就**不能**再创建指向其某个变体的 mut 借用引用。这与之前结构体的规则相反:如果你有一个指向结构体的 mut 借用引用,你**可以**再创建指向其某个字段的 mut 借用引用。 在理想情况下,Ante 会拒绝上述程序,并给出如下错误: ``` match uniq ship.engine | StringTheoryEngine str -> // 错误:修改 `other_ship.engine` 可能导致 `ship.engine` 在被使用时被销毁 // 注意:`other_ship.engine` 是一个 `Engine`,可能与 `ship.engine` 存在别名 other_ship.engine := ImpulseEngine 0x42 str.[0] := 'z' | ImpulseEngine fuel -> () ``` 但 Ante 怎么会知道呢?这有可能吗?答案是:可以的!Ante 知道要给出这个错误,是因为 `ship.engine` 经历了一次**临时的 `uniq` 转换。**那么,我所说的“临时 uniq 转换”是什么意思?`uniq` 又是什么? 12 联合体之所以快,是因为它们将内容内联存放,从而减少缓存未命中(或“指针追逐”)。例如,在 C 中,如果我们有一个像这样的联合体: ``` union Engine { StringTheoryEngine ste; ImpuseEngine ie; }; ``` 并且它位于一个 `Spaceship` 内部,如: ``` struct Spaceship { Engine engine; }; ``` 那么 `StringTheoryEngine` 和 `ImpulseEngine` 就存放在 `Spaceship` 的内存*内部*。其他语言如 Java 则不同,它们将 `Engine` 作为接口,让 `Spaceship` 包含一个 `Engine` 指针。指针是慢的。联合体是快的。Ante 在这方面与 C 类似。 ### `uniq` 引用 **`uniq`** 表示“独占可变引用”。如果一个变量包含 `uniq Spaceship`,那么它就是**唯一可用的**指向该 `Spaceship` 的引用。13(https://verdagon.dev/blog/ante-blending-borrowing-rc#note13) ``` set_fuel (engine: uniq Engine) (other: mut Engine) = // 在这里,我们知道没有其他东西指向 `engine` ``` 这通常单独使用时没什么用,但它允许我们指向联合体内部的内容。 13 Ante 的 `uniq Spaceship` 类似于 Rust 的 `&mut Spaceship`。 ### `uniq` 如何帮助处理联合体 由于可能存在其他别名指向同一数据,允许随意修改联合体字段是不安全的。例如,一个用户可能持有来自 `Maybe String` 的 `String`,而另一个用户将其改为 `None`。或者,就像上面例子中,有人在别人持有指向其某个字符的引用时,销毁了 String 的容器。 语言通常通过引用计数(Swift)或完全禁止共享可变性(Rust)来解决这个问题。但是,尽管我们想阻止上述不安全程序,我们仍希望允许那些*安全地*拥有指向联合体内部内容的可变引用的程序: ``` type Engine = | StringTheoryEngine (str: String) | ImpulseEngine (fuel: I32) type Spaceship = engine: Engine name: String launch (var ship: Rc Spaceship) = match uniq ship.engine // match 语句需要 uniq 引用 | StringTheoryEngine str -> // 获取指向 StringTheoryEngine 的 uniq 引用 str.[0] := 'z' | ImpulseEngine fuel -> () ``` 在诸如 Swift(https://github.com/Verdagon/AntePost/blob/main/swift-union/Sources/SwiftUnion/main.swift)和 Rust(https://github.com/Verdagon/AntePost/blob/main/rust-union/src/main.rs)等语言中,相等效的程序允许这种安全操作,但不幸的是,它们会添加额外的运行时开销/检查。幸运的是,Ante 不需要额外的运行时开销/检查,因为它知道在这个函数执行期间,没有人会修改 `ship.engine`。Ante 之所以知道这一点,是因为它的**临时 uniq 转换**机制。 ### `uniq` 转换 Ante 的洞察在于:我们可以**临时获得某个东西的 `uniq` 引用**,只要在该作用域内**不访问任何其他可能指向它的内容。**例如,下面是一个带有 `uniq` 转换的程序,注释标明了作用域的起始和结束: ``` type Engine = | StringTheoryEngine (str: String) | ImpulseEngine (fuel: I32) type Spaceship = engine: Engine name: String launch (var ship: Rc Spaceship) = // 开始作用域:在此作用域内,不能访问任何可能包含 Spaceship 的其他变量 match uniq ship.engine | StringTheoryEngine str -> str.[0] := 'z' | ImpulseEngine fuel -> () // 结束作用域:在此作用域内,不能访问任何可能包含 Spaceship 的其他变量 ``` `uniq` 引用存在于 `// 开始作用域` 和 `// 结束作用域` 注释之间的作用域内(更具体地说,是从 `ship` 的声明到其最后一次使用之间的所有代码)。当 `uniq` 引用存在时,Ante 会阻止我们使用任何其他可能间接包含 `Spaceship` 的已存在变量。 而 Rust 则禁止 `uniq` 存在,“因为其他地方可能存在其他引用”。Ante 则认为,只要我们**在此作用域内不使用**那些引用,就可以创建 `uniq`。这正是关键所在。 一个 `uniq Spaceship` 并不是指向该 `Spaceship` 的唯一引用,它只是**在此作用域内唯一可用的**引用。

相似文章

无类型检查的生命周期借用检查

Lobsters Hottest

一篇博客文章介绍了一种玩具语言,它在运行时而非静态类型系统中强制执行借用检查,通过在栈上使用轻量级引用计数来支持内部指针和单一所有权,适用于动态类型环境。

使用:counters和:atomics模块在Erlang中快速计数

Hacker News Top

这篇技术文章解释了如何使用Erlang的:counters和:atomics模块进行高性能计数和共享可变状态,从而突破标准的进程隔离模型。内容涵盖BEAM运行时中的原子操作,如add_get、exchange和compare-and-swap(比较并交换)。

Windows堆栈限制检查回顾,后续

The Old New Thing (Raymond Chen)

Raymond Chen跟进了他之前关于ARM64堆栈限制检查的文章,指出了堆栈探测函数中x15寄存器的非常规使用细节,并比较了多个架构的寄存器使用。

简化ZGC中的弱引用处理

Lobsters Hottest

这篇乌普萨拉大学的硕士论文与Oracle合作完成,研究了通过提出三种流水线修改和一种替代的注释字段机制,来减少ZGC垃圾收集器中弱引用处理的开销。