大约两年前,Fish Shell 的核心维护者 @ridiculousfish 发布了一个迅速成为社区焦点的拉取请求(Pull Request):#9512 - Rewrite it in Rust。这一提议最初只是为 Fish Shell 开发者们开的一个玩笑,并未打算作为广为传播的新闻稿。
然而,它意外地引起了广泛关注和热烈讨论,成为了 Fish Shell 社区中的一个重要里程碑。随着 fish 4.0 beta 版本的成功发布,Fish Shell 完全告别了 C++,几乎全部代码都由 Rust 编写而成。本文将回顾这段转型过程中的挑战、收获以及未来的展望。
背景:为什么要用 Rust 重写 Fish Shell?
Fish Shell 并不是第一次经历语言转换——它曾经从纯 C 移植到了 C++。但是,这次从 C++ 转向 Rust 是一个更为雄心勃勃的项目,因为 Rust 在 Fish Shell 初创于 2007 年时甚至还没有诞生。在使用 C++ 的过程中,Fish Shell 团队遇到了一系列挑战:
工具链和编译器/平台差异:C++ 的工具链不够成熟,尤其是在支持老旧系统(如 LTS Linux 和较旧版本的 macOS)方面。由于缺乏类似 rustup 这样的工具,安装最新的 C++ 编译器变得非常复杂,这给打包者和潜在贡献者带来了额外的负担。
易用性和安全性:C++ 的语法和特性(如模板、头文件等)虽然强大,但也容易导致编译错误和不安全的操作。特别是多线程编程,C++ 中的线程管理非常棘手,容易引发跨线程对象共享的问题,而这些问题往往只能通过事后检查工具(如 Thread Sanitizer)来发现。
社区参与度低:C++ 并没有吸引到足够的贡献者。在过去 11 年中,只有 17 个人对 C++ 代码库有至少 10 次以上的提交。对于一个依赖社区贡献的开源项目来说,这是一个令人担忧的现象。
性能与易用性的权衡:C++ 强调性能优化,但在实际开发中,许多功能(如 string_view)虽然现代且高效,但容易引发使用后释放(use-after-free)等安全隐患。
转型中的挑战与错误
在从 C++ 向 Rust 转型的过程中,Fish Shell 团队也遇到了一些弯路和错误。例如,团队最初尝试使用复杂的宏来简化字符串处理,但最终发现这种方法并不实用,因此改用了更传统的 L!("foo") 宏调用。此外,团队还遇到了一些因误解原代码细节而导致的 bug,通常这些 bug 会导致程序崩溃,因为开发者使用了 assert! 或其现代替代品 .unwrap() 来处理错误。尽管这些问题是浅层次的,但它们仍然需要团队花费时间去解决。
另一个值得注意的问题是关于 libc crate 中的 time_t 类型。团队最初试图绕过一个关于 time_t 将在未来切换为 64 位的弃用警告,添加了许多包装器以保持对该类型的无关性。然而,后来他们意识到这个问题并不会影响 Fish Shell,因为他们并没有在不同 C 库之间传递 time_t。这个问题的解决让团队意识到,有时过度优化反而会带来更多复杂性。
此外,团队在启用链接时优化(LTO)并将其设置为 CMake 默认的发布构建时,无意中增加了构建时间,给开发者带来了不便。尽管这些挑战存在,但团队最终找到了解决方案,确保了项目的顺利推进。
收获与改进
尽管转型过程中遇到了不少挑战,但 Fish Shell 团队也从中获得了许多宝贵的收获。首先,Rust 的工具链和生态系统为 Fish Shell 带来了显著的好处。团队不再依赖复杂的 C++ 工具链,而是可以通过 rustup 快速安装和升级编译器,极大地简化了开发流程。Rust 的编译器错误信息也非常友好,帮助开发者更快地定位和解决问题。
其次,团队成功摆脱了对 curses 库的依赖。在 C++ 版本中,curses 用于访问终端信息(如 terminfo),但这带来了许多问题,尤其是在不同操作系统上安装和配置 curses 时。现在,Fish Shell 使用了一个专门的 Rust crate 来处理 terminfo,消除了这些麻烦。用户不再需要手动安装 curses,cargo 会自动下载并构建所需的依赖项。此外,Fish Shell 仍然可以读取 terminfo 文件,这意味着用户只需要在运行时安装这些文件,或者使用内置的 xterm-256color 定义。
另一个重要的改进是 Fish Shell 现在可以创建“自安装”包,将所有函数、补全和其他资源文件嵌入到二进制文件中,并在运行时写入。这使得 Fish Shell 可以生成静态链接的版本,特别适用于 Linux 系统。通过使用 musl 而不是 glibc,团队解决了 glibc 在某些情况下不可避免的崩溃问题。这种自包含的二进制文件使得用户可以在没有 root 权限的情况下,通过 SSH 登录远程服务器并立即使用 Fish Shell,极大地方便了用户的日常使用。
未竟之事与未来展望
尽管转型取得了巨大成功,但 Fish Shell 团队也承认有一些目标尚未实现。例如,团队原本希望完全摆脱 CMake,但由于 Cargo 在安装方面的局限性,CMake 仍然被保留下来,尽管其作用已经大大简化。Cargo 专注于构建二进制文件,而 Fish Shell 需要处理大量的脚本文件、文档和测试套件,因此 CMake 仍然是不可或缺的一部分。团队表示,未来可能会考虑使用其他任务管理工具(如 Just 或 make)来替代 CMake,但这并不是当前的优先事项。
此外,Cygwin 作为 Windows 上的一个重要平台,目前暂时失去了支持,因为 Rust 尚未提供针对 Cygwin 的目标。团队希望这种情况能够在未来得到改善,但在现阶段,Windows 用户仍然可以通过 WSL(Windows Subsystem for Linux)来使用 Fish Shell。
最后成果
经过近两年的努力,Fish Shell 成功完成了从 C++ 到 Rust 的转型。这一过程不仅解决了许多长期存在的技术问题,还为项目带来了新的活力和更多的可能性。Rust 的安全性、易用性和强大的工具链使得 Fish Shell 的代码库更加健壮和易于维护。
尽管转型并非一帆风顺,但团队通过不断学习和调整,最终实现了预期的目标。未来,Fish Shell 将继续探索更多创新功能,并为用户提供更好的体验。
作者:码农匠人
参考:
https://fishshell.com/blog/rustport/