> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/20161112-bian-cheng-yu-yan/its-better-to-rust-than-wear-out.md).

# Rust 比磨损更好

* 原文：[It’s Better to Rust Than Wear Out](https://freebsdfoundation.org/wp-content/uploads/2016/12/Its-Better-to-Rust-Than-Wear-Out.pdf)

同事首次向我热情推荐 Rust 时，我心存怀疑。早年间我为安全关键系统开发软件，吃过苦头后才明白：编程语言常不如表面看上去那么正常。以 C 为例，尽管标准化工作可观，C 规范仍充斥未指定、未定义与实现定义的行为 \[2]。即便到了 2016 年，研究人员仍在探究 C ISO 标准与实际用法之间的差异 \[4]。并非所有软件工程师都需关注位字段以非 int、signed int 或 unsigned int 类型声明时会发生什么（这是未定义行为 \[2]）这类看似冷僻的问题，但我与安全关键及安全系统打交道太久，这部分较真无法关掉。于是略带轻蔑，我把 Rust 与 Go、Haskell 等听起来很酷但我永远用不上的技术归为一类。今年早些时候，我有机会重新审视 Rust，发现自己有些草率。

我当时在为基于 DTrace 的分布式追踪框架开发原型。原型用 C 编写，作为 DTrace 消费者（与 libdtrace 交互），通过 Apache Kafka 将 DTrace 记录向上游发送以便进一步处理（聚合、重排序等）。原型阶段还算好用，但随着工作推进，我需要快速探索设计空间。这一任务更适合用高级语言，但该选哪一种？像所有靠谱工程师一样，我开始列需求。我需要一门强调程序员生产力的语言，需要能轻松高效地与 C 库（如 libdtrace）交互，需要便于部署，因此需要庞大运行时的语言（尤其是 Java）直接出局。理想情况下，要良好支持并发并预防数据竞争。最后，戴上安全帽子，我不想因引入一堆可利用漏洞而丢人。我回想起与同事早先的对话：这些需求不正是 Rust 所针对的吗？于是我决定试一试 Rust，所幸我这么做了，因为我非常喜欢 Rust。那么 Rust 究竟是什么？

Rust 的愿景很简单——为 C++ 提供安全替代品，让系统程序员更高效，让关键任务软件更不易出 bug，让并行算法更易处理。Rust 的主要益处有 \[5]：

* 零成本抽象、保证内存安全（无需垃圾回收）、线程间无数据竞争；
* 类型推断；
* 极简运行时，高效的 C 绑定。

Rust 语言已有不少详尽教程，尤其“Rust Book”\[5]。因此本文不再重复，而是着重介绍我觉得特别有吸引力的特性。其间会讨论最难掌握的 Rust 特性。最后，演示如何在 FreeBSD 上开始 Rust 编程。

## 与借用检查器搏斗

在动手打开你最爱的文本编辑器（当然是 vim）之前，必须先理解 Rust 最显著的代价：陡峭的学习曲线。在这条学习曲线上，没有什么比反复触怒“借用检查器”（borrow checker，即 Rust 所有权系统的名义执行者）更让人抓狂的了。所有权是 Rust 最引人注目的特性之一，是 Rust 内存安全保证的基石。在 Rust 中，变量绑定（将值绑定到名称）拥有其所绑定值的所有权。所有权是互斥的，即资源必须只有一个所有者。借用检查器的职责就是维护这一不变式，方式是尽早（编译期）且大声地失败。

下面这个例子取自“Rust Book”（The Rust Programming Language，2016），v 绑定到向量 `vec![1, 2, 3]`（`vec![1, 2, 3]` 是一个 Rust 宏，创建包含 1、2、3 的连续可增长数组）。函数 `foo()` 是变量绑定 v 的“拥有作用域”。v 进入作用域时，在栈上分配新向量，元素则在堆上；作用域结束时，v 的内存（栈上与堆上的部分）自动释放。哦也，无需垃圾回收的内存安全。

```rust
fn foo() {
    let v = vec![1, 2, 3];
}
```

所有权可通过赋值 `let x = y` 转移（移动语义）。但记住所有权互斥，所以下面示例中，变量 v 在所有权转移给 v2 之后被引用（在宏 `println` 中）时，借用检查器会大声抗议 `error: use of moved value: 'v'`。

```rust
let v = vec![1, 2, 3];
let v2 = v;
println!("v[0] is: {}", v[0]);
```

下面示例中，调用 `bar()` 并把向量 v 作为参数传入，会转移 v 的所有权。当拥有作用域（函数 bar）结束时，v 的内存自动释放（同前）。可从 bar 返回 v 把所有权还给调用者。这种做法很快会让人厌烦，因此 Rust 允许借用引用（即“借用”变量绑定的所有权）。被借用的绑定在自身作用域结束时不会释放资源。这意味着在调用 `bar()` 之后，我们仍可使用最初的绑定：

```rust
fn bar(v: &Vec<i32>) {
    // 在此处对 v 做些有用的事
}
let v = vec![1, 2, 3];
bar(&v);
println!("v[0] is: {}", v[0]);
```

## 默认不可变

Rust 的变量绑定默认不可变。在 C 与 Java 中分别敲了无数 `const * const` 与 `final` 之后，这一特性本身就让我欣喜；更重要的是，与 `const` 不同，它确实提供了不可变性。可用 `mut` 关键字将变量绑定指定为可变：`let mut x = 10`（也注意类型推断的合理使用）。与变量绑定一样，引用默认不可变，可用 `mut` 关键字变为可变（`&mut T`）。共享可变状态会引发数据竞争。Rust 通过强制以下二者之一来防止共享可变状态：

* 对资源的一个或多个引用（`&T`），或
* 恰好一个可变引用（`&mut T`）。

## 选择你的保证

Rust 的哲学是让程序员掌控保证与代价。Rust 关于“一个或多个不可变引用或恰好一个可变引用”的规则在编译期强制执行。但与整体哲学一致，也支持运行期与编译期强制之间的多种权衡。引用计数指针（`Rc<T>`）允许多个“拥有”指针指向同一（不可变）数据；只有所有引用计数指针都离开作用域时，数据才被丢弃、内存才被释放。这在只读数据被共享且无法确定所有消费者何时访问完毕时很有用。引用计数指针给出的保证（所有拥有指针离开作用域时内存才释放）与所有权系统在编译期强制的保证不同。但这伴随额外代价（维护引用计数的内存与计算开销）。类似地，可变状态也可共享（使用 `Cell<T>` 类型），这又带来保证与代价的不同权衡。

## 生命周期

所有权还有一个相当微妙的最后议题。变量绑定存在于其拥有作用域内，对这些绑定的借用引用也存在于各自独立的作用域内。变量绑定离开作用域时，所有权放弃，内存自动释放。那么如果变量绑定离开作用域时，被借用的引用仍在使用，会怎样？一言以蔽之，糟糕的事情会破坏 Rust 的内存安全保证。因此这种情况不能允许发生。生命周期是 Rust 防止借用引用比原资源活得更久的机制。

在 Rust 中，每个引用都有相关联的生命周期。但生命周期常常可以省略。下面示例展示了引用 s 的生命周期（`'a`）省略形式与显式形式等价的语法：

```rust
fn print(s: &str);          // 省略
fn print<'a>(s: &'a str);   // 展开
```

全局变量很可能是 Rust 新手与生命周期的首次遭遇。全局变量用 Rust 特殊的静态生命周期声明：`static N: i32 = 5;`。静态生命周期表示变量绑定拥有整个程序的生命周期（注意字符串字面量拥有 `&'static str` 类型，因此活到程序结束）。如果让我猜生命周期下次在哪里冒头，那大概是在 struct 中存储引用。在 Rust 中，struct 用于创建复杂（复合）数据类型。当 Rust 的 struct 包含引用（即它借用了所有权）时，必须确保对该 struct 的任何引用不会比 struct 所持有的引用活得更久。因此，Rust struct 的生命周期必须等于或短于它所包含的任何引用的生命周期。

## 高效继承

与 C++ 和 Java 重量级的继承方式不同，Rust 采取低调策略，事实上“继承”一词被刻意回避。传统继承消失了，AWOL 的类也就不再需要。从类的束缚中解脱出来，方法可定义在任何地方，类型可有任意的方法集合。与 Go 一样，Rust 中的继承已简化为共享一组方法签名（这种方式有时被称为“无类对象”）。Rust 的 Trait 将一组方法签名聚合在一起——一个 Rust 类型可实现任意一组 Trait（因此 Trait 类似 mixin）。

## 与借用检查器再搏斗

为何 Rust 的所有权系统如此难以掌握？所有权并非 Rust 语言引入的复杂性，而是编程本身的内在复杂性，与所用语言无关。不处理所有权的语言会在运行期失败（数据竞争等）。相比之下，Rust 让所有权问题显式化，使语言能在编译期尽早且大声地失败。Rust 的借用检查器就像那种初次见面时你处不来的朋友。日久天长，他们多次帮你解围之后，你才意识到他们其实有不少好品质，幸亏结识了他们。

## 外部函数接口（FFI）

Rust 另一个特别吸引我的特性是高效 C 绑定支持（从 Rust 调用 C 代码无额外开销）。高效 C 绑定支持软件的增量重写，使程序员能利用大量短期内不会消失的 C 代码。外部函数不在 Rust 保护之下，因此始终被视为不安全（注意，有许多行为如死锁与整数溢出是不希望发生的，但按 Rust 的语义并非显式不安全）。在 Rust 中，不安全动作必须放在 `unsafe` 块中。在 `unsafe` 块内，Rust 那位更野的表亲“Unsafe Rust”统治一切。“Unsafe Rust”可以打破有限的一组 Rust 常规规则，其中最重要的是允许调用外部函数。

实践中，从 Rust 调用 C 函数并不总是像教程说的那么简单。考虑调用 `dtrace_open()`（来自 libdtrace）。`dtrace_open()` 的 C 原型如下：

```c
dtrace_hdl_t *
dtrace_open(int version, int flags, int *errp)
{
}
```

要从 Rust 调用 `dtrace_open()`，先在 `extern` 块中声明 `dtrace_open()` 的签名（`extern "C"` 表示调用使用平台 C ABI）。然后可直接在 `unsafe` 块中调用该函数，如下：

```rust
extern crate libc;
…
extern "C" {
    fn dtrace_open(arg1: ::std::os::raw::c_int,
                   arg2: ::std::os::raw::c_int,
                   arg3: *mut ::std::os::raw::c_int) -> *mut dtrace_hdl_t;
}

fn main() {
    let flags = 0;
    let dtrace_version = 3;
    let mut err: libc::c_int = 0;
    let handle = unsafe {
        dtrace_open(dtrace_version, flags, &mut err)
    };
}
```

但存在一个重大问题：类型 `dtrace_hdl_t` 定义在哪里？虽然 `dtrace_hdl_t` 可手工声明，但它包含许多字段，又用到更多必须定义的新类型。手工声明这一切极为繁琐且易错。所幸有解决方案：可用 Rust 的 bindgen crate 自动生成 C 绑定（`cargo install bindgen`）。可惜 bindgen 工具不太成熟，因此常需手动调整其输出（通常是增删可变性）。鉴于 SWIG 对 Rust 的支持短期内无望，迫切需要更好的原生工具来生成 Rust 绑定。

## 包管理

吸引我转向 Rust 的最后一个、也是最重要的特性，是对现代应用软件包管理的支持。Rust 提供灵活的 crate 与模块系统，用于组织、切分软件并管理可见性。Rust crate 等同于其他语言的库或软件包，Rust 模块则切分 crate 内的代码。一个 Rust 程序通常由单个可执行 crate 组成，可选依赖一个或多个库 crate。可复用的、社区开发的库 crate 托管于 crates.io——Rust 软件包管理工具 cargo 的中央软件包仓库（crates.io 大致相当于 Python 的 PyPI）。Rust 的 cargo 工具从 crates.io 拉取项目构建依赖并管理软件构建。是啊，我知道，世界真的需要又一套打包软件、解决依赖、构建软件的机制吗？也许不需要，但 cargo 确实挺好用（不过对用惯 Maven 的人来说，门槛本来也不高）。

## 在 FreeBSD 上起步

Rust 的平台支持分为三层，各提供不同保证。x86\_64 上的 FreeBSD 目前是 Tier 2 平台，即保证能构建，但不保证能用。尽管没有保证，实践中通常都挺好用。Tier 2 平台提供 Rust 编译器 rustc 与标准库 std（`pkg install rust`）、软件包管理器 cargo（`pkg install cargo`）的官方发布。FreeBSD 的 Rust 二进制包目前（撰文时）为 v1.12，而最新稳定版是 v1.13。安装后，可执行 rustup 脚本将 Rust 更新到最新版本：

```sh
curl -sSf https://static.rust-lang.org/rustup.sh | sh
```

32 位 FreeBSD 处于 Rust 低下的第三层，既不保证能构建也不保证能用，状况相当不稳定。例如，Rust 1.13 在 ARM 平台上明明有严重的代码生成 bug（涉及硬件浮点）却照常发布。此处有龙，慎之！

## 现状如何？

Rust 于 2009 年作为 Mozilla 员工 Graydon Hoare 的个人项目起步。随后几年，Rust 转型为 Mozilla 赞助的社区项目，拥有超过 1200 名贡献者。自 2015 年 6 月发布的 1.0 版本以来，Rust 已用于多个实际部署。2016 年 6 月，Mozilla 在 Firefox 48 中为 Servo 渲染引擎交付了 Rust 代码，这是通向成熟的又一个重要里程碑。

人们在用 Rust，但它是否真正兑现了提供 C++ 安全替代方案的愿景？我认为答案基本是肯定的，尽管差异并非那么巨大。例如，在 C++ 中，unique\_ptr 拥有并管理一个对象，当 unique\_ptr 超出作用域时销毁该对象。此外，所有权可通过 std::move 转移；作为额外好处，还有使用 auto 关键字的类型推断。尽管有这些相似之处，智能指针并不能提供 Rust 所有权系统的一切。在下面的例子 \[3] 中，move 之后访问 orig 会导致运行时段错误——在 Rust 中道德等价的例子将无法编译。及早失败是好事。认真的、熟练的 C++ 程序员不会犯这样的错误，这种说法多少有些循环论证的意味，因为如果这类错误不普遍，试图阻止它们的语言根本不会存在。C++ 还缺乏模块系统，并有一些相当糟糕的特性，如头文件和文本包含。这些都是 Rust 的优势。

```cpp
#include <iostream>
#include <memory>
using namespace std;

int main () {
  unique_ptr<int> orig(new int(5));
  cout << *orig << endl;
  auto stolen = move(orig);
  cout << *orig << endl;
}
```

Rust 与 C++ 在性能上相比如何？比较惯用 C++ 和 Rust 性能的对照研究很难找到。Firefox 的 Servo 和 Gecko 渲染引擎（分别用 Rust 和 C++ 编写）之间的比较报告称，Servo 引擎的速度大约快一倍 \[1]。虽然这些数字应持保留态度，但共识是 Rust 在性能上至少与 C++ 相当。原因之一是 Rust 的真正的不可变性等特性允许进行 C++ 中无法实现的优化。Rust 的语义还为进一步优化带来巨大潜力。

尽管在生产环境中部署 Rust 取得了进展，问题仍然存在。Rust 的 ABI 不稳定；与 Glasgow Haskell 编译器一样，稳定的 ABI 可能永远不会实现，几乎可以肯定短期内不会。这个问题对 Rust 原生共享库影响最大，因为没有稳定的 ABI，它们在主要版本变更之间不兼容。ABI 不稳定并非致命问题。那么是否存在将 Rust 代码上游到 FreeBSD 的技术障碍？在我看来没有，但我有兴趣听听其他人对这样做在技术和政治层面的挑战的看法。

我喜欢 Rust。它很有趣。而这不正是让我们早上来到办公室的真正原因吗？

***

**Graeme Jenkinson** 是剑桥大学计算机实验室的高级研究员，领导 Causal, Adaptive, Distributed, and Efficient Tracing System（CADETS）项目的分布式追踪开发。在从事 CADETS 之前，他在国防和汽车行业拥有 13 年的经验。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/20161112-bian-cheng-yu-yan/its-better-to-rust-than-wear-out.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
