写点什么

C++ 用了 11 年,仅 17 位贡献者代码提交超过 10 次,迁移到 Rust 后,再也不想回去了

  • 2024-12-31
    北京
  • 本文字数:6662 字

    阅读完需:约 22 分钟

大小:3.32M时长:19:19
C++用了11年,仅17位贡献者代码提交超过10次,迁移到Rust后,再也不想回去了

编者按:

 

本月初,Fish Shell 4.0 正式进入 beta 测试阶段,主要变化就是从 C++迁移至 Rust。如今,随着大部分 Fish Shell 代码被成功转换为 Rust(正式发布 Fish 4.0 beta 测试版,几乎百分之百由 Rust 代码构成),项目组日前也发布了一篇博文,回顾了他们在将大型 C++代码库迁移至 Rust 过程中的种种心得与挑战。

 

Fish (英语:friendly interactive shell) 是一个Unix shell。Fish 旨在成为一个比其他 shell 交互性更强、用户体验更好的 shell,并让其丰富的强大功能能够被用户轻松发现、记住并学以致用。它由 Axel Liljencrantz 于 2005 年创建。由于不符合 POSIX shell 标准,Fish 的语法既不派生于 Bourne shell 也不派生于 C Shell,故被分类为一种“外来”shell。有别于为节约系统资源而默认禁用部分功能的其他 shell,Fish 的全部功能都是默认启用的。

 

Fish Shell 开发人员指出,他们遇到的一系列 C++难题,促使其开始积极探索其他语言。最核心的难题体现在工具及编译器/平台差异、人体工程学与线程安全以及开发社区等。举例来说,Fish 曾尝试用 C++制作一个真正的多线程执行原型,整个过程对于该开源 shell 项目而言痛苦万分。

 

至于为何使用 Rust 语言,他们坦言“Rust 很酷,也很有趣。”他们高度评价了 Rust 的工具、便捷设置、出色的人体工程学、更好的依赖项管理以及强大的发送与同步功能,这一切都使其非常适合线程处理类的应用场景。

 

开发人员们也承认,Rust 语言仍存在一些不足,例如可移植性处理能力不佳、工具有时未能考虑到其他目标并存在某些本地化问题。有人指出,Cargo 虽然特别适合构建需求,但在涉及安装的场景下 Fish 仍然选择了 CMake。

 

Fish 项目组在博文结尾写道:

 

“整个迁移过程并非没有挑战,期间也出现了不少计划之外的状况。但总的来说,进展还算顺利。我们现在拥有一套我们非常喜爱的代码库,其中不乏如果沿用 C++将很难实现的功能。另有更多功能正在开发当中,我们正在开发单独的 3.7 版本,期待能将更多酷炫的成果带给广大用户。”

 

本文将带大家回顾 Fish Shell 编程语言的迁移历程、经验心得、教训与不足,以及接下来的规划。


为什么要再折腾一次?


因为我们被 C++折磨得不轻,包括:

 

  • 工具与编译器/平台差异;

  • 人体工程学与线程安全问题;

  • 社区活力不足。

 

坦率地讲,C++的语言生态并不好。比如说,因为 C++没有“rustup”、也没有在旧有操作系统上安装最新 C++编译器的标准方法,所以我们在为 LTS Linux 和其他早期版本 macOS 发布最新 Fish 软件包时就遇到了麻烦。

 

Fish 还被迫使用线程来实现广受好评的自动建议与语法高亮显示,添加并发性的计划也因为 C++的自身局限而长期缺乏进展。

 

这里跟大家说个秘密:虽然外部命令能够并行运行,但 FISH 的内部命令(内置命令与函数)目前仍只能串行执行,而无法在后台运行。解除这一限制将实现异步提示和非阻塞补全等功能,同时提高性能表现。

 

POSIX shell 选择用子 shell 来解决这个问题,但子 shell 是一种不完善的抽象,可能会在种种意想不到的情况下造成麻烦。我们希望尽量避免这些不可控的因素。

 

我们还尝试使用 C++开发真正的多线程执行原型,但没能成功。比如说,其很容易意外跨线程共享对象,还得配合 Thread Sanitizer 等辅助工具才能防止此类问题。

 

C++的人体工程学也很糟糕——头文件很烦人、模板很复杂、经常触发编译错误,导致标准库中出现大量重载。许多函数使用起来不够安全,C++字符串处理非常冗长,许多方法的重载都容易引起混淆,因此很容易变成 C 样式的字符指针并引发安全风险。总而言之,C++是一种优先考虑性能、而非人体工程学的语言,这对开发者显然很不友好。

 

Curses 这个 C 库则是 C++开发实践中相当典型的案例。这是个用于访问终端功能的古老库,我们用它来访问 terminfo 数据库,后者负责描述终端功能与行为中的差异。

 

整个过程不仅相当麻烦、用起来也不够安全,而且感受上也很别扭——cur_term 指针(有时是宏)可以为 NULL,经常在意想不到的地方被取消引用,而且在源代码构建时也会引发大量问题。

 

最后得承认,C++对开发者的吸引力不强,贡献者们其实对它有点“嫌弃”。在 FISH 使用 C++这 11 年里,只有 17 位提交量超过 10 次的贡献者。

 

所以值此离别之际,我们也想给 C++社区提点感想:希望 C++语言和工具的人体工程学及安全性能有所改进,这些改进其实比性能更重要。我们也希望 C++编译器在实际系统上的升级过程能简单一点。

 

为什么选择 Rust?


因为 Rust 很酷,也很有趣。

 

首先,FISH 只是个业余项目,所以我们都是用爱发电来参与的。没人因为开发 FISH 而拿过一分钱报酬,因此趣味性就成了留住贡献者的关键。

 

Rust 的工具生态也很出色,注重实用性且编译器错误机制出色。这可不是跟 C++相比,而是 Rust 拥有绝对意义上优秀甚至卓越的错误消息机制,非常棒。

 

其安装体验也很好——Rustup 堪称神奇,能够让大家快速上手、将 root 权限使用频度降到最低。相较于 C++编译器那复杂到爆炸的升级流程,Rust 这边只须使用 rustup。

 

Rust 具有出色的人体工程学——即使对于刚刚接触的新手来说,C++指针也是被碾压的一方。

 

Rust 还有明确的使用系统,能够帮助开发人员确切了解哪个函数来自哪个模块,要比 C++的 #include 好用很多。

 

Rust 的依赖项添加体验也更好。现在我们可以轻松使用各种工具能够读取的特定格式,而 Rust 则顺畅支持 YAML/JSON/KDL 等主流选项。

 

从 FISH shell 的角度来看,Rust 真正的王炸其实是 Send 和 Sync,即静态执行线程规则。“无所顾虑地并发”太强大了,我们可以通过 Send 和 Sync 实现完全的多线程执行,并对其正确性充满信心。

 

当然,肯定也有其他同样适合的编程语言,只是我们没有那么多时间一一了解。我们相信 Rust 能够胜任这项任务,并果断开始行动。

 

平台支持


网上有不少关于 Rust 平台支持力偏弱的讨论,但在我们这边没什么大问题——我们的 macOS、Linux 和 BSD 几大平台都受支持,Opensolaris/Illumos 和 Haiku 也不在话下。反正我们是没听说过有人想在 NonStop 上运行 FISH。

 

从 Debian 系统的流行度来估算,99.9995%的 Debian 设备都安装有 Rust 包。再结合 Fish 在 Debian 系统中 1.92%的安装比例来看,预计每 25 万台设备中只有两、三台不受支持,完全可以接受。

 

跟很多网友的猜测不同,我们并不是为了支持原生 Windows 端口才转向 Rust 的。其实 Fish 本质上是一款 UNIX shell,它不仅依赖于 UNIX API,还依赖于其语义,并且在脚本语言中直接暴露。总之我们是搞 UNIX 的,开发的也是 UNIX shell,跟 Windows shell 没啥关系。

 

我们唯一关注但缺乏支持的平台是 Cygwin,很遗憾,但一点点妥协也完全可以接受。

迁移的故事


我们决定以“忒修斯之船”的方式完成移植——即逐个组件迁移,直到 C++代码被彻底替换掉。而且在过程中的每个阶段,项目都仍能正常运行。

 

这非常必要,因为如果不这样做,我们在几个月时间里就没有可以正常工作的版本。这不仅令人沮丧,还会影响大部分测试套件(即运行脚本或者伪终端交互的端到端测试)的正常运行。

 

闭门造车的问题就在这里——不光迁移可能根本没法完成,而且在测试阶段没准还要打回重来。此外,这种方式也让我们保持了 C++代码结构的基本完整,这样我们可以比较迁移前后的情况,找出转译错误出现在哪里。

 

因此,我们使用 autocxx 生成 C++与 Rust 代码之间的绑定,确保每次只移植一个组件。

 

移植的第一步从内置函数开始。这些函数本质上就是小型独立程序,拥有自己的参数、流、退出代码等。也就是说,只要有办法从 C++这边调用 Rust 内置函数,就能轻松将它们与 shell 的其余部分拆解开来分别移植。

 

对于如何将函数接入主 shell,我们使用了以下三种方法:

  1. 添加 FFI 胶水代码,使得 Rust 可以调用 C++函数,这样就能先移植调用方、后移植被调用函数。

  2. 将被调用函数迁移至 Rust,如有必要则保证其可从 C++处调用。

  3. 编写被调用函数的 Rust 版本,并从移植后的调用方处调用,且继续保留 C++版本。

 

例如,几乎每个内置函数都需要解析其选项。我们有自己的 getopt 实现,并在初始 PR 中用 Rust 进行了重新实现,但同时也将 C++版本保留到了最后。若非如此,我们就得编写一个 C++到 Rust 的桥接器,再调整 C++调用方来使用,这显然就太麻烦了。

 

迁移工作大概就是这样有序推进,但最终我们还是遇到了更复杂的系统,这时必须选择更大的移植块,从而减少需要临时编写并终将被丢弃的 FFI 胶水代码的数量。比如说输入/输出“读取器”,作为 FISH 中最大的部分,其最终转换成了约 1.3 万行 Rust 代码。

 

在移植过程中,我们在 autocxx 上遇到了不少问题。有时候它理解不了特定 C++ construct,我们只能花时间尝试解决。比如说,我们在 C++端引入了一个打包 C++向量的 construct,但出于某种原因,autocxx 总是弹出 vector<wstring>。为此我们不得不通过分叉添加了对 wstring/wchar 的支持。但请大家别误会,当初使用 wchar 就是个错误,只是年深日久确实不太好改了。

 

另外,因为 autocxx 生成了大量代码,所以某些辅助工具的表现也不如预期。比如 rust-analyzer 的运行速度就特别慢。

 

总之,虽然我们的代码库已经算是相当适合迁移至 Rust(因为没怎么使用异常或者模板),但 autocxx 的使用体验确实一般。它能正常起效的确令人眼前一亮,也确实帮助我们完成了移植,但距离完美还差得很远。

时间线

  • 初始 PR 发布于 2023 年 1 月 28 日,并于 2023 年 2 月 19 日合并。

  • Fish 3.7.0 是 C++分支下的另一个版本,用于整理某些增量改进,于 2024 年 1 月发布。

  • 最后一点 C++代码于 2024 年 1 月被彻底删除(残留的额外测试代码于 2024 年 6 月 12 日被移植至 C)。

  • 首个移植后的 beta 测试版于 2024 年 12 月 17 日发布。

 

当初的移植计划本来打算在半年之内完成,最终显然没有达成,但大家对此并不失望。坦率地讲,14 个月也是份相当不错的成绩了,毕竟我们在期间还发布了一个 C++版本,就是说迁移工作甚至没有打乱我们的常规发布节奏。

 

大部分工作是由 7 位贡献者完成的(即至少提交过 10 次.rs 文件的贡献者),同时也要感谢很多社区成员的热心参与。

 

造成延迟的原因主要有以下几点:

  1.  “最后 10%需要翻倍的时间才能完成”——我们进行了全面测试,清除了大量 bug。如果急于发布,那绝对是个极其糟糕的版本。

  2. 不光是迁移,还要有新东西——用新代码做同样的事情没啥意义,一定要有所差别。所以我们推迟了发布,直到做出让人眼前一亮的成果。

  3. 有时候,部分成员需要休息一段时间,这也是人之常情。

 

所以大家在评判之前,请先了解一个基本事实:我们只是一群志愿者,大家完全是在自发参与,能做成这样已经很不容易了。

一点抱怨


必须承认,Rust 并不完美,我们对它的某些状况也颇有微词。

 

最主要的一点就是 Rust 对于可移植性的处理方式。虽然它提供大量系统抽象,允许使用相同的代码指向多种系统,但在较低层级的系统上进行代码适配时,仍然完全依赖于手动枚举,即使用 #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))]之类的检查。

 

这个办法明显有很多问题,可能会遗漏某些系统并忽略版本间的差异。据我们所知,libc 能够能够为我们要使用的函数添加 FreeBSD 12,但如果不经精心检查,在 FreeBSD 11 上直接调用则会触发失败。

 

但直接在代码中列出目标,实际上是在重复 libc crate(在本项目中)已经完成的工作。所以要想调用 libc::X(仅在系统 A、B 和 C 上定义完成),则需要单独为 A、B 和 C 添加该检查;如果 libc 添加了系统 D,则需要额外添加。好在我们使用自己的 rsconf crate 在 build.rs 中实现了编译时功能检查。

 

假如 Rust 能有某种形式的“如果该函数存在,则将其编译”的功能——#[cfg(has_fn = "fstatat")],那情况就会好得多。这样 libc create 就能进行任何检查,而 FISH 则遵循其结果,帮助我们大大精简现有 rsconf。现在的方案无法支持缺少某些功能的陈旧发行版,只能通过 min_target_API_version cfg 来解决。

 

我们还遇到了本地化问题——Rust 往往依赖于在编译时检查的格式字符串,可遗憾的是这些内容无法转译。我们只能将 musl 移植为 printf,这是我们内置 printf 函数运行所需,确保在运行时内复用预先存在的格式字符串。

 

遇到的错误

迁移期间我们也遇到了不少错误。例如,我们最初使用一个复杂的宏以允许将字符串写为“foo”L,但其最终未能起效,所以我们将其删除并转而使用更常规的 L!(“foo”) 宏调用。

 

libc crate 中的弃用警告也经常让人摸不着头脑。它解释说“time_t”将在 musl 上转换为 64 位。我们曾尝试解决这个问题,添加了很多打包器,但最终发现其实没有实际影响,毕竟我们不会把从一个 C 库处获取的 time_t 传递到另一个 C 库。

 

有时对原始代码中细微差别的忽略也会引发 bug,进而导致崩溃。比如我们使用了断言或者断言的现代实现“.unwrap()”。这通常就是转译 C++的直接方法,但事实证明其准确性不足,有时需要替换成其他错误处理机制。

 

但总的来说,这些问题并不太难发现。而且出现之后,往往经常几次尝试和调整就能解决,所以就还好。

 

我们还因为开启了 link-time-optimization 并在 CMake 默认使用发布 bulds(目前需要运行完整的测试套件)而引发了一些问题,导致构建时间意外超过预期。

聊聊优点


虽然抱怨不少,但迁移至 Rust 的好处也随着时间推移而开始显现。

 

还记得我们之前提到的(n)curses 问题吗?现在问题没了,因为我们压根就不使用 curses。相反,我们转为使用一个 Rust 包,它唯一的功能就是访问 terminfo 并扩展其序列。这消除了尴尬的全局状态,用不着费心保证在系统上“正确”安装 curses——cargo 下载相应包并构建就行。

 

我们仍然会读取 terminfo,就是说用户还是需要安装。但这个过程可以在运行时内完成,其已经预安装在所有主流系统之上;如果找不到,也只需使用 xterm-256color 定义的随附副本。

 

我们还高潮创建了“自安装”FISH 包,其中包括 FISH 二进制文件在运行时写出的所有函数、补全及其他资产文件。如此一来,我们就能创建静态链接版本的 FISH(在 Linux 上则使用 musl,因为 glibc 总会崩溃且无法解决),这样我们就第一次拥有了能够在任意 Linux 上下载并顺利运行的单独文件(唯一需要注意的就是架构匹配)!

 

对于想要使用 FISH,但有时候又需要以 SSH 接入服务器的朋友来说,这无疑是个巨大的福音。他们可能没有 root 访问权限来安装软件包,而现在一个 scp 就能解决所有问题。

 

我们当然也可以使用 C23 的 #embed 实现类似的效果,但 Rust 的办法更简单也同样有效。

一点遗憾


我们没能成功完成的一项目标,就是在移植之后删除 CMake。

 

这是因为 Cargo 虽然在构建方面表现出色,但在安装方面却过于简单粗暴。Cargo 希望把所有内容都塞进几个简洁的二进制文件之内,但这对我们的项目并不适用。FISH 拥有约 1200 个.fish 脚本(961 条补全,217 条相关函数),外加约 130 页的说明文档(html 及手册页面),外加 web-config 工具与手册页面生成器(均由 Python 编写)。

 

项目中还有一个测试套件,其在单元测试方面比重不大,主要关注端到端脚本和交互式测试方面。脚本测试通过我们自己的 littlecheck 工具运行,该工具会运行脚本并将输出结果与嵌入的注释进行比较。交互式测试由 pexpect 驱动,其会模拟终端交互并检查按下按钮时是否触发了正确的行为。

 

于是我们保留了精简版的 CMake 来完成上述任务,但将构建工作移交给了 Cargo。

 

当然,也可以把这些都交给更简单的任务运行器,比如 Just 或者更常见的 makefiles。但因为之前的设计运行良好,所以我们决定暂时保留,这样对于程序包来说构建过程并不会发生实质性改变。

 

我们暂时未将 Cygwin 列为受支持平台,因为 Cygwin 无法针对 Rust 构建二进制文件。我们希望这种情况未来会有所改变,但目前在 Windows 上运行 FISH 的唯一方法只有使用 WSL。

 

现状与未来

我们这个巨大的迁移项目取得了成功,下面列举几个数字让大家直观感受一下:

 

  • 变更文件达 1155 个,涉及 110247 次插入(添加)、88941 次删除(削减),其中不包括转译。

  • 200 多位贡献者共提交 2604 次。

  • 提交 498 个问题。

  • 近 2 年的工作周期。

  • 将 57000 行 C++迁移为 75000 行 Rust(外加 400 行 C)。

  • 彻底清退 C++代码。

 

目前的 beta 版本运行良好,性能整体上比之前的版本略好一些,内存使用量的下限比之前高、但上限比之前低——闲置时为 8M,高于之前的 7M;但即使是在运行高负载任务时也不会增加太多。当然,这些还有改进的空间,但对于初步迁移成果来说已经令人相当满意了。

 

必须承认,如今的 FISH 还是有点怪……作为一款 Rust 程序,它仍然会直接使用 C API,也在沿用 UTF-32 字符串。希望接下来能找到更好的解决方案,但在迁移后的首个版本中,就不要求那么多啦。

 

移植过程的确充满挑战,很多工作也没能按计划进行。但总体来看,进展还算相当顺利。现在我们拥有了让人更加心情舒畅的新代码库,增添了不少在 C++时代难以处理的功能,另有更多功能正在开发当中。

 

总之,Rust 干得不错,我们很开心。


原文链接:

https://fishshell.com/blog/rustport/

2024-12-31 14:2114845
用户头像
李冬梅 加V:busulishang4668

发布了 1041 篇内容, 共 655.0 次阅读, 收获喜欢 1204 次。

关注

评论

发布
暂无评论

堡垒机全称是什么?是运维安全审计系统吗?

行云管家

网络安全 堡垒机

一文读懂数字化转型中的数据存储

元年技术洞察

数据库 数据中台 数据治理

带你了解CANN的目标检测与识别一站式方案

华为云开发者联盟

人工智能 目标检测 CANN 企业号九月金秋榜 目标识别

区块链商城系统开发NFT交易技术

薇電13242772558

区块链

“易+”开源 | 简单可信赖,GameSentry 正式开源

网易智企

开源 安全测试

百分点大数据技术团队:Cesium技术在智慧应急行业的应用

百分点科技技术团队

哪家web前端培训班比较好?

小谷哥

认识Java的整形数据结构

华为云开发者联盟

Java 开发 企业号九月金秋榜

Seata AT 模式代码级详解

SOFAStack

seata

关于Linux中Keepalived高可用热备自动化部署的一些笔记

山河已无恙

9月月更 #九月金秋

5种kafka消费端性能优化方法

华为云开发者联盟

大数据 企业号九月金秋榜

百草味推出“潮卤江湖”系列新品 聚焦地域风味创新

E科讯

反诈骗要卷起来!隐私计算助攻反诈行动把握主动权

Jessica@数牍

数据安全 隐私计算 反欺诈

以百分点大数据操作系统(BD-OS)为例 解读ToB产品架构设计的挑战及应对方案

百分点科技技术团队

LED显示屏行业大数据分析

Dylan

LED显示屏 led显示屏厂家

合同抵万金,禅道项目管理服务包免费领!

禅道项目管理

项目管理 禅道

基于RESTful页面数据交互案例

十八岁讨厌编程

RESTful 后端开发 9月月更

ESP32-C3入门教程 基础篇(五、RMT应用 — 控制SK6812全彩RGB 灯)

矜辰所致

ESP32-C3 9月月更 RMT

后疫情时代,远程办公发展趋势如何?

Baklib

协同办公 文档管理

clickhouse 索引、索引局限与解决方案

水滴

Clickhouse 索引 解决方案 稀疏索引

如何学习大数据分析?

小谷哥

学习ui设计需要掌握哪些东西呢

小谷哥

《2022 社交泛娱乐出海白皮书》发布,最全出海破局指南

融云 RongCloud

社交 白皮书 泛娱乐

Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量

PPPHUANG

Java 协程 吞吐量 虚拟线程

RDS:一致性处理事务的神器

华为云开发者联盟

数据库 后端 企业号九月金秋榜

[SpringMVC]REST入门案例与优化

十八岁讨厌编程

spring 后端开发 9月月更

直播预告 | PolarDB-X 动手实践系列——PolarDB-X 的表组与分区变更

阿里云数据库开源

MySQL 数据库 阿里云 开源 PolarDB-X

推动零信任加速落地应用 天翼云为企业铸牢安全基石

Geek_2d6073

学习ui设计自学好还是参加UI培训好?

小谷哥

语雀桌面端技术架构实践

阿里巴巴终端技术

桌面端

C++用了11年,仅17位贡献者代码提交超过10次,迁移到Rust后,再也不想回去了_编程语言_Fish Shell_InfoQ精选文章