为什么我要用 Rust 重写 tmux?

介绍 tmux-rs

在过去的 6 个月里,我一直在悄悄地将 tmux 从 C 语言移植到 Rust 语言。最近,我达到了一个重要的里程碑:代码库现在 100% 都是(不安全的)Rust 语言。我想分享一下将原始代码库从大约 67,000 行 C 代码移植到大约 81,000 行 Rust 代码(不包括注释和空行)的过程。你可能会问:为什么用 Rust 重写 tmux?是的,我其实没有很好的理由。这是一个业余爱好项目。就像园艺一样,只是会遇到更多的段错误。

从 C2Rust 开始

我开始这个项目是为了尝试 C2Rust,这是一个 C 到 Rust 的转译器。这个工具的设置有些复杂,但一旦运行起来,生成的输出就是 tmux 代码库在 Rust 上的成功移植。

尽管生成的代码能够运行,但它基本上无法维护,且比原来的C代码大了3倍。你不会想碰它。以下是一个输出示例:

// original C code
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 & y)
    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);
}
// generated Rust code
#[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 as libc::c_int);
    }
    if c >= 90 as libc::c_int && c <= 97 as libc::c_int {
        c = 8 as libc::c_int + c - 90 as libc::c_int;
    } else if c & 0x1000000 as libc::c_int != 0 {
        c &= !(0x1000000 as libc::c_int);
    } else if c >= 8 as libc::c_int {
        return -(1 as libc::c_int)
    }
    if !((*p).palette).is_null()
        && *((*p).palette).offset(c as isize) != -(1 as libc::c_int)
    {
        return *((*p).palette).offset(c as isize);
    }
    if !((*p).default_palette).is_null()
        && *((*p).default_palette).offset(c as isize) != -(1 as libc::c_int)
    {
        return *((*p).default_palette).offset(c as isize);
    }
    return -(1 as libc::c_int);
}

这段代码还算不错,但情况可能会糟糕得多。我主要担心的是丢失命名常量中的信息,例如COLOUR_FLAG_256被翻译为0x1000000。此外,还有大量将类型转换为libc::c_int的操作,这会污染代码。我猜这是为了处理 C 的整数提升规则。在 Rust 中对文字进行操作时,这些转换大多完全没必要。元素周期表

我花了很多时间手动将糟糕的 Rust 代码重构为不太糟糕的 Rust 代码,但我发现自己还是得看原始的 C 代码才能理解程序的意图。在以这种方式手动重构了许多文件后,我放弃了这种方法。我扔掉了所有的 C2Rust 输出,决定将所有文件从 C 手动翻译成 Rust。

尽管这个项目没有使用 C2Rust,但我仍然认为它是一个很好的工具。对我来说,能够从一开始就编译和运行这个项目是非常重要的。这让我意识到这个努力是可行的。我甚至将其作为我其他副项目的一部分进行了整合。

构建过程

┌─────────────┐    ┌────────────┐     ┌──────────────┐    ┌──────────┐         ┌───────┐    
│ Makefile.am │───►│ autogen.sh ├────►│ configure.sh │───►│ Makefile │         │ cargo │    
└─────────────┘    └────────────┘     └──────────────┘    └──────────┘         └───┬───┘    
                                                                                   │        
                                                                                   │        
                                ┌──────┐       ┌──────┐                            │        
                           ┌───►│tmux.c├──────►│tmux.o├───────┐                    │        
               ┌──────┐    │    └──────┘       └──────┘       │                    │        
               │tmux.h├────┤                                  │                    │        
               └──────┘    │  ┌────────┐     ┌────────┐       │                    │        
                           ├─►│window.c├────►│window.o├───────┤                    │        
              ┌────────┐   │  └────────┘     └────────┘       │                    │        
              │compat.h├───┤                                  │                    │        
              └────────┘   │    ┌──────┐       ┌──────┐       │                    │        
                           └───►│pane.c├──────►│pane.o├───────┤                    ▼        
                                └──────┘       └──────┘       │             ┌──────────────┐
                                          ┌───────────┐       │    ┌────┐   │              │
                                          │           │       ├───►│tmux│◄──┤ libtmux_rs.a │
                                          │ libc.so.6 ├───────┤    └────┘   │              │
                                          │           │       │             └──────────────┘
                                          └───────────┘       │                             
                                      ┌───────────────┐       │                             
                                      │               │       │                             
                                      │ libtinfo.so.6 ├───────┘                             
                                      │               │                                     
                                      └───────────────┘                                     

这次重写最重要的部分是首先对项目的构建方式有深刻的理解。对于 tmux 来说,这是 autotools。我弄清楚了在 autogen.sh 中添加/删除文件的位置,以及如何修改生成的 Makefile,以使用 crate-type = “staticlib” 选项链接由我的 rust crate 创建的静态库。

这确实意味着我的构建过程并不像只是运行 cargo build 那么简单。我编写了一个小的 build.sh 脚本,它会调用 cargo,然后运行 make。这暂时可行,但每次完成文件的翻译后,我必须重新配置和修改 Makefile

早期,我尝试将内容分成多个小型 crate。最终,将所有内容放在同一个 crate 中更容易,原因有两个:1. Crates 不能有循环依赖关系;2. 将多个 Rust 库链接到同一个二进制文件时,可能会遇到链接问题。

最初,我每次只翻译一个文件,且无法在文件翻译过程中验证更改。在翻译一个大型文件并陷入调试困境后,我调整了开发流程,改为每次只翻译一个函数,并在每次翻译后快速执行 build.sh run 以确保一切正常。这意味着需要在 C 代码中为原先静态的函数添加额外头文件。新流程如下:

  • 复制 C 函数的头文件
  • 将 C 函数体注释掉
int colour_palette_get(struct colour_palette *p, int c);
// int colour_palette_get(struct colour_palette *p, int c) {
// ...
//
  • 在 Rust 中实现该函数

只要函数具有 #[unsafe(no_mangle)] 属性 extern “C” 注释,并且重要的是具有正确的签名,C 代码就会与 Rust 实现链接。

在翻译了大约一半的 C 文件后,我开始觉得当前的构建过程有些愚蠢。现在,大部分代码都在 Rust 中。与其构建 C 二进制文件并链接到 Rust 库,我应该构建 Rust 二进制文件并链接到 C 库。这正是使用 cc crate 可以做到的。

我设置了一个这样的 build.rs

// simplified version of tmux-rs/build.rs
fn main() {
    println!("cargo::rerun-if-changed=build.rs");
    println!("cargo::rustc-link-lib=bsd");
    println!("cargo::rustc-link-lib=tinfo");
    println!("cargo::rustc-link-lib=event_core");
    println!("cargo::rustc-link-lib=m");
    println!("cargo::rustc-link-lib=resolv");

    let mut builder = &mut cc::Build::new();

    static FILES: &[&str] = &[
        "osdep-linux.c",
        "cmd-new-session.c",
        "cmd-queue.c",
        // ...
        "window-customize.c",
        "window-tree.c",
    ];
    for f in FILES {
        builder = builder.file(std::path::PathBuf::from("..").join(f))
    }

    builder.compile("foo");
}

有趣的错误

在翻译代码过程中引入了许多错误。我想分享发现并修复其中几个错误的过程。

错误 1

翻译一个简单的函数后,程序开始发生段错误。源代码和翻译后的代码如下:

void* get_addr(client* c) {
  return c->bar;
}
unsafe extern "C" fn get_addr(c: *mut client) -> *mut c_void {
  unsafe {
    (*c).bar
  }
}

在调试器中运行后,错误信息如下:Invalid read at address 0x2764

我再次仔细检查了代码。在 Rust 函数 (*c).bar 中,有一个有效的地址,如 0x60302764,但在函数之外,从调用 C 代码接收到的值是 0x2764。你已经知道问题所在了吗?需要另一个提示吗?如果我仔细查看 C 编译警告,就会发现:

warning: implicit declaration of function ‘get_addr’ [-Wimplicit-function-declaration]

没错,C 代码使用了隐式声明,即:

int get_addr();

这解释了为什么值是错误的!C编译器认为返回的是一个4字节的整数,而不是一个8字节的指针。因此,前4字节被截断或忽略。解决方法非常简单,只需在C代码中添加正确的原型声明,编译器就会生成正确的代码。

错误 2

我再次在翻译一个本不应引发问题的简单函数时发现了这个错误。它大致如下:

void set_value(client* c) {
  c->foo = 5;
}
unsafe extern "C" fn set_value(c: *mut client) {
  unsafe {
    (*c).foo = 5;
  }
}

翻译完这个简单的函数后,程序开始出现段错误,这让我感到非常震惊。在调试器中检查后发现,Rust 代码中的段错误发生在该行,这应该与 C 代码相同。在调试器中,我注意到 C 代码中的地址与 Rust 代码中的地址略有不同,也许只是地址随机化造成的。

那么问题出在哪里呢?原来,当我手动翻译客户端结构体的类型声明时,我错过了一个类型的 *。这个类型就在数据字段的上方。这意味着 C 和 Rust 代码在那个不匹配的字段之后对类型的看法不同。

例如,C 结构体看起来像这样:

struct client {
  int bar;
  int *baz;
  int foo;
}

而 Rust 看起来像这样:

struct client {
  bar: i32,
  baz: i32,
  foo: i32,
}

Rust 中还没有触及 baz,因此没有编译器错误,但数据会被错误地解释和访问。这次的修复非常简单,只需更正 Rust 代码中的错误类型即可。

Rust 中的 C 模式

原始指针

Rust 有两种引用类型:&T:共享引用,或 &mut T:独占(或可变)引用。Rust 引用是一个具有其他几个不变量的指针。其中一个不变量是,Rust 引用永远不能为空,且所指向的值必须完全初始化且有效。

C 程序中的指针在 Rust 中自然对应的是引用,根据代码中的修改情况,可以是独占引用或共享引用。问题是,如果我们将 C 源代码直接与 Rust 进行一对一映射,Rust 中引用所需的一些不变量往往无法始终得到维护。这意味着我们目前还无法在移植版本中使用 Rust 引用。我们必须使用另一种类型,即原始指针:*mut T*const T。从语义上讲,原始指针与 C 指针相同,但由于你不会在不安全的 Rust 之外使用它们,因此它们的使用非常不方便。

考虑使用 goto

C 语言有 goto 语句。goto 通常被认为是低效的,但实际上在 tmux 代码库中的使用相当温和,只有一两处使用会导致实现困难。

c2rust 转换器使用一种算法来模拟 goto 逻辑。一个描述类似算法的优秀视频可在此 视频 中找到。然而,大多数情况下并不需要使用该算法,而是可以采用一种更简单的方法。

  • 前向跳转可通过带 break 语句的标记块实现:
fn foo() {
  'error: {
    println!("hello");

    if random() % 2 == 0 {
      break 'error; // same as goto error in C
    }

    println!("world");
    return;
  } // 'error:
  println!("error");

}
  • 向后跳转可通过带 continue 语句的标记循环实现:
fn bar() {
  'again: loop {
    println!("hello");

    if random() % 2 == 0 {
      continue 'again; // same as goto again in C
    }

    println!("world");
    return;
  }
}

这些是 tmux 代码库中 goto 语句最常见的用法。仅有少数更复杂的 goto 场景需要我动用纸笔来推导控制流映射(如对 window_copy_search_marks 感兴趣可查阅代码库)。

侵入式宏

tmux 廣泛使用透過宏定義的兩種資料結構:侵入式紅黑樹和連結清單。侵入式資料結構是指資料結構的各個部分直接存在於結構體內。這與當今大多數容器資料結構的實現方式不同,後者由容器持有未修改的結構體,且不需要結構體提供支援來存放集合中的資料。

我经过多次迭代,最终实现了模仿 C 代码的良好 Rust 接口。最终结果如下:

// cmd-kill-session.c
RB_FOREACH(wl, winlinks, &s->windows) {
  wl->window->flags &= ~WINDOW_ALERTFLAGS;
  wl->flags &= ~WINLINK_ALERTFLAGS;
}
// cmd_kill_session.rs
for wl in rb_foreach(&raw mut (*s).windows).map(NonNull::as_ptr) {
    (*(*wl).window).flags &= !WINDOW_ALERTFLAGS;
    (*wl).flags &= !WINLINK_ALERTFLAGS;
}

如果我没有从迭代器返回 NonNull,代码实际上会更简洁。为了模仿这个接口,我实现了自己的特性。其中一个挑战是,某些实例可以同时存在于不同的容器中。这会带来问题,因为一个特性只能对给定类型实现一次。解决方案是使特性成为通用的,这样它就不是单个特性,而是多个特性,具体取决于通用参数。当需要区分代码中使用哪个 trait 时,我使用了一个虚拟单元类型。以下是实现与 C 语言非常相似的漂亮接口的丑陋代码:

pub trait GetEntry<T, D = ()> {
    unsafe fn entry_mut(this: *mut Self) -> *mut rb_entry<T>;
    unsafe fn entry(this: *const Self) -> *const rb_entry<T>;
    unsafe fn cmp(this: *const Self, other: *const Self) -> std::cmp::Ordering;
}

pub unsafe fn rb_foreach<T, D>(head: *mut rb_head<T>) -> RbForwardIterator<T, D>
where
    T: GetEntry<T, D>,
{
    RbForwardIterator {
        curr: NonNull::new(unsafe { rb_min(head) }),
        _phantom: std::marker::PhantomData,
    }
}
pub struct RbForwardIterator<T, D> {
    curr: Option<NonNull<T>>,
    _phantom: std::marker::PhantomData<D>,
}

impl<T, D> Iterator for RbForwardIterator<T, D>
where
    T: GetEntry<T, D>,
{
    type Item = NonNull<T>;
    fn next(&mut self) -> Option<Self::Item> {
        let curr = self.curr?.as_ptr();
        std::mem::replace(&mut self.curr, NonNull::new(unsafe { rb_next(curr) }))
    }
}

Yacc 剃须

Tmux 使用 yacc 为其配置语言实现了一个自定义解析器。我之前知道 lexyacc,但从未使用过它们。将项目从 C 转换为 Rust 的最后一步是弄清楚如何将 cmd-parse.y 中的解析器从 yacc 重新实现为 Rust。完成此步骤后,我就可以完全抛弃 cc crate 并简化构建过程了。

经过一两次使用不同库的尝试后,我最终决定使用 lalrpop 库来实现解析器。lalrpop 的代码结构与 yacc 非常相似,这使得我能够像项目中的其他部分一样,进行一对一的重新实现。

原始的 yacc 解析器如下所示:

lines		: /* empty */
		| statements
		{
			struct cmd_parse_state	*ps = &parse_state;

			ps->commands = $1;
		}

statements	: statement '\n'
		{
			$$ = $1;
		}
		| statements statement '\n'
		{
			$$ = $1;
			TAILQ_CONCAT($$, $2, entry);
			free($2);
		}

这是一个包含一系列规则匹配时执行的动作的语法。该语法的对应部分可转换为以下 lalrpop 代码片段。

grammar(ps: NonNull<cmd_parse_state>);

pub Lines: () = {
    => (),
    <s:Statements> => unsafe {
      (*ps.as_ptr()).commands = s.as_ptr();
    }
};

pub Statements: NonNull<cmd_parse_commands> = {
    <s:Statement> "\n" => s,
    <arg1:Statements> <arg2:Statement> "\n" => unsafe {
      let mut value = arg1;
      tailq_concat(value.as_ptr(), arg2.as_ptr());
      free_(arg2.as_ptr());
      value
    }
};

lalrpop 存在一些缺陷,例如无法正确处理原始指针(* 符号似乎会干扰解析器),这没关系,我最终在所有相关位置都使用了 NonNull

在重新实现语法后,我还必须实现一个适配器,以将 lalrpop 与自定义词法分析器连接起来。词法分析器与原始代码库中的相同,只是被包裹在 Rust 迭代器中。令我惊讶的是,词法分析器与解析器连接后,似乎就直接起作用了。最后一步使我能够删除所有剩余的 C 代码和头文件。

开发过程

Vim

在整个项目开发过程中,我使用了多种不同的文本编辑器和集成开发环境(IDE)。我的典型工作流程是使用 neovim,并大量依赖自定义宏来加速翻译过程。例如,我为以下操作创建了 Vim 宏:

  • ptr == NULL 转换为 ptr.is_null()
  • ptr->field 转换为 (*ptr).field

这些机械性修改虽然单独操作非常简单,但通过查找替换一次性完成所有修改却非常困难。这意味着需要手动重复操作数千次。

AI 工具

我在开发过程的后期开始尝试使用 Cursor。但我最终停止使用它,因为我觉得它并没有真正提高我的速度。它只是让我免受手指疼痛。这是因为在使用Cursor翻译代码时,它仍然会偶尔插入错误,就像我一样。因此,我花在审查生成的代码上的时间,与我自己编写代码所需的时间相当。它唯一节省的是我的双手。进行如此大规模的重构对手指来说真的很辛苦。

尽管我已停止使用C2Rust,但若手部疼痛到无法继续工作,我仍会考虑使用它。通常当手指磨出水泡时,我认为还是休息一下比较好。鉴于AI工具的发展速度,我不会惊讶于通过其他方法能显著缩短该项目完成时间。

结论

尽管代码现在已经完成了 100%,但我并不确定自己是否已经实现了主要目标。我手动翻译的代码并不比 C2Rust 的输出好多少。而且,它很容易崩溃,我发现了许多错误。下一个目标是将代码库转换为安全的 Rust。

尽管如此,我还是发布了 0.0.1 版本,与其他 Rust 和 tmux 粉丝分享。如果您对这个项目感兴趣,可以通过 Github 讨论 与我联系。请参阅README中的安装说明。

本文文字及图片出自 Introducing tmux-rs

共有 219 条讨论

  1. htvnjf 对这篇文章的反应是俺的神呀
  2. 这是一篇关于一项真正具有里程碑意义的努力的精彩文章。我对作者的坚持不懈深表敬意。那句“就像园艺,但有更多段错误”真的让我感同身受。正是这种深入研究的业余项目才能让你学到最多。

    与`c2rust`的经历尤其有趣。这让我想起多年前在其他语言间自动代码转换器中看到的类似转变。它们确实能帮助项目起步并验证可行性,正如作者所发现的,但最终生成的代码往往完全不符合目标语言的“惯用风格”。尽管放弃一切转而进行手动移植的决定令人心痛,但这是正确的抉择。你无法将原始 C 代码的“意图”自动转换为安全、符合惯例的 Rust 代码。

    “有趣的错误”部分让我回忆起往事。错误 2 由于缺少 `*` 导致结构布局不匹配,这是典型的 FFI(外部函数接口)噩梦。我曾花了一周时间调试一个类似的 C++ 和 C# 问题,其中结构体对齐方式的单一更改以非常微妙的方式静默破坏了下游数据。这是那种让你怀疑自己理智的 bug。找到它需要极强的调试毅力,因此向作者致敬。

    这个项目是现代化关键基础设施代码的现实挑战的绝佳案例研究。作者提到下一个重大目标是将代码库从“不安全”转换为安全的 Rust。我对这一策略非常好奇。

    将原始指针和复杂的控制流(如“goto”模式)重构为安全、符合惯例的 Rust,同时又不破坏一切,这似乎比最初的移植更具挑战性。是否会采取按模块逐步引入生命周期和借用检查器的策略?对于侵入式数据结构的处理计划是什么?用标准库集合如BTreeMap替换它们是显而易见的选择,但我担心这是否会带来性能问题,而原有的侵入式设计正是为了避免这类问题。

    无论如何,这是令人惊叹的工作。感谢您如此详细地分享这一过程。我一定会关注这个项目在 GitHub 上的进展。

      1. 我很抱歉,chickenzzzzu,我恐怕无法做到。

  3. 这个公告引起了我的注意。

    几年来,我一直在开发一个基于 Rust 的 tmux 会话管理器,名为 rmuxinator(即 tmuxinator 克隆版)。它(基本上)可以正常运行,但进展缓慢,因为……生活,但我最近又重新开始修复一些错误。我添加的最新功能之一是能够将 rmuxinator 作为其他 Rust 程序的库使用。我打算尝试分叉 tmux-rs,将 rmuxinator 作为依赖项添加,并看看是否能……直接通过项目配置文件启动会话。我绝对不主张将 rmuxinator 添加到上游,但如果能将这种会话模板功能直接内置到“终端多路复用器”本身,那将非常棒。

    另一个我能预见的有意思的可能性是反过来操作,让 rmuxinator 作为库使用 tmux-rs 来设置和管理会话,而不是仅仅输出 shell 命令——这会遇到很多边界情况。(不确定 tmux-rs 目前是否支持这种方式。)

    一旦我完成了目前正在处理的错误修复,我可能会分叉这个项目,尝试上述一种或两种方法。

    无论如何,richardscollin 做得很好!

  4. _>你可能会问:为什么用 Rust 重写 tmux?是的,我确实没有很好的理由。这是一个业余项目。就像园艺,但伴随着更多的段错误。

    我喜欢这种态度。我们不需要理由来构建新事物。谁知道一个业余项目会带来什么。感谢作者的精彩文章!

    此外,我的园艺充满了段错误,编写新项目对我的花园来说肯定更安全。

    1. 完全同意。并不是每个项目都必须改变世界。

      我最近用 Rust 重写了 `fzf` [1]。我这样做有什么特别的原因吗?没有,其实没有,普通的 `fzf` 已经很好了,但我认为这是学习模糊搜索算法的工作原理和如何利用 Rust 中的通道的一个有趣的借口。这很有趣。毫无疑问,原版的fzf更好,但那不是重点,重点是玩弄技术并学习。

      [1] https://github.com/Tombert/rs-fzf-clone

      1. 不错,我认为 fzf 确实是一个很好的候选对象,如果用 Rust 编写的话会更好。fzy[1] C 重写版本非常快,但我在搜索 bash 历史记录时,无法获得有用的结果。

        [1] jhawthorn/fzy: :mag: 终端上的简单快速模糊搜索工具 https://share.google/TBp3pVaFngBTfaFyO

        1. 是的,我认为 Rust 有些道理,而且我确实做了一些聪明的事情,比如利用“分数”数量是离散且有限的事实来获得线性时间的“排序”[1],以及通过取索引值并将其明确移到源通道来避免额外的复制,从而避免复制[2])。

          比我更聪明且更熟悉 TUI 编程的人几乎肯定能改进我写的内容;我只在觉得有趣的时候才花时间在上面。我用它来实现我自制的程序启动器 Sway,不过大多数人可能用真正的 fzf 会得到更好的结果。

          [1] https://github.com/Tombert/rs-fzf-clone/blob/main/src/helper… [2] https://github.com/Tombert/rs-fzf-clone/blob/main/src/proces…

    2. “园艺是成为哲学家的最方便的借口。”——雷·布拉德伯里,《蒲公英酒》

    3. 我原本打算发表一条尖刻的评论,因为每当有人暗示 Rust 应用程序本质上比 C 更好时,我都会做出下意识的反应。有时我会忘记人们做事情是为了乐趣。

    4. 说实话,我经常讨厌别人问“为什么?”,不明白“为了乐趣”是个正当的答案。我理解工作或其他事情,但爱好?我们做很多事情都是为了乐趣!人类天生就是为了玩耍,就像其他动物一样。这是我们学习和探索周围世界的方式。

      坦率地说,引用克努特的话

        > 事实上,我希望看到成千上万的计算机科学家被放任自由,做他们想做的事。这才是真正推动领域进步的方式。
      

      这一点适用于任何领域或任何项目。我们是富有创造力的生物。我们梦想并探索。重大变革几乎从未来自于沿用旧有方式。很多时候,“仅仅因为”能让你获得尝试新事物、挑战既有范式的自由。奇怪的是,如果你总是必须为每件事辩解,反而会阻碍进步。

      1. 我仍然认为,作为一名初级软件工程师,阻碍我成长的最大问题是过度思考个人项目和所用语言的选择,而不是单纯地构建那些我认为有趣或令人着迷的东西。

        1. 你培养了对编程语言和技术的兴趣

          其他人则使用最愚蠢的JavaScript来推出产品,并真正专注于产品和营销

          还有一些人则尽量远离计算机,专注于更人性化的活动

          你只需做你自然倾向于做的事情

        2. 这总是很难判断。但你是否考虑过你可能在用错误的方式衡量?

          对我来说,这听起来像是你学到了一堂非常重要的课,而有些人似乎永远都学不到。

          我认为,做事情“仅仅因为”的最有益方面之一是你在过程中获得的其他技能或信息。如果你过于专注于更具体事物的进展,很容易错过所有这些进步。但这并不意味着这些不是进步。所以不要因此贬低自己,因为我确信你学到了很多。你能够回过头来看到更好的方法,正是因为你取得了这些进步并学到了这些教训。这些是导师通常可以帮助你更快克服的,但并非每个人都有导师或能接触到导师。但不要因为没有完成项目或因为你与他人做法不同就贬低自己的进步

      2. 我怀疑他们明白“为了乐趣”是一个合理的答案。问题可能在于他们看不到你的爱好如何能带来乐趣。

      3. 询问“为什么”仍然是一个合理的问题,“为了好玩”也可能是一个合理的答案。

        我对项目有不同的处理方式:如果他们想推出产品、替换现有的开源工具、为了自己好玩,或是作为业余项目。

        1. 抱怨的重点不在于被问“为什么”,而在于“为了好玩”这种理由不被接受。

          后续问题完全没问题,但上下文不同,对吧?

          如果这种理由不被接受,那么语气会变得负面,问题会聚焦于实用性,通常是他们“试图帮助你”找到实用性。

          如果这种理由被接受,他们会问你兴趣所在以及你在学习什么。有时这可能会转向实用性,但那更自然。

          说实话,这更多是文化问题。有些人就是有毒,如果事情在他们脑海中没有意义,那么在任何人的脑海中都没有意义。

      4. 享受乐趣的方式不止一种。某些享受乐趣的方式会为他人带来比其他方式更大的价值。

    5. > 就像园艺,但有更多段错误。

      有趣,我刚接触Rust。你为什么需要使用unsafe?

      1. C 允许你做很多事情(即使进入未定义行为领域),但这些事情根本无法用 C 编译。正如作者所说,指针和 Rust 的引用在语义上存在差异。

        C 指针可以拥有任意多个所有者,可以进行数学运算,并且可以无错误地转换为任何类型。编译器会假设你知道自己在做什么。如果你启用了足够多的编译器警告,它可能会警告你,但 C 编译器默认不会生成太多此类警告。

        Rust 只允许你一次生成一个可变(独占)引用。这意味着直接将 C 代码移植到 Rust 上是无法编译的。

        通过切换到与 C 指针非常相似的指针,你可以更轻松地移植代码,但当然也会失去 Rust 安全机制带来的好处,因为大多数指针操作都会抛弃 Rust 提供的所有安全保证。

        1. > Rust 只允许您一次生成一个可变(独占)引用。

          安全的 Rust 包括许多形式的共享可变性,包括 Cell<>,它可能是与 C 代码中的典型模式最接近的比较。

        2. 那么,不安全的 Rust 实现有什么优势呢?只是为了做到这一点吗?

          1. 它被重写成另一种语言,许多人认为 Rust 更易于阅读,它对 IDE 等有更好的类型提示支持。此外,您不会失去所有安全性,仍然有许多规则需要遵守,例如安全链接、无未定义函数等。不安全的 Rust 意味着所有进行非法指针操作的代码部分都明确标记为“不安全”关键字。现在,您可以逐一修复它们。

          2. 一旦代码库全部转换为 Rust,您就可以开始引入 Rust 的惯用语和模式了。基本上,第一步是在 Rust 中使其正常运行,第二步是清理 Rust,以真正发挥该语言的优势。

          3. 文章中写道:“下一个目标是将代码库转换为安全的 Rust。”

            1. Rust 就像是在雷区中行走,所有的地雷都为你标记好了。你可以慢慢挖出地雷,并清除标记。或者至少知道要小心避免踩到它们。

              在 C 中,你只能看到人们知道或记得放置的标记。还有许多地雷没有被标记,有时你会踩到它们。

              对我来说,哪一种更安全显而易见,但我却不明白为什么其他人看不出来。

        3. 嗯……我喜欢编译器不干涉我,假设我知道自己在做什么。这就是我不喜欢 Rust 的原因。但如果你喜欢它,那就尽情享受吧!

          1. “嗯……我喜欢汇编器不干涉我,假设我知道自己在做什么。这就是我不喜欢 C 语言,而更喜欢汇编器的原因。但如果你喜欢它,那就尽情享受吧!”

            1. C 代码比汇编和 Rust 都短,但它们并不相同。

      2. 我猜将 C 移植到不安全的 Rust 比移植到安全的 Rust 要容易得多。

    6. “我们并不一定需要一个理由来构建新事物。”

      但 tmux 并不是新事物

      用其他语言重写软件是否一定需要一个理由?

        1. 我真的不知道 tmux 是如何在屏幕上获得如此多的关注的。它在任何方面都不明显更好。也许屏幕只是名字不好?

          1. Screen很长时间都无法实现垂直分割。当屏幕变得更大更宽时,这个问题开始变得更加突出。我认为这就是我开始使用tmux的原因。tmux还提供了更多的自动化功能。如今,screen主要处于维护模式,而我已经习惯了tmux,所以没有理由切换回去。

            1. tmux 用于许多事情,包括脚本

              屏幕用于需要事物以特定方式行为和工作的情况

          2. 我认为其中很大一部分是因为 tmux 拥有更友好的默认设置和更清晰的命令设计(例如,开箱即用的状态栏)。而且,我怀疑,人们并不熟悉屏幕。

            后者是为什么 Helix 在做类似 vim 的事情时进展缓慢,尽管它设计得更加一致且默认设置更合理。每个人都知道 vim 存在。

            我个人从未从 Screen 切换,尽管我只是一个轻度用户。

            1. > 默认启用的状态栏

              软件包往往因最荒谬的理由胜过其他软件包。

    7. 我觉得这挺有趣的,不知道他在这上面花了多少时间。看起来极其枯燥哈哈

      1. 有时候,这正是人们所需要的。只要工作没有强制性的时间表,你可以随时随地以自己喜欢的速度完成,这种感觉会很好。

      2. 我认为编织看起来很单调,但我能理解它的魅力。

    8. 并非每个代码项目都必须改变世界。这样的实验有时会产生卓越的结果。

    9. 直觉与逻辑是理性(logos)的两部分。我们有时会有一种无法用语言表达(logos)但直觉上明白的理由。

    10. 也许我对一个或多个概念的理解是错误的,但“更多段错误”这一点让我感到困惑。Rust 编译器不应该阻止可能导致段错误的代码编译吗?除非涉及许多不安全的块。

      编辑:显然,事实证明确实有很多不安全的代码。

      1. 这是音译。他基本上是在 Rust 中实现了 C 程序。他在结论中说,下一个目标是将其转换为安全的 Rust。

      2. 在第二句中,他提到:

        > 代码库现在是 100% (不安全的) Rust

        我并没有理解为它 100% 不安全,但我确实认为这可能意味着使用了大量不安全的块。仅在这篇文章中的示例代码中,就有相当一部分是不安全的块。

      3. 我的理解是,尽管 tmux-rs 采用更安全的语言编写,但它仍无法超越由一群高素质开发者维护的、经过长期实战检验的成熟项目的稳定性。

        每个新项目在初期都不可避免地存在需要逐步修复的 bug。

        1. 他们用不安全的 Rust 编写了所有内容,这很可能导致段错误。这不是一个正常的 C 到 Rust 的移植。在正常的移植中,你不会追求 100% 不安全的 Rust 代码——相反,你会将应用程序中需要不安全的部分分离出来,以便突出显示和审计。这显然是一个为了好玩而做的练习。

          1. 我认为这是一个正常的移植过程,只是目前只完成了一半。转换为安全的 Rust 还有待完成。

        2. 不,问题是,从 C 这样的基本不安全的语言直接转换无法解决安全问题。

          你必须进行适当的重写,这样才能从一开始就编写安全的代码。

          > 每个新项目在开发过程中都不可避免地会出现需要解决的 bug。

          但这些 bug 的严重程度远不及不安全语言所滋生的关键安全和可靠性问题。这就是为什么 CISA 和 FBI 都强烈推荐使用内存安全语言。

        3. 我理解作者的意思是,编程中出现的段错误比园艺中出现的要多。

        4. 这只是因为有很多不安全的地方,而且从 C 到 Rust 的转换引入了语义不匹配的错误。

    11. > 新事物

      或者显然是旧事物的复制品。

  5. 这是一个很大的插曲,但:

    > 我的感觉是,如果我的手真的疼得厉害,而我需要继续工作,我还是会选择它。通常一旦我的手指上长了水泡,我就觉得还是休息一下比较好

    我感到震惊,并且以一种不健康的方式感到钦佩。你们中有些人经常打字打到起水泡吗?

  6. 我喜欢这个。我也想尝试喜欢 rust 相关的东西!

    在这里,我想提一下 zellij。Zellij 是一个基于 rust 的终端多路复用器。

    我不是创建者,而是用户。我喜欢 Rust 的一切,喜欢寻找并迁移到基于 Rust 的解决方案(在可行的情况下)。

  7. 我喜欢这个项目的态度,而且大多数评论都给予支持。虽然将一个成熟的应用程序重写成另一种语言听起来总是个坏主意,但这个过程可以学到很多东西。重要的不是结果,而是过程。

    鉴于您在这里获得的支持以及人工智能领域的进步,我相信这可以成为 Rust 初学者非常吸引人的业余项目,可能有很多容易修复的错误。修复错误、添加新功能和优化代码就是您需要做的。

    以下是一个启动这个项目的想法:为 Gemini CLI(或您最喜欢的大语言模型(LLM))创建一个临时缓冲区,并使其能够与 tmux 会话的各个窗口和面板进行交互。

    这是我的使用场景:我使用同步面板向多个服务器发送命令,但有时某些命令会因各种原因失败。如果我能让 AI 发送一系列命令,并根据输出结果进行调整,那该多好。这就像在运行时动态生成一个自定义 shell 脚本。

    1. 我完全支持人们为了学习和娱乐目的从事任何他们想做的业余项目。但我真的不明白直接将某个项目从一种语言移植到另一种语言的吸引力所在。

      例如,我每天都使用gvim。如果我要做一个业余项目的文本编辑器,我绝对不会克隆gvim,而是会制作一个拥有我想要的全部功能且行为方式完全符合我预期的文本编辑器。既然要投入那么多时间到一个项目中,为什么不做些有创意且独特的东西呢?

  8. 我刚刚在不到一小时内将tmux移植到了Fil-C(这包括移植libevent并使其测试套件通过)。

    运行良好且完全内存安全

    1. 你愿意将graphviz移植到Fil-C吗?

      这可能有些愚蠢,但我一直认为graphviz和dot没有真正的替代品(是的,有UI版本,还有mermaid之类,但没有能在真正大的图上工作且具有相同表达力的),而且我怀疑很快所有编写graphviz的人都会去世或退休。我希望看到一个移植到新语言的版本,这样后续维护会更容易。

      再次,这可能只是个固执的想法,但一切都很好。

  9. 巧合的是,我刚好在看这个视频,“Oxidise Your Command Line”

    https://www.youtube.com/watch?v=rWMQ-g2QDsI

    视频中的一些内容对于非 Rust 开发人员来说毫无用处,但其中一些内容对于熟悉命令行界面的人来说非常有用。

  10. 这似乎是一个非常好的未来用例,即通过大型语言模型实现完全自动化的流程,在不到一个小时的时间内将非平凡的 C 代码库高精度地翻译成 Safe Rust。然而,正如作者所指出的那样,即使在开发结束时尝试了 Cursor,该工具仍然无法有效地加速翻译(在 2025 年年中)。因此,尽管潜力巨大,但似乎我们还有很长的路要走。

    1. > 这似乎是一个非常好的未来用例,即通过大型语言模型实现完全自动化的流程,在不到一个小时的时间内将非平凡的 C 代码库转换为 Safe Rust,且准确度很高。

      这是具体的。

  11. 当然,可以对 c2rust 进行改进,以减少因命名不变而导致的信息丢失,从而减轻初始转换的负担?

    1. 是的,这似乎是 C2Rust 缺少的一个重要功能。如果我理解正确的话,主要目的是作为将代码移植到惯用 Rust 的基础。但如果失去了所有常量,生产力就会大大降低。

      1. 的确如此。我们曾尝试过支持保留(部分)常量,但最终放弃了。我们计划在未来几个月内重新引入该功能。

  12. 不知道有没有人尝试过使用这些蓬勃发展的 AI 代码辅助工具来大幅提升移植项目的速度?如果有,使用体验如何?

    1. 楼主提到他们尝试过 cursor,但并未感受到相较于 vi 宏的提速效果。

  13. 我不明白 tmux 的鼠标支持。tmux 运行良好,但当需要启用鼠标支持时,它会接管一切,导致性能极差。

    1. 你无法重新绑定键盘映射到其会话管理器插件,这使得它无法使用,因为我绑定了与插件用于选择目录或类似操作相同的键。因此,我无法通过它创建新会话,只能从命令行操作。

  14. 不错,我喜欢 tmux,我每天都使用它,我几乎离不开它。我希望这个版本能让滚动鼠标滚轮或使用 Ctrl+Page Up/Down,或通过 Ctrl+Tab 在窗格之间切换,或者在左下角显示完整的未拼接标题变得更容易 😉

    抱歉我知道这不是抱怨的地方,但这将非常棒!

    1. 我使用byobu,它基本上是tmux的一个有自己风格的分支。滚轮滚动正常,Alt-PgUp/PgDn也正常。

      Ctrl-Tab 可能无法正常工作,因为终端通常不会将其与 Tab 键区分开来。但你可以尝试将 Alt-Tab 或其他类似的组合键绑定到 tmux 配置中,以在窗口之间切换。这应该只需要一行代码。

    2. 对我来说,滚轮直接就能用。其他功能可以用 `bind-key` 轻松配置。我用 Ctrl-Space 在窗口中切换面板:

      bind-key -n C-Space select-pane -t +1

    3. 你可能会更喜欢 zellij 而不是 tmux。

  15. 作者并未声称这一点,但值得明确指出:tmux 是一款安全关键型软件,却未获得应有的关注。

  16. > 我没有特别好的理由。这只是个业余项目。就像园艺,只是多了些段错误。

    太棒了。你绝对值得这 +350 分!

    1. 这不是我的项目,我看到那行代码后读了剩下的内容,觉得有趣所以提交了。

  17. tmux 在我的系统上占用大量内存,尤其当滚动缓冲区足够大时(我经常有超过 10k 行内容)。

    我经常执行 `pkill -9 tmux` 来拯救我的工作。我希望 rust 版本能在这方面提供一些帮助?

    1. Tmux 的滚动缓冲区大小是可以配置的,也许可以在配置文件中设置 set-option -g history-limit something-smaller?

    2. 多少算多?即使是 10,000 行文本也应该在兆字节级别吧?

  18. Tmux 对我来说是游戏规则改变者。能够用一行代码启动十几个不同项目(使用 tmuxinator):服务器、监控日志文件、激活虚拟环境、运行 Docker 容器,所有操作都在一行代码中完成:太棒了。多年未用,两天前刚从 iTerm 迁移到 Ghostty 时重新开始使用。现在非常喜欢这个设置。再加上 nvim。纯粹的精彩。

    期待尝试 tmux-rs。

    1. 听到你从 iTerm 迁移过来感到惊讶。其实 iTerm 有一个 tmux 集成模式(使用 -CC 选项),非常棒。这样你就不用记住任何 tmux 特定的快捷键了。切换窗口只需按 Cmd+`,和其他地方一样。

      当我无法使用 iTerm 时,我完全停止使用 tmux。

    2. > 期待看看 tmux-rs。

      你查看过网站了吗?这是一个 c2rust 项目。Rust 代码充满了不安全的代码,可能比 C 版本更容易出错,更不用说可读性和可维护性了。也许这只是个玩笑。

      1. 你看了吗?比如第 2 段:

        > 我扔掉了所有的 C2Rust 输出,决定将所有文件从 C 手动翻译成 Rust。

        谈到了从它开始,意识到它太粗糙了,然后改变了方法。现在它是危险的 Rust,但最终的目标是一个安全的版本。

        1. 是的,时间会证明一切。我不会屏息以待。你可以,但你可能会发现自己死了(或者没有,实际上)。

          你读过结论吗,顺便问一下?

    1. zellij有一些有趣的想法,但还有很长的路要走。你可以随意重新绑定基础模式及其操作,但如果你与插件的键绑定冲突,你就完蛋了,因为插件似乎都使用了硬编码的键绑定。

  19. 我希望 tmux 能支持远程连接到多个后端实例。

    1. 使用 tmuxinator 启动多个 ssh/mosh 连接。

  20. “尽管生成的代码可以正常运行,但基本上无法维护,而且比原始 C 代码大 3 倍。”

    这使得 C2Rust 看起来毫无用处?

    “我最近达到一个重要的里程碑:代码库现在 100% 是(不安全的)Rust。我想分享一下将原始代码库从大约 67,000 行 C 代码移植到大约 81,000 行 Rust 代码(不包括注释和空行)的过程。”_

    然而,手动移植(且仍然不安全)的 Rust 版本 C 程序却仍然大了近 20%?

    如果我记得没错,Go的gc编译器是自动从80K行C代码转换为80K行Go代码的。手动移植的版本会小得多。

    1. > 这让C2Rust看起来似乎没什么用?

      它能用几秒钟完成他花6个月才做完的事。当然,它并不完美,无法保留常量的名称是一个明显的缺陷。但可以想象,经过一些改进,你可以将这 6 个月中的部分时间用于清理代码,仍然可以节省时间。听起来 C2Rust 已经接近完美,但尚未完全达到。

      > 然而,用 Rust 手动移植(且仍然不安全)的 C 程序重写版本仍然大 20% 左右?

      大小并不是一个非常有用的指标。但移植工作仍然只完成了一半。他已经让它在 Rust 中运行了,现在他准备进行移植的“艰难”部分,将“基本上是 C 语言”的代码重写成符合 Rust 语言习惯的代码。这样就可以提高安全性,并希望获得更易读的代码。

      1. “它在几秒钟内完成了原本需要他六个月才能完成的工作。”

        它在几秒钟内生成了无法使用的垃圾代码,这与他手动编写六个月的代码完全不同。

        “大小并不是一个非常有用的指标。”

        大小是一个非常有用的指标。统计令牌数量是更准确的“大小”估算,但代码行数是一个不错的初步估算。

        高级语言的全部目的是让用更少的代码做更多的事情。大小并不是唯一重要的,但它非常重要。

        Rust 代码不仅比 C 更冗长,而且更不规则、更复杂。代码行数的 20% 增长可能相当于代码复杂度的 50% 增长,而且这还没有考虑安全性。

        只需比较帖子中的示例中的令牌即可:

          // cmd-kill-session.c
          RB_FOREACH(wl, winlinks, &s->windows) {
            wl->window->flags &= ~WINDOW_ALERTFLAGS;
            wl->flags &= ~WINLINK_ALERTFLAGS;
          }
        
          // cmd_kill_session.rs
          for wl in rb_foreach(&raw mut 
          (*s).windows).map(NonNull::as_ptr) {
              (*(*wl).window).flags &= !WINDOW_ALERTFLAGS;
              (*wl).flags &= !WINLINK_ALERTFLAGS;
          }
        
        1. C-flavored-rust 当然比 C 更冗长,但这并不能让你了解 Rust 的惯用语。

    1. 作者将该移植描述为“100% 不安全的 Rust”。你还能指望什么呢?

  21. 你说“100%(不安全的)Rust”的时候,其实并没有说谎吧?

  22. 下一步是将不安全的代码慢慢移植到安全的 Rust 上吗?哈哈

    1. 这有什么好笑的?我正是这样做的(在专业环境中):使用c2rust生成可用的代码,然后逐步重写代码并生成单元测试(通过insta进行的审批测试特别方便)。最终结果是一个更易于维护的代码库,拥有更好的工具和测试。

  23. 我喜欢这篇文章,可以学到很多东西。

    似乎将 Rust 自动翻译成 C 并不是一个很好的主意:“我扔掉了所有的 C2Rust 输出,决定将所有文件从 C 手动翻译成 Rust。”。手动翻译似乎也不理想:“在翻译代码时引入了许多 bug。我想分享发现并修复其中几个的过程。”或使用 AI:“这是因为使用 Cursor 翻译代码时仍会偶尔插入 bug,就像我一样。因此,我花在审查生成的代码上的时间与自己编写代码所需的时间相当。”

    作为业余项目,祝你好运。但否则,也许最好不要重写已工作的代码….

    1. > 但如果不是这样,也许最好不要重写已经工作的代码……

      不过,最终结果允许在内存安全的语言中进行扩展和改进。

      1. 似乎对这个问题有一种相当不理性的执着。

        1. 这源于这样一个事实:几乎每个用C语言编写的有用程序都存在多个安全漏洞,只是等待被发现。如果你的代码库中没有这些漏洞,那么任何重大更改都可能引入新的漏洞。

          1. 与其机械地断言任何C程序都存在安全漏洞,并且修改C程序也是一个安全问题,不如看看tmux的实际记录。

            tmux已经存在了近18年,而M. Marriott直到上周仍在积极改进它。人们可以实际查看它在这段时间内的记录,如果记录不佳,就用基于实际证据的证明取代未经证实的笼统断言。

            * https://cvedetails.com/product/20683/Nicholas-Marriott-Tmux….

            1. 该搜索遗漏了以下内容:https://www.cvedetails.com/cve/CVE-2020-27347/

              这仍然是一个相当不错的记录,但我的观点仍然成立。它得到了我数十年来在C语言衍生语言中工作的经验的支持。当然,你不必接受我的经验或相信我的观点,对我来说都一样。

              1. 这有点像“关门捉贼”——如果你担心安全问题,那么在终端复用器中运行任何不可信的软件本身就是个坏主意,无论你的复用器是用内存安全语言编写还是其他语言。

                …在有人抱怨我是 C 语言的粉丝之前,我必须说明,我并不是。十多年来,我一直在使用内存安全的语言编写软件。但我也非常务实,知道何时关于 RiR(在 Rust 中重写)的争论是合理的,何时是不合理的。就 tmux 的具体情况而言,关于安全的争论是偏离了重点。

                1. 你从来没有运行过将数据输出到标准输出的 curl 命令吗?你从来没有对下载的文件进行过 cat 或 less 操作吗?你从来没有通过 ssh 连接到其他人运行的服务器吗?

          2. 作为一个使用 Rust 超过 7 年、最近为个人项目转换到 Zig 的人,我发现其中有很多细微的差别。是的,rust 非常可靠,即使不考虑内存安全性方面,它也确实非常不错。但与使用 c 或 zig 等简单语言相比,使用 rust 进行开发非常痛苦,无法享受开发过程。

            此外,开发时间大大缩短,我节省的时间可以用于添加更多功能和测试。

            如果您关心构建时间,并且不想为所有内容使用依赖项,建议使用 zig 等语言构建低级项目。

            1. 我喜欢 C 和 C++ 的各个部分,我仍在用这些语言编写新代码。但对于任何可能暴露在恶意数据下的组件,安全是一个永无止境的“打地鼠”游戏。我不是说每个人都必须放弃,只是当人们这样做时,这是主要原因之一。

        2. 当你不理解某件事时,它可能会显得不合逻辑。

          本帖中另一条评论希望出现“一个全新的、无懈可击的tmux-resurrect”。人们渴望这类工具的原因与C语言编写的非trivial程序的局限性密切相关。

          这类程序更难在不引入 bug 的情况下扩展,新团队成员也更难理解,等等。

          这种“不合理的痴迷”与超越 20 世纪 70 年代开发的原始高级汇编语言的技术进步有关。

          1. 我非常理解他们,但我认为这并不比其他考虑因素更重要。此外,我不认为 Rust 比 C 更简单。它也不那么有趣,可移植性较差,还有其他令人讨厌的生态系统成本。

            1. 这里有很多主观因素。我最后一次认真编写C代码是在20世纪90年代初,我一点也不怀念它,因为我不喜欢花时间在与我正在处理的问题领域无关的低级细节上。

              我发现 Rust 非常有趣,而且易于编写系统级代码,我非常欣赏它所提供的构造正确性。通常,只要你正确使用类型系统,只要它能够构建,它就能正常工作——正如人们所说,让非法状态无法表示。用 C 语言做到这一点非常困难。

              Rust 并不是完美的。对于大多数事情,我更愿意使用 Haskell、ML 或同级别的语言。但它与 C 还是完全不同的,用它来重写软件生态系统只会带来改进。

              1. C23 与 C89 截然不同。C89 的变量声明绝对不有趣。

                嵌入、指定初始化和 constexpr 都是非常好的补充。

            2. 实际上,Rust 比 C 更具可移植性。一般的 C 程序是为 Unix 或 Windows 编写的,而 Rust 具有足够的抽象能力,能够一次编写大多数业务逻辑。

              我维护着 cargo-nextest,这是一个广泛使用的 Rust 测试运行器。虽然可以将 nextest 的运行器循环用 C 语言实现,但这将极其困难——每个测试的状态机拥有数十个状态,存在多个动态事件源作为输入,且事件循环高度依赖 epoll/kqueue/Windows 对应的机制(通过 Tokio 进行抽象化)。因此,大多数用 C 语言编写的测试运行器甚至无法接近 nextest 的质量、可靠性和可移植性。

              https://nexte.st/docs/design/architecture/runner-loop/

              1. 我想你可能不知道 C 生态系统有多庞大。我不确定 cargo-nextest 是何物,但见过有人用 C 解决最复杂的程序。

                1. 正如我提到的,cargo-nextest 是 Rust 中广泛使用的测试运行器——欢迎访问其网站了解其功能集。

                  在 C 中也可以做到这一点,因为它最终会编译成机器代码。但这将超出绝大多数C团队的能力范围,即使是顶尖团队也需要数年时间才能实现,且移植成本将极为高昂。因此,我不知道有哪个用C语言编写的测试运行器能接近nextest的功能集和移植性。

                  > 我想你根本不知道C语言生态系统有多庞大。

                  我当然清楚C生态系统比Rust生态系统要大得多。

                  1. 这似乎也不是大多数人会花大量时间去研究的事情。我使用“make”来运行测试,虽然效率不高,但能完成任务。那么从编程角度来看,你认为在C语言中实现什么会特别困难?

                    1. > 这似乎也不是大多数人会花大量时间去研究的事情。

                      这是因为 C 语言创造的条件使这个问题很难解决,而不是因为这个问题不值得解决。

                      对于 Rust 来说,这仍然是一个难题,需要大量使用异步状态机来管理相当复杂的复杂性。但至少像我这样的个人开发者可以以一种稳健、几乎无错误的方式来完成这项工作。

                      > 我使用“make”来运行我的测试,虽然它有些差强人意,但还能完成工作。

                      没错,“make”确实不是一个高性能的企业级测试运行器,它不支持并行测试执行、高质量报告、信号处理、动态状态查询、超时、重试、不稳定测试检测、测试之间的互斥、允许你指定测试集的 DSL、灵活配置、将测试存档以在另一台计算机上运行、测试运行分片、JUnit 支持、包装脚本、设置脚本以及其他许多功能。Make甚至不支持Windows,而这对于一个便携式测试运行器来说是基本要求。

                      您可以查阅设计文档:

                      https://nexte.st/docs/design/architecture/runner-loop/(已在上文链接)

                      https://nexte.st/docs/design/architecture/signal-handling/

                      https://nexte.st/docs/design/architecture/input-handling/

              2. 看起来你只考虑了 Windows 和 Linux。好吧,但你的 Rust 有多少是在 freeRTOS 或裸机上运行的,又在多少个处理器系列上运行呢?C?在所有处理器上都能运行。6502?没问题?8051?当然!CRC16C?是的。Eco32?是的。i386?开发用于运行 C。Arm64、Arm?它们也一样。它们在内部控制硬件上运行 Minix。用 C 编写…

                与 C 代码相比,Rust 完全不具备可移植性。

                1. Nextest 可以在各种平台和架构上运行——远不止 x86_64 Windows 和 Linux。只要有人让 Tokio 在新平台上运行,那么将它移植到新平台就非常简单了。(这就是抽象的力量!将 MxN 可移植性问题转化为 M+N 问题。

                  C 当然有它的用武之地,但“与 C 代码相比,Rust 完全不具备可移植性”的说法是不正确的。与 C 代码相比,Rust 代码在 Windows 和 Unix 上的运行情况要好得多。Rust 的可移植性与 C 不同,在许多方面要好得多,但在其他方面则更差。实际上,我认为在多数实际场景中,Rust 最终比 C 更具可移植性——例如,看看 `eza` 在全球第一大开发平台 Windows 上的运行情况。

            3. > 它也不那么有趣

              根据Stackoverflow的统计,它是目前最有趣的编程语言。可移植性只是时间问题,就像任何语言一样。

              1. 当你不把它用于工作时,它很有趣。而大多数人并不把它用于工作,只是重新编写他们能找到的任何工具。

          2. 哇,这是我最近看到的最傲慢的评论开头。

          3. > 该线程中的另一条评论希望出现“一个全新的防弹 tmux-resurrect”。人们渴望此类工具的原因与用 C 语言编写的非trivial程序的局限性密切相关。

            我不明白;tmux-resurrect 并非用 C 语言编写,且主要用于在重启后保持会话。

          4. > 它诞生于20世纪70年代。

            它诞生于20世纪70年代,并在80年代和90年代被标准化。它持续发展。添加了多种数据类型,以及Unicode和线程。C23标准去年发布。

            1. 你可以对COBOL和FORTRAN说类似的话。C语言的基本缺陷无法修复,因为这需要一种新语言。

              到了某个阶段,就必须向前迈进。

                1. 与所有在50多年前(FORTRAN的情况是75年前)设计并在此后不断添加功能的语言一样,存在相同的问题。最终,语言中嵌入了长期流行的概念,如基于类的面向对象编程,这阻碍了它们向更原则性的设计演进。C++也面临类似情况。

                  所有这些语言都是图灵完备的。因此,如果你对使用某种语言编写代码感到满意且不想改变,那这是你的选择。但Fortran、C或C++为何不是许多人新项目首选的原因,与我之前提到的原因密切相关。总会有坚持使用熟悉工具的人,但科学进步并非仅靠固守旧法。

                2. 要真正定义Fortran的含义并不容易。标准中包含一些不具备可移植性的特性,而许多可移植的特性又未被纳入标准。这简直是一团糟,而且情况还在恶化。

                  1. FORTRAN的含义始终如一。标准是实现者的指南。用户需要根据实现来编写代码。

                    这是一种与大多数人通常不同的语言思考方式,但自20世纪50年代以来,FORTRAN一直如此,而且今天每种具有多个实现的语言在某种程度上也是如此。

                    1. 你支持我的观点,但同时也错过了它。

              1. 在你看来,C语言的基本问题是什么?因为内存安全显然不是其中之一,因为新的系统语言正在被编写而没有它(Zig)。

                1. 内存安全无疑是C语言的一个基本问题。Zig实际上解决了一些这些问题,即使它在定义上不是完全“内存安全的”。此外,新系统语言在没有内存安全性的情况下被编写,并不意味着这是一个好主意。人们出于各种原因编写各种语言。

                  C语言缺乏内存安全性涵盖了广泛的问题,包括手动内存管理、无限制指针、空指针(Tony Hoare的“十亿美元错误”)、缓冲区溢出、释放后使用、整数提升等等。

                  其弱类型系统是另一项根本性限制,与对抽象支持的局限性密切相关。标准库的弱点反映了这一点。弱类型系统意味着其提供的静态保证极为有限。1975年时对此还有借口,如今已不再成立。

                  未定义行为在C语言中比大多数语言更常见。这显然不是系统语言所期望的特性。

                  语言级别的并发支持几乎不存在。

                  使用文本预处理作为语言特性,但语义集成有限。除了对源代码含义的影响外,它还使构建C程序更加复杂。

                  而C23未能显著解决这些问题的原因在于语言本身的根本限制。你无法在不开发新语言的情况下“修复”这些问题。

                  1. 我认为这种关于 C 语言存在无法修复的根本缺陷的说法是无稽之谈。当然,C 语言确实存在许多危险方面,但大多数都比较容易避免。Rust 具有临时内存安全性的优势。我认为 C++、Zig 或 Go 并没有根本性的优势。当然,有很多糟糕的 C 代码,但与其改变语言,不如直接编写现代 C 代码。

                    1. > 确实存在许多危险的方面,但大多数都相对容易避免。

                      这是一个几乎幼稚的论点。如果这些问题如此容易避免,那么如何解释C和C++程序中长长的CVE列表?

                      > 我不认为C++、Zig或Go具有根本优势。

                      我们对此表示认同。它们客观上确实没有。它们都是试图延续C语言遗产的尝试。Go尤其荒谬,它是由20世纪70年代的人设计,这些人坚决拒绝从过去50年的编程语言发展中吸取任何教训。

                      为了明确:我也是20世纪70年代的人。我在1977年学习了FORTRAN。但与Go的设计者不同,我没有让对编程语言设计的理解停滞在1970年代。我学习了东西。我研究了东西。我发现了东西。

                      你认为C是系统编程语言设计的终极形态吗?如果你同意它不是,那么我们到底在争论什么?

                2. 我对 Zig 绝对没有偏见,因为它还处于 1.0 之前,但如果你看看最近哪些新系统语言得到了广泛采用,而不是刚刚创建的语言,你会发现是 Rust。行业采用它的明确原因是内存安全性。

                  1. 什么采用?博客文章?回声室?Rust 已经达到顶峰了。炒作者们不愿相信这个事实,但这是现实。对 Rust 绝对没有偏见。

                3. zig 也在尽力实现内存安全(在运行时,如果你需要的话),而 c 却停滞不前(你可以添加净化器,但它们并不是语言内置的)。

      2. tmux 其实从内存安全中得不到任何好处,因为:

        1. 在 tmux 中运行的任何程序已经拥有执行权限,而且通常与 tmux 属于同一用户。

        2. 任何想要利用 tmux 的人只需运行 ‘tmux -C’ 即可自动获取 tmux 内的所有交互操作权限。

        3. 该软件本身已经非常稳定。我从未遇到过它崩溃的情况。

        如果你担心有人利用你的终端,那么无论它是用 C 语言还是 Rust 语言编写的,tmux 都是一个糟糕的选择。我是一个非常喜欢 tmux 并每天使用它的人,但我还是要这么说。

        [编辑]

        如果你担心与安全无关的 bug 会影响用户体验,那么无论使用哪种语言重写,如果你的应用程序已经经过近二十年的实战考验,重写都是一个更糟糕的解决方案。你最好创建一个全新的东西,而不是将代码从一种语言移植到另一种语言,因为至少这样你会有新的想法,而不是同一个应用程序但有新的 bug 在不同地方。

        我这么说并不是因为 Rust 粉丝们认为我存在偏见。我喜欢内存安全的语言,认为 Rust 是新项目的绝佳选择。我在这里想说的是,重写对 tmux 并没有多大好处,因为 tmux 已经非常稳定了。

        1. 你忘记了tmux是一个终端模拟器。可信程序可能包含不可信/攻击者控制的终端输出。如果在tmux内部运行的程序(如cat、curl -s、weechat)能够输出格式错误的Unicode或触发崩溃或代码执行的逃逸命令,这实际上是一个重大问题。

        2. 除了安全性之外,还有其他值得担忧的安全问题。我读这篇文章时的第一个想法是,如果 tmux 中的一个漏洞导致一个长期运行的或特别依赖状态的会话崩溃,那将是一个巨大的麻烦。当然,我在自己的使用中从未遇到过这种情况,但如果能降低这种可能性,那本身就是一个价值所在。

          1. 如果 tmux 是個新專案,我同意你的看法。但就像你一樣,我使用 tmux 已經接近 15 年了,從未見過它當機。

            事實上,該專案的作者已承認,他們的重寫版本引入了漏洞。我知道这是一个业余项目,所以我并不批评他们的工作。但如果我们只关注减少错误,那么重写现有项目并不是正确的方法。像 Zellij 这样的项目更合理,因为它除了用 Rust 编写之外,还提供了新的功能。

        3. 任何程序都能从内存安全性中获益。内存安全性不仅仅涉及安全问题。它关乎消除一类特定的错误——缓冲区溢出、空指针错误、释放后使用等,列表还可以继续。这些错误恰巧也往往会带来严重的 security 后果。

          我真的不明白这种对 1970 年代风格编程的执着辩护。你认为 C 语言是编程语言设计的巅峰吗?不是?那你的观点究竟是什么?

          1. > 任何程序都能从内存安全中获益。内存安全不仅仅关乎安全。它旨在消除一类特定的错误——缓冲区溢出、空指针错误、释放后使用等,列表还可以继续。这些错误恰好是那些往往会导致严重安全后果的类型。

            你真的在tmux中遇到过这样的错误吗?因为我使用它已有大约15年,可以坦率地说我从未遇到过。

            然而这次重写确实引入了错误。我知道这是一个业余项目,所以我并不苛责。但如果你只是想减少错误,那么用另一种语言重写经过实战检验的代码,无论哪种语言,都不是正确的方法。

            > 我真的不明白这种对20世纪70年代编程风格的执着辩护。你认为C是编程语言设计的巅峰吗?不是?那你的观点究竟是什么?

            我在哪里在为20世纪70年代的编程风格辩护?我甚至没有在为C辩护。事实上,我最后一个基于C或C++的项目是在10年前。相信我,我是内存安全语言的粉丝 😉

            我的观点非常明确,而且非常具体地针对tmux。你只是在试图在字里行间寻找,并制造一个根本不存在的论点。

      1. 哇,这位微软开发者在.NET仓库中尝试使用Copilot,却连基本配置都没做好?

        还是说那条评论只是因为Copilot的输出完全是胡说八道而找的借口?

  24. > 代码库现已100%转换为(不安全的)Rust。我希望分享将原始代码库从约67,000行C代码迁移至约81,000行Rust代码的过程。

    听起来这似乎是一个C到Rust的转译器。:D

    编辑:我猜对了,他们使用了 c2rust。

    然后还有“// 生成的 Rust 代码”。

    至于 https://richardscollin.github.io/tmux-rs/ 上的代码片段,我更喜欢阅读 C 版本,而不是生成的 Rust 代码。

      // cmd-kill-session.c
      RB_FOREACH(wl, winlinks, &s->windows) {
        wl->window->flags &= ~WINDOW_ALERTFLAGS;
        wl->flags &= ~WINLINK_ALERTFLAGS;
      }
    
      // cmd_kill_session.rs
      for wl in rb_foreach(&raw mut (*s).windows).map(NonNull::as_ptr) {
          (*(*wl).window).flags &= !WINDOW_ALERTFLAGS;
          (*wl).flags &= !WINLINK_ALERTFLAGS;
      }
    

    请告诉我您觉得哪一个更易读。

    1. 我认为没有人会认为生成的 Rust 比原始的 C 更优雅。

      它是自动生成的,目的是保持与 C 代码完全相同的语义,而不考虑安全性、最佳实践等——当然,它比实际的手写 Rust 更杂乱。

      正如 c2rust 在其文档 [1] 中所述,这是将代码库从 C 语言移植到 Rust 语言的手动和增量移植的第一步,作者在结语中也承认了这一点:

      > 下一个目标是将代码库转换为安全的 Rust 代码。

      [1] https://github.com/immunant/c2rust/raw/master/docs/c2rust-ov…

      1. 多么美好的画面……但故事到此为止。

    2. 你应该再往下读一段。他们确实使用了c2rust,但发现它真的很糟糕,于是把它扔到了一边。然后他们手动完成了转换。所以最终结果并不是c2rust。

      1. 是的,我也很期待。

        顺便问一下,你读过结论了吗?

        它可能最终会成为 vaporware。

    3. 我的意思是,当 C 直接移植到 Rust 时,你还指望什么呢?Rust 程序通常不会像 C 程序那样编写。

        1. 那为什么还要发表那个评论? :’)

  25. > 通常当我手指上长出水泡时,我觉得还是休息一下比较好。

    真传奇。

  26. 我喜欢这个倡议,但所有这些努力都是为了……不安全的 Rust?我知道这是一个热门话题,我希望最终目标是拥有一个内存安全(且更快的)tmux。我只希望作者不要止步于此 🙂

    编辑:如下文所指出的,我太蠢了,文章里已经说明了,但我没读到那部分。

    1. 这是两步流程中的第一步:

          1. 用(不安全的)Rust 重写。
          2. 随着时间的推移更新代码,朝着安全的 Rust 迈进。
      

      这是老生常谈的“先让它运行,然后再修复它”的过程。在商业环境中,这通常是个坏主意,因为你最终会浪费比一开始就做对更多的时间——但对于一个业余项目来说,这没问题。因为这样你更有可能学到东西,并且最终可能得到一个更好的最终产品。

      对企业而言,开发人员花在学习上的时间(尤其是通过艰难方式学习)是浪费的。

      对业余爱好者而言,花时间学习是值得的。

      1. 在商业环境中,这种做法也可能有其合理性,因为如果你一直等待代码完美无缺,可能永远无法交付产品,即使你已经有一个可用的(但不完美)产品。一个折衷的方法是,让目标功能的一部分正常工作,其余部分不实施,然后在继续之前交付。

    2. 在文章的最后,他指出下一步是朝着安全的 Rust 努力。

      1. 我非常怀疑这会发生。应该从头开始,用 Rust 编写。

      2. 谢谢!我粗略地浏览了这篇文章,但没找到这一点。应该用 CTRL+F 搜索一下 🙂

    3. 这并不是一个愚蠢的想法。我的第一反应是:1)代码行数增加了 25%;2)在不安全的 Rust 中;3)对于一个已经具有出色维护性的工具而言。

      这之所以能登上 HN 榜首,唯一的原因是 Rust 目前在 Gartner 炒作周期中的位置。

      这很不错,但我不会说它有用。

    1. 这是为了将不安全的 Rust 改写成更符合惯例且更安全的 Rust 而采取的渐进措施。我还没有进行过 Rust 移植,但在将其他项目在语言之间转换时,我发现先进行 1:1 移植以确保系统能够按预期运行,然后从那里进行重构以清理内容,是非常有用的。

  27. 如果 Rust tmux 可以在 Windows 上运行,我会接受它。我目前还得使用 Windows,直到 Bill 把它拿走,我才意识到 tmux 对我的意义有多大 🙁

      1. 是的,我从WSL 1发布以来一直在使用它,非常稳定。

        我的配置文件托管在https://github.com/nickjj/dotfiles,其中包含一个安装脚本,可自动在Debian、Ubuntu、Arch Linux或macOS上配置所有内容(包括tmux及插件)。这包括原生 Linux 和 WSL 2 支持。

      2. WSL 不是 Windows。除非你能完全在 WSL 中工作(如果我能做到,我就会直接使用 Linux),否则在假文件系统、不兼容的符号链接、两个 PATH 和三个 shell 之间切换实在太麻烦了。

    1. 为什么你还要坚持使用 Windows?

      1. 有些程序只能在 Windows 上运行,这并不是什么新问题。我个人在桌面上安装 Windows 的唯一原因是 Ableton 通过 Wine 运行效果不佳,所以其实选择不多。

        1. 没错。Linux上的DAW支持一直不太理想。不过随着Bitwig和Reaper等软件的出现,情况正在慢慢改善。

      2. 大公司里的人大多只能用Windows。但WSL2真的很棒,即使只是个权宜之计。

  28. 我不知道 tmux 的维护者是否会对此感兴趣?

    将更多软件从 C 语言转换到 Rust 是一个很好的想法。

    1. 据我了解,tmux 主要是 OpenBSD 项目,而 Rust 不太适合他们(原因可归结为可移植性问题),因此这种可能性极小。此外,这是一个业余项目,目前完全不安全,因此甚至没有任何特别意义。(编辑:当然,随着项目的重写,后一点可能会减少)

        1. > 该项目仍处于早期开发阶段。存在 bug、崩溃和编译错误。请勿将其用于任何重要任务。

          > rustc_codegen_clr 仅在 Linux x86_64 系统上经过测试,使用 CoreCLR 运行时(通常简称为 .NET 运行时),基于 .NET 8。它可能在其他平台上运行,但不保证兼容性。

          我猜未来它可能成为一种选择,但目前看来更像是一个有趣的技术演示。

    2. 似乎有点理所当然地认为可以随意重写代码,然后让原维护者继续维护。

      1. > 期望

        我好奇,但我并不期望。

        这首先需要 Rust 实现超越代码转换的 POC。就目前的情况而言,我怀疑它能否吸引任何原始作者或维护者。但如果它变得强大、稳定并加快速度,那么拥有两款类似的软件就会带来一些重大问题。

    3. 之前看过源代码,我猜可能不会。

      如果你要将一个项目迁移到 Rust,你肯定希望它看起来像 Rust。目前它看起来更像用 Rust 编写的 C 语言。这真的不会让任何人感到高兴。

      (显然,这并不是对维护者的轻视,这是一个采用特定方法的个人项目)

    4. 这是个玩笑吗?你见过生成的 Rust 代码吗?这不是真正的重写。

      1. 看起来你根本没读过这篇文章。作者在发现自动生成的代码不够好后,谈到了自己编写代码的事情。

        1. 我读过,但我并不抱有太大的希望。你读过结论吗?

  29. 为什么这些由不同作者重新实现的项目要借用原项目的名称?

    你想重新实现一个知名项目,没问题

    但请给它起个别的名字

    1. 这艘船在开源诞生之初就已经启航了。看看核心工具包(coreutils)有多少个不同的重新实现版本就知道了。

    2. 不,我喜欢在后面加上“-rs”的想法,因为这样更容易过滤。

  30. 该死!

    我原本希望看到一个全新的、坚不可摧的 tmux-resurrect 的发布。

    但事实并非如此,它只是 tmux-(recodedIn)rust。

  31. 用新语言重写旧代码是人工智能的杀手级应用。应该用这个代替转译器。

  32. 我喜欢这个,但他们展示的代码片段示例真的、真的不像是 Rust 的惯用表达方式。

    他们基本上抛弃了所有 Rust 模式,只是用 Rust 编写了一个 C 程序(例如所有原始指针)。

  33. 大语言模型(LLM)非常擅长将一种编程语言转换成另一种编程语言。

    事实上,我有时会将代码移植到另一种语言,然后再移植回来,只是为了清理代码(或者至少为可以清理的内容提供一些想法)。

    我好奇为什么原帖作者没有以此作为起点?

    1. 这在文章中有所讨论吗?他们尝试过使用光标,但错误率并不比手动操作更好,因此至少在这种情况下,这种方法并未奏效。

    2. 因为他不想这样做?他提到对他来说,这就像一个“园艺”项目,所以为什么要剥夺编程的乐趣,只是为了成为一个AI操作员?

  34. 稍微离题:直到最近我才意识到终端倍增器(如tmux)本身就是一个终端。

    因此,即使你运行的是最强大/最快/功能最丰富的桌面终端……但如果你的终端倍增器不支持某些功能,它会阻碍你使用高级桌面终端。

    Ghostty 创建者解释的 3 分钟短视频

    https://youtu.be/o-qtso47ECk

  35. 有趣的是,这篇文章和评论中都没有提到初始翻译的大语言模型(LLMs)。这真的令人感到惊讶,因为这是我进行翻译/移植任务时首先会想到的东西(尽管验证可能会比较棘手)。

    现在,我真的很想知道像 Sonnet 4 这样的优秀模型会表现如何。

      1. 使用 Cursor 重构不安全的代码与使用大语言模型(LLM) 翻译成安全的 Rust 代码是完全不同的任务。我只是好奇它的表现如何。

    1. 看看下面吧 🙂

      > 我确实在开发过程的最后阶段开始尝试使用 Cursor。但我最终停止使用它,因为我觉得它并没有真正提高我的速度。它只是让我免于手指疼痛。这是因为在使用Cursor翻译代码时,它仍然会偶尔插入错误,就像我一样。因此,我花在审查生成的代码上的时间,与我自己编写代码所需的时间相当。它唯一节省的是我的双手。进行如此大规模的重构对手指来说真的很辛苦。

    2. 它不会完成2%。它甚至无法编译。考虑它都是在浪费时间。

      1. > 它连2%都完成不了

        你是什么意思?我以为这个过程会非常非常渐进。一次添加一个函数加上配套测试,验证后继续,逐步向上推进。

        这是一个有趣的问题,因为我设想未来很多东西都会以这种方式移植。

        1. 我的朋友,你被骗了。

          -编辑祝你好运,去阅读 10 万行你一无所知的克劳德生成的 Rust 代码吧,哈哈。大语言模型(LLMs) 并不是适合这个任务的工具。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: