# 反向改写：把 Rust 转回 C++

## Part 2：完整绕一圈 —— uutils/coreutils

*Su Qingyue*

---

### 问题怎么继续往下走

第一篇里，hexyl 这个例子已经说明了一件事：把一个结构清楚的 Rust CLI 工具转成现代 C++，大部分地方并不会立刻撞墙。

但 hexyl 还是太干净了。它是单个作者主导的项目，边界清晰，架构也比较整洁。真正更有意思的问题是：

**如果我们处理的是一个本身就来自“旧 C 程序重写”的 Rust 项目，会发生什么？**

[uutils/coreutils](https://github.com/uutils/coreutils) 给了一个很好的观察窗口。对于同一个工具，我们可以同时看到三层材料：

1. 更早的 GNU C 实现
2. uutils 的 Rust 重写
3. 这次从 Rust 反向转写出来的 C++

如果表达力真的是关键差别，那么这里本来应该最容易卡住。反过来，如果真正大的差别在别处，这个“三方对照”会更容易把它照出来。

### 选了哪三个工具

| 工具 | 做什么 | 复杂度 | 主要看点 |
|------|--------|--------|----------|
| `echo` | 打印参数 | 简单 | 自定义 flag 解析、POSIXLY_CORRECT |
| `cat` | 拼接文件输出 | 中等 | 行号、不可打印字符表示、缓冲 I/O |
| `tr` | 字符转换 / 删除 | 偏复杂 | 集合语法、POSIX 类别、多个操作组合 |

### echo：最轻的一例

`echo` 规模很小，但它已经能看出一些生态层面的差别。

GNU C 版本 286 行，Rust 版本 256 行，转写后的 C++ 版本 236 行。

真正有意思的，不是 `echo` 本身有多难，而是 Rust 版本为了落进 uutils 的统一框架，会自然引入一些抽象：

- `clap`
- `uucore` 里的转义处理
- `OsString` 到字节的转换

在 Unix 目标上，这些东西到了 C++ 往往会塌成非常直接的实现：`argv` 本来就是字节序列，几十行就够写完转义处理。也就是说，这里减少的行数并不是“算法更高明”，而是某些为统一性、跨平台性、框架化而存在的层，在当前目标下不再需要。

### cat：真正开始有对照价值

`cat` 是这个对比里最舒服的一例，因为三种版本都很有代表性。

**GNU C** 基本是一种经典 Unix 工具的写法：一个大 `main()`，分两条路径处理普通复制和格式化输出，大量手工 buffer 管理。行号递增用的是 ASCII 数字位进位，这种做法很老，也很聪明。

**Rust** 则把结构分得更清楚：快路径一套、格式化路径一套、错误类型单独列出来，输出依赖 `BufWriter`，扫描换行用了 `memchr2`。算法其实没变，只是组织方式现代得多。

**C++** 基本沿着 Rust 的结构走，但换成 C++ 习惯的表达：自己包一个 `OutputBuffer`，命令行用 `getopt_long`，错误直接打印，结构比 GNU C 清楚，又比 Rust 少一层 trait 和错误类型体系。

这里最值得记的一点是：

**行号递增、不可打印字符的 `M-^X` 表示、`\r\n` 的处理、空行压缩这些核心算法，在三种语言里几乎是同一件事。**

### tr：压力更大，但结论没突然翻过来

`tr` 是三个工具里最复杂的。GNU C 版本接近 1900 行，Rust 版本约 1100 行，转写后的 C++ 版本大约 700 行。

这里最大的行数变化，主要来自两个地方：

**第一，`nom`。** Rust 版本用 parser combinator 来定义字符集合语法，组合性很好，但也更重。C++ 里直接写递归下降解析器，针对 `tr` 这种小语法，反而更短。

**第二，trait 层次。** Rust 为了把不同操作模式统一起来，引入了几层 trait 和组合结构。C++ 这边直接按 5 种操作模式分几个函数，再用 switch 调度，行为一样，结构更直。

换句话说，`tr` 看起来更像是在比较：

- 一种面向库复用、组合和可测试性的 Rust 写法
- 一种面向当前问题、直接收束的 C++ 写法

而不是在比较“哪门语言才能表达这个逻辑”。

### 三方数字

| 指标 | GNU C | Rust | C++ |
|------|-------|------|-----|
| 总行数 | 3,021 | 2,217 | 1,530 |
| 外部依赖 | autoconf / gnulib | 8 个 crate | 无 |
| 构建系统 | autotools | Cargo | CMake |
| 行为测试 | n/a | n/a | 66/66 |
| 行为差异 | — | — | 未观察到 |

这里有个必要的说明：GNU C 的行数包含大量注释、历史兼容代码和构建相关材料，所以更适合把它当作“成熟 Unix 工具的现实形态”来比较，而不是把数字直接和 Rust / C++ 摆成一条纯粹的简洁度排行榜。

### 这轮转写里，常见的映射

| Rust（coreutils） | C++ | 说明 |
|-------------------|-----|------|
| `uucore::fast_inc_one` | 内联的数字进位逻辑 | 与 GNU C 思路一致 |
| `nom` 组合子 | 手写递归下降解析 | 小语法下更直接 |
| `thiserror` derive | `fprintf(stderr, ...)` | 去掉错误类型层次 |
| `memchr2` | 简单扫描循环 | 这里不需要额外 crate |
| `SymbolTranslator` trait | 直接函数分派 | 操作模式本来就有限 |
| `BufWriter<StdoutLock>` | `OutputBuffer` | 语义类似 |
| `OsString` / `os_str_as_bytes` | `argv` / `const char*` | Unix 下直接就是字节 |
| `clap` | `getopt_long` | 标准 POSIX 方案 |

### Rust 重写真正带来的东西

把三方放在一起看，最重要的感受其实不是“Rust 发明了新的算法”，而是它把几个工程属性抬得更稳：

**1. 内存安全。** GNU C 版本里大量指针运算和手工边界控制，在 Rust 里会被类型系统和借用规则天然约束住。

**2. 错误路径更清楚。** Rust 版本会把错误情况做成明确的类型和分支，而不是散落的打印语句。

**3. 平台差异更整洁。** Rust 用 `#[cfg]` 这类机制把平台特例隔开，组织方式比 C 时代的 `#ifdef` 更干净。

**4. 测试材料更完整。** Rust 版本自带的单元和集成测试，让“转回去之后还能不能对上”这件事有了更可靠的抓手。

这些都很重要。但它们更像安全性和工程组织层面的收益，而不是“只有 Rust 才能表达这些算法”。

### 这一轮对照让我更倾向于怎么说

GNU C → Rust 的那一步，看起来像是：

- 算法基本没变
- 结构更清楚了
- 编译器开始替程序员承担一部分安全检查

Rust → C++ 的那一步，则更像：

- 算法仍然没变
- 一些为了编译器验证服务的类型层抽象塌掉了
- 剩下的是一个更直接的、但也更少保护的版本

所以到第二篇为止，我能接受的表述还是那句：

**这里最明显的差别，不在“程序到底能不能写出来”，而在“工具链会替你检查到什么程度”。**

### 下一篇

前两篇都还是顺序程序。第三篇会去看最应该卡住的地方：异步和并发。对象是 tokio-rs 的 `mini-redis`。如果前面的判断站不住，那里最容易先露出来。

---

*这三组转写的完整材料在 [`02-coreutils`](../02-coreutils/)。当前整理出的行为测试一共 66 个，C++ 版本和系统工具输出一致。*
