Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

如何将C/C++项目迁移至Rust?

#rust#c/c++迁移#内存安全#增量迁移#性能关键系统

免责声明:本文是在 AI 的辅助下创建,并由 JetBrains RustRover 团队审阅。

从 C 和 C++ 到 Rust 的迁移已不再只是一个实验性的想法。越来越多的团队开始将 Rust 视为一种提升内存安全性、降低长期维护成本以及现代化关键性能系统的实际可行方案。但一次成功的迁移并非只是因为 Rust 流行就要重写所有代码。更实际的问题在于:

“为什么团队应该考虑将现有的 C 或 C++ 系统迁移到 Rust?这些系统中的哪些部分应该迁移到 Rust?又如何在不破坏现有功能的前提下进行迁移?”

[LOADING...]

这是我们最近与来自 Mainmatter 的 Luca Palmieri(《100 Exercises To Learn Rust》作者,以及即将出版的《C to Rust Migration》一书的作者)以及 JetBrains 的 Vitaly Bragilevsky 进行的直播中的主要话题之一。Mainmatter 与采用 Rust 进行实际软件项目的团队合作,包括咨询、培训和迁移支持。在直播中,Luca Palmieri 分享了实际经验所揭示的内容:从 C 和 C++ 到 Rust 的迁移项目在哪里会成功,通常在哪里会遇到困难,以及为什么渐进式迁移往往是更安全的选择。

完整直播视频可在这里观看:

TL;DR: 对于许多生产系统,最安全的从 C++ 到 Rust 的迁移策略并不是完全重写。而是一种渐进式迁移:从独立的模块开始,让 Rust 在现有的构建和发布流程中工作,随着信心增长逐步扩展。

相关资源: Mainmatter 提供 Rust 咨询 给计划或正在进行迁移项目的团队。如需更深入了解迁移模式,请查看 Mainmatter 的 C to Rust Migration 书。如果您的团队想先加强 Rust 基础,Luca Palmieri 的 100 Exercises To Learn Rust 可作为 JetBrains Academy 课程使用。

为什么团队要从 C 和 C++ 迁移到 Rust

几年前,提出从 C 或 C++ 迁移到 Rust 可能会让人感觉冒险。没有人愿意成为试验品,尤其是当项目承担着负载、支撑着生产流量或对业务重要时。如果一个团队要投入数年的工程努力进行迁移,他们需要确信不会中途发现无法克服的障碍。

如今这种犹豫已经减弱。像 Google 这样的公司不仅自身在迁移到 Rust,还公布数据论证 Rust 采用能够提升安全性,尤其是减少内存安全漏洞,并相比之前的 C++ 代码库降低缺陷率。除了降低风险之外,还有几个因素影响了当前的局面:

  • 专业知识正在传播。 在一家公司学会了 Rust 的工程师会将知识带到下一个岗位,从而在整个行业产生乘数效应。
  • 工具链已经成熟。 过去使早期采用变得痛苦的生态系统缺口大体上已被填补。
  • 招聘担忧正在消退。 “我们找不到 Rust 开发者”这一说法在越来越多开发者拥有生产环境 Rust 经验的今天已不再站得住脚。
  • AI 辅助减少了摩擦。 生成式 AI 工具有助于降低学习曲线,使初始上手阶段不再那么令人畏惧。

问题已经改变。团队现在在问:我的 C 或 C++ 代码库是否存在一个适合用 Rust 解决的问题?如需更全面地比较两种语言(包括内存安全、性能和并发),请参阅我们的 Rust vs C++ 对比。让我们看看哪些项目应该迁移以及如何迁移。

哪些项目应该迁移?

并非每个 C 或 C++ 项目都能从 Rust 迁移中受益。对于个人项目,计算很简单:如果你想学习 Rust 或者觉得这样做合适,就迁移。对于生产系统,迁移需要有商业意义。最有可能候选的通常是那些维护成本已经很高的代码库:

  • 性能敏感的代码库,在其中为了榨取每一点效率,团队不得不采用难以理解的复杂模式。Rust 的安全保证不会牺牲性能,但它确实让这些复杂模式变得更容易管理。
  • 并发或多线程系统,在其中借用检查器提供了在 C++ 中难以复制的安全网。在 C++ 中需要时刻警惕的数据竞争和内存安全问题,在 Rust 中会成为编译时错误。
  • 安全关键组件,漏洞会带来高昂成本。如果你的代码是诱人的攻击目标,那么在漏洞进入生产环境前防止内存安全问题具有明确的经济价值。
  • 大规模部署,即使微小的效率提升也能转化为显著的基础设施成本节约。如果迁移到 Rust 让你能在性能更低的硬件上运行,这些节约会迅速累积。

共同的主线是可维护性。成功的迁移能够更快地交付功能、降低缺陷率,或两者兼得。迁移成本必须通过减少返工、减少安全事件、提高开发者生产力或运营节约来证明。

C++ 到 Rust 迁移:完全重写 vs. 渐进式迁移

完全重写和渐进式迁移之间的选择并非意识形态之争。它取决于你的部署模型、代码库特性以及可用的测试基础设施。

完全重写听起来很吸引人。你从头开始,按照自己的意愿搭建代码库,设立边界,抛弃旧的决策。但让我们看看完全重写何时有意义。

何时完全重写 C/C++ 到 Rust 有意义

  • 代码库相对较小,范围可控
  • 你拥有一套详尽的黑盒测试套件,能够验证行为而不假设内部结构
  • API 表面定义良好且稳定
  • 你控制着部署环境

作为后端部署的服务是不错的候选。你可以将影子流量路由到新实现,与旧系统比较响应,然后逐步转移负载。如果出现问题,你有多种杠杆可用:立即回滚、仅路由一小部分流量,或将迁移限制在特定客户群。

关键在于建立信心的机制。你需要能够证明新实现行为与旧实现一致的机制,包括用户可能无意识依赖的微小细节。

何时渐进式迁移更好

对于许多团队来说,最安全的 C++ 到 Rust 迁移路径是渐进式的,尤其是对于大型、活跃或部署在客户端的系统。你不会一次性替换整个代码库,而是迁移一块、发布它、从中学习,然后继续。成功的标志是稳步前进,理想情况下是加速前进。一个发布版本可能包含 95% 的 C 或 C++ 和 5% 的 Rust。后续版本可能包含 90% 的 C 或 C++ 和 10% 的 Rust。

随着时间的推移,Rust 部分增长,旧代码缩小,团队持续关注重要信号,如缺陷率、性能、用户反馈和开发者速度。

这种方法的价值在于,每一个迁移的部分都集成到实际产品中。用户获得新代码。团队看到它的性能是更好还是更差。Bug 在正常的问题追踪器中显现。迁移逐版本建立信心。

理想情况下,你拥有的 Rust 越多,添加更多 Rust 就越容易。起飞阶段是最难的。一旦构建系统、测试设置、FFI 约定和发布流程就绪,后续模块应该比第一个更容易。

当机构知识很重要时,渐进式迁移也很有意义。如果你是一块一块地迁移,维护代码的开发者会全程参与。完全重写有丢失知识的风险:你最终可能得到结构更好的代码,但没人记得为什么做出某些决定。

从哪里开始:从叶子节点逐步蚕食依赖图

一种实用的方法是查看模块依赖图,找到独立的模块。从叶子节点开始:那些没有依赖或只有少量依赖其他系统模块的模块。这通常是最容易的起点,因为第一批 Rust 代码不需要调用 C 或 C++ 代码。它只需要能够被现有的 C 或 C++ 代码库调用。

换句话说,Rust 模块暴露一个外部的 API,但其内部仍然可以像正常 Rust 一样组织。这比表面上看起来更重要。在你编写有意义的 Rust 代码之前,你需要解决集成问题:

  • Rust 如何融入现有的构建系统?
  • C、C++ 和 Rust 代码如何链接在一起?
  • 内存消毒器是否还能跨语言边界运行?
  • 所有支持的平台上都能正常工作吗?
  • 团队将如何测试和发布多语言代码?

从一个简单、低复杂度的模块开始,你可以在解决更困难的软件挑战之前先解决这些问题。你希望发布一行几乎什么都不做但能正确构建、正确链接并在所有 CI 流程中工作的 Rust 代码。

一旦第一个模块完成,你就解放了其他模块的依赖。接着处理这些模块,逐步扩大你的 Rust 代码孤岛。最终,你会得到外部是 C、内部是 Rust 的结构,然后继续扩展直到 C 消失。

缺点是你可能要在远离核心功能的模块上花费数月,这些模块可能已经多年未被触及。感觉就像你没有增加业务价值。但你正在构建基础,以便将来重写复杂、重要的部分,而无需在底层管理 C 依赖。

另一种方案:纵向切片

相反的方法是通过 Rust 驱动一个特定的用户流程或功能,沿着栈切出一个纵向切片。这将使 Rust 代码从一开始就处于关键路径中,从第一天起就展示业务价值。

挑战在于复杂性。你的 Rust 代码会不断调用 C 并被 C 调用。到处都是原始指针。它虽然是 Rust,但感觉像 C 风格 Rust。很长一段时间内你不会获得安全性好处,因为大多数操作发生在借用检查器所能验证的范围之外。

这种方法会让团队怀疑:“这段 Rust 代码真的比它替换的 C++ 好吗?看起来和感觉都一样。”两种策略都有其优点。从叶子节点自下而上通常更干净,允许更好的重构。纵向切片能更快展示业务价值,但需要一开始就应对更多复杂性。

Rust FFI 是迁移中最棘手的部分

渐进式迁移最困难的部分是跨越语言边界。在纯 Rust 内部,编译器帮助强制执行所有权、借用和生命周期。你可以尝试一种设计,借用检查器会告诉你内存安全性是否成立。

一旦原始指针在 Rust 和 C 或 C++ 之间传递,那个安全网就会变弱。程序员必须手动追踪假设:

  • 这个指针还活着吗?
  • 它有没有别名?
  • 谁拥有这个值?
  • 谁被允许释放它?

这时候,你本人就成了借用检查器。这正是 Rust FFI 成为核心的地方。团队需要清楚的关于所有权、分配和清理的规则。

一个有用的原则是:谁分配内存,谁就释放它。避免在 C 中分配而在 Rust 中释放,或者反过来,除非边界被非常小心地设计。在混合 C、C++ 和 Rust 的代码库中,unsafe 代码是预期的。这并不意味着迁移是错误的。

它意味着 unsafe 代码需要被视为重要的设计层面,而不是无人审查的胶水代码。许多迁移工作就是让已经在 C 或 C++ 中存在的行为在 Rust 编译器看来可见且正确。这可能感觉像是艰苦的工作,但至关重要。迁移到 Rust 的意义在于利用静态分析,而代码必须以一种允许这些工具发挥作用的方式进行组织。

在开始迁移之前要做什么

从 C++ 到 Rust 的迁移不仅仅是一次重写。这是一个长期工程项目,会影响构建系统、发布流程、测试策略以及之后维护代码的人。这就是为什么最安全的迁移通常是逐步建立信心的迁移。从小处开始,早期解决集成问题,保持 Rust FFI 边界易于理解,并确保团队学习足够的 Rust 来拥有新代码。

实际要点很简单:从 C++ 到 Rust 的迁移不是要尽可能快地替换每一行代码。而是要正确的方式将系统中的合适部分迁移到 Rust,以降低风险、保留知识,并让产品继续前进。