从 C 到 Rust:一位开发者的 `tmux` 全面移植之旅

大家好,这里是架构资源栈 !点击上方关注,添加"星标",一起学习大厂前沿架构!

关注、发送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 发布!

相关推荐
Dcs11 分钟前
用不到 1000 行 Go 实现 BaaS,Pennybase 是怎么做到的?
java
Cyanto2 小时前
Spring注解IoC与JUnit整合实战
java·开发语言·spring·mybatis
qq_433888932 小时前
Junit多线程的坑
java·spring·junit
gadiaola2 小时前
【SSM面试篇】Spring、SpringMVC、SpringBoot、Mybatis高频八股汇总
java·spring boot·spring·面试·mybatis
写不出来就跑路2 小时前
WebClient与HTTPInterface远程调用对比
java·开发语言·后端·spring·springboot
Cyanto2 小时前
深入MyBatis:CRUD操作与高级查询实战
java·数据库·mybatis
麦兜*3 小时前
Spring Boot 集成Reactive Web 性能优化全栈技术方案,包含底层原理、压测方法论、参数调优
java·前端·spring boot·spring·spring cloud·性能优化·maven
天上掉下来个程小白3 小时前
MybatisPlus-06.核心功能-自定义SQL
java·spring boot·后端·sql·微服务·mybatisplus
知了一笑3 小时前
独立开发第二周:构建、执行、规划
java·前端·后端
今天背单词了吗9803 小时前
算法学习笔记:17.蒙特卡洛算法 ——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·笔记·考研·算法·蒙特卡洛算法