大家好,这里是架构资源栈 !点击上方关注,添加"星标",一起学习大厂前沿架构!
关注、发送C1
即可获取JetBrains全家桶激活工具和码!

对于大多数人来说,移植一个 6 万多行 C 语言项目到 Rust 听起来像是一项疯狂的任务。但对于 Collin Richards,这就是他过去 6 个月"园艺"般的爱好 ------ 一次充满挑战与乐趣的 Rust 探索。
他的目标是将著名的终端复用器 tmux 完整移植为 100% Rust 实现(尽管目前仍是基于 unsafe
的版本)。最终成果名为 tmux-rs,代码量达到惊人的 8 万行左右。
为什么是 Rust?
Collin 并不是为了安全性或性能而开始这个项目,而纯粹是出于兴趣。他坦言:"就像园艺一样,只不过这里是更多的段错误(segfault)。"
这个项目涵盖了:
- C 到 Rust 的翻译过程与挑战
- 如何构建与调试大型系统级项目
- C 中常见模式在 Rust 中的最佳实践
从 C2Rust 启航
项目最初使用了 C2Rust 工具,它可以将 C 代码自动转换为 Rust。虽然迁移后的代码能编译运行,但极其难以维护,体积膨胀至原来的三倍。

举个例子,以下是原始 C 代码片段:
c
int colour_palette_get(struct colour_palette *p, int c) {
if (p == NULL)
return (-1);
if (c >= 90 && c <= 97)
c = 8 + c - 90;
else if (c & COLOUR_FLAG_256)
c &= ~COLOUR_FLAG_256;
else if (c >= 8)
return (-1);
if (p->palette != NULL && p->palette[c] != -1)
return (p->palette[c]);
if (p->default_palette != NULL && p->default_palette[c] != -1)
return (p->default_palette[c]);
return (-1);
}
转换后的 Rust 代码却变成了这样:
rust
#[no_mangle]
pub unsafe extern "C" fn colour_palette_get(
mut p: *mut colour_palette,
mut c: libc::c_int,
) -> libc::c_int {
if p.is_null() {
return -1;
}
if c >= 90 && c <= 97 {
c = 8 + c - 90;
} else if c & 0x1000000 != 0 {
c &= !0x1000000;
} else if c >= 8 {
return -1;
}
if !(*p).palette.is_null() && *(*p).palette.offset(c as isize) != -1 {
return *(*p).palette.offset(c as isize);
}
if !(*p).default_palette.is_null() && *(*p).default_palette.offset(c as isize) != -1 {
return *(*p).default_palette.offset(c as isize);
}
return -1;
}
最令人头疼的是原本清晰的宏常量如 COLOUR_FLAG_256
被硬编码为十六进制数字,严重影响可读性。
于是 Collin 下定决心 ------ 手动重写所有 C 文件为 Rust!

构建系统的大改造
移植工作中最棘手的一部分是构建流程。原始 tmux
使用的是 Autotools(autogen.sh + configure + Makefile
),而 Rust 使用的是 Cargo。
初期他通过一个 build.sh
脚本调用 cargo
构建静态库,再由 make
链接生成可执行文件。
后期,为了更加合理,他直接使用 cc
crate 将残余 C 文件作为辅助库集成,主流程由 Rust 控制,结构如下:
markdown
┌──────────────┐
│ build.rs │────┐
└──────────────┘ │
▼
┌─────────────────┐
│ Cargo build │
└─────────────────┘
│
▼
┌─────────────────┐
│ tmux-rs binary │
└─────────────────┘
翻译中遇到的那些 Bug
在逐个函数迁移的过程中,Collin 遇到不少棘手 bug,其中两个最具代表性:
Bug 1:函数签名不匹配导致地址错乱
c
// 原始 C 调用
void* get_addr(client* c) {
return c->bar;
}
rust
// Rust 实现
unsafe extern "C" fn get_addr(c: *mut client) -> *mut c_void {
(*c).bar
}
问题出在调用端没有包含正确声明,C 编译器默认函数返回 int
,于是高位地址被截断。
解决方案:在 C 头文件中添加准确的函数声明。
Bug 2:结构体字段对齐错误导致崩溃
c
struct client {
int bar;
int *baz;
int foo;
};
但在 Rust 中被误写为:
rust
struct client {
bar: i32,
baz: i32, // 应该是 *mut i32
foo: i32,
}
错误的类型导致字段偏移错位,最终在访问 foo
时导致段错误。
Rust 中复刻 C 编程模式
使用裸指针代替引用
由于 Rust 的引用(&T
/ &mut T
)需要符合诸多内存安全约束,Collin 在整个迁移过程中主要使用 *mut T
来保持原始行为。
模拟 goto
虽然 goto
在现代 C 编程中被广泛避免,但 tmux
中的跳转控制流仍较常见。Collin 采用 Rust 的标签块与 break/continue
成功复刻了这类逻辑。
rust
'outer: {
if error_condition {
break 'outer;
}
do_something();
}
handle_error();
宏定义的数据结构
C 中通过宏构建的红黑树与链表结构,Collin 在 Rust 中则使用 trait + NonNull<T>
组合来实现遍历:
rust
for wl in rb_foreach(&mut (*s).windows).map(NonNull::as_ptr) {
(*(*wl).window).flags &= !WINDOW_ALERTFLAGS;
}
词法/语法分析器:从 Yacc 到 lalrpop
tmux
的配置解析器原本是用 yacc
实现的,为了彻底摆脱 C,Collin 最终使用 Rust 的 lalrpop
重写了整个语法树生成逻辑。
rust
grammar(ps: NonNull<cmd_parse_state>);
pub Lines: () = {
=> (),
<s:Statements> => unsafe {
(*ps.as_ptr()).commands = s.as_ptr();
}
};
配合原始词法器接口包装为 Rust 迭代器,最终达成全 Rust 版本的配置解析能力。
总结:从 Rust 爱好到系统重构
Collin 目前已将 tmux
代码 100% 移植为 Rust,虽然仍是 unsafe
版本、还有不少 bug,但他为社区带来了巨大的启发。
"虽然我写的代码还远不完美,但能走到这一步,已经很令人欣慰。"
他计划下一步将代码逐步转为 安全 Rust ,让 tmux-rs
不仅能用,还能放心使用。
项目地址
如果你也曾想将 C 项目重写为 Rust,却不知道从哪里开始,也许 tmux-rs
的这段旅程就是你最佳的参考。
本文由博客一文多发平台 OpenWrite 发布!