编程语言 Rust 与 Carbon 的对比
在华盛顿州西雅图举办的RustConf 2025大会上,Rust与C/C++之间安全且符合人机工程学的互操作性成为热门议题。Chandler Carruth发表了主题演讲,探讨了Rust与实验性“(C++)++”语言Carbon在互操作性方面的不同实现路径。其最终结论指出:尽管Rust与其他语言的接口能力正持续增强,但短期内仍无法提供完整的C++互操作解决方案——这为Carbon提供了空间,使其能通过不同方法逐步升级现有C++项目。读者可通过其演示文稿深入研究示例代码。
许多听众似乎都了解Carbon,因此卡鲁斯花在解释该语言动机上的时间相对较少。简而言之,Carbon是一个旨在为C++创建替代前端的项目,它剔除了该语言中某些晦涩的语法,并为编译器检查的内存安全提供了更完善的注释机制。Carbon旨在实现与C++的完全兼容,现有C++项目可逐文件重写为Carbon代码,理想情况下无需更换编译器或构建系统。目前该语言尚未投入使用——正如卡鲁斯演讲中阐明的,项目贡献者正致力于完善语言中更复杂的细节。
“在Rust大会上谈论非Rust编程语言总会让人有点兴奋,”卡鲁斯开场说道,引得全场哄堂大笑。他从事C++开发多年,自2020年项目启动以来便一直致力于Carbon语言的研发。目前他在谷歌语言与编译器团队任职,负责Carbon项目开发工作。他简要展示了谷歌的研究数据——其中显示多数安全漏洞本可通过内存安全语言避免,但并未过多展开,因为他认为RustConf的听众早已深谙内存安全的重要性。
关键在于,全球已有海量软件采用C和C++编写。没有魔法棒能让这些软件凭空消失。他强调,若要将任何现有软件迁移至内存安全语言,必须确保这些语言能与现有软件生态系统无缝集成。互操作性不仅是锦上添花——它是实现内存安全语言普及的关键要素。
Rust已具备多种工具,可实现与C/C++代码的互操作。Carruth列举了Rust原生的外部函数接口bindgen、cbindgen、cxx crate以及谷歌自主开发的Crubit。但他声称这些方案对现有C++软件都非理想选择。他将软件定义为存在于“绿地”(新代码,与C++耦合度低,抽象边界清晰)与“棕地”(与现有C++紧密耦合,API接口庞大)之间的连续体。绿地软件相对容易移植到Rust——可利用现有绑定工具逐模块迁移。而棕地软件则困难得多,因为其难以分解,导致C++代码与Rust代码间的接口必须具备更复杂的双向交互特性。
卡鲁斯指出,关键问题在于Rust能否缩小差距?他认为不可能——至少短期内无法实现,除非付出巨大努力。但Rust并非实现内存安全的唯一途径。理想情况下,现有C++代码应能就地实现内存安全。许多人尝试过,但“C++委员会大概率不会这么做”。他强调,在现有C++框架下根本无法成功实现内存安全。
不过确实存在几种语言成功实现了从基础语言向更强大灵活的继承语言的转型: TypeScript 是 JavaScript 的演进,Swift 是 Objective-C 的演进,而 C++ 本身就是 C 的演进。Carruth 认为 Carbon 可能成为 C++ 的类似演进——一条优先处理最根深蒂固的存量软件、逐步迁移至内存安全语言的道路。他指出,Rust是从绿地开发方向解决内存安全问题,而Carbon则从另一端切入。这使得Rust和Carbon成为截然不同的语言。
深入剖析
他演讲的真正重点在于揭示这些差异所在,以及他认为每种语言能从对方身上汲取哪些经验。Rust与Carbon的语法“差异并不巨大”;他着重探讨的差异更具抽象性。例如在Rust中,编译单元是一个完整的crate,可能由多个模块组成。因此允许模块以循环方式相互引用,这种机制完全可行。而Carbon无法支持这种特性,因为“现有C++代码往往奇特地依赖”独立文件编译能力。因此Carbon继承了C++模型,包含前向声明、(可选的)独立头文件以及更复杂的链接器机制。这使得Carbon模型更为复杂,但这种复杂性并非凭空而来——“它源自C++”。
另一个例子是特质与类的差异。Rust特质和Carbon类的语法差异不大——Carbon将方法写在结构体定义内部,而Rust将它们单独编写——但它们存在重大的概念差异。Carbon必须处理继承、虚函数、受保护字段等特性。“这些复杂性在Rust中根本不存在,也无需处理。”他解释道,Carbon旨在与C++ API无缝对接,甚至支持跨C++/Carbon边界的继承。
这种差异无处不在,他说,语言的各个部分都存在这种现象。运算符重载、泛型和类型转换在Carbon中都更为复杂。为何要这样设计?这些额外的复杂性是否值得?为了解释这一点,他展示了一个假想但并不罕见的C++ API示例:
int EVP_AEAD_CTX_seal_scatter(
const EVP_AEAD_CTX *ctx,
std::span<uint8_t> out,
std::span<uint8_t> out_tag,
size_t *out_tag_len,
std::span<const uint8_t> nonce,
std::span<const uint8_t> in,
std::span<const uint8_t> extra_in,
std::span<const uint8_t> ad);
该示例改编自BoringSSL密码库中的真实函数。每个std::span都是指针与长度的组合。在Rust中忠实实现该函数的核心问题其实并不体现在代码本身:函数文档说明out必须与in指向相同内存区域,或指向完全不重叠的内存片段。当两者指向相同区域时,函数会就地加密给定的明文输入缓冲区;否则加密结果将写入输出缓冲区而不影响输入缓冲区。其他指针均不应发生别名关系。
Carbon仍在开发中,但当前计划通过“别名集”以机器可验证方式表达此类API。这些注解将明确哪些指针允许别名关联,哪些禁止。最终生成的Carbon代码可能如下所示:
fn EVP_AEAD_CTX_seal_scatter[^inout](
ctx: const EVP_AEAD_CTX ^*,
out: slice(u8 ^inout),
out_tag: slice(u8 ^),
out_tag_len: u64 ^*,
nonce: slice(const u8 ^),
input: slice(const u8 ^inout),
extra_input: slice(const u8 ^),
ad: slice(const u8 ^)) -> i32;
此处的 inout 是为特定别名集指定的名称,用于标注输出和输入。函数签名中其余指针均未指定别名集,因此编译器会确保它们不会形成别名。
尝试用Rust实现该API行不通。该语言禁止可变引用相互别名,最终只能为就地操作和复制操作分别编写两个签名不同的封装函数。若将包含此函数的模块重写为Rust,将演变为复杂过程——实际代码的简单转换与接口重构将相互交织。
卡鲁斯指出,Carbon在互操作性方面的优势在于能将这些操作解耦为独立的小步骤。他展示了另一个C++程序示例:该程序虽具备内存安全性,却无法与Rust的生命周期分析兼容。由于计算机化的内存安全分析永远无法做到完美,Carbon在此方面可能也难以有显著突破——但值得注意的是,在Carbon中,编译器无法证明内存安全的模式可被转换为警告而非报错。
这种立足于C++现状的设计理念使Carbon成为一种独特的语言。它最终被特别定制为兼容互操作与渐进式迁移的工具,而这种特性并非免费获得。这使得该语言比其他设计更复杂,卡鲁斯认为这种权衡并非适用于所有语言。但若目标是在整个软件生态中实现内存安全,他认为Rust和Carbon都应有其生存空间。这并非语言间的竞争,而是两种不同语言协同工作,以满足不同项目截然不同的需求。
本文文字及图片出自 Comparing Rust to Carbon


> “C++委员会大概不会这么做”
别让C++圈的人听到这话。他们坚称配置文件是C++安全性的唯一可行方案。
相关观点:https://www.theregister.com/2025/09/16/safe_c_proposal_di…
显然C++团队不希望内存安全函数只能调用其他内存安全函数。而这恰恰是Rust的核心设计。
但这难道不是安全性的基本要求吗?据我理解,配置文件最大的问题在于:当混合不同配置文件的代码时,原本应由配置文件提供的保障就会失效!?
除非规定“仅存在单一配置文件”,否则配置文件机制根本无法运作。这意味着STL将被迫强制采用单一配置文件,进而要求所有程序都使用相同配置文件。如此一来,C++开发者实际上将禁止安全函数调用非安全函数——这恰恰是他们声称*不愿*做的事。
祝好,
Wol
> 显然C++团队不希望内存安全函数只能调用其他内存安全函数。而这恰恰是Rust的核心设计。
呃…Rust完全允许调用非安全对象,反之亦然。
如果我们稍微宽容些,我认为关键在于:从安全函数调用不安全函数,其烦扰程度几乎(但并非完全)等同于调用抛出检查异常的Java函数。这当然是设计使然——在合理可避免的情况下,你本就不该调用不安全函数,而Rust恰到好处地降低了这种烦扰感,使其在真正需要不安全操作时尚可忍受。
但C++并非Rust。C++需要应对大量无法证明安全的遗留代码。若调用这类代码的体验与调用Rust中的不安全代码同样糟糕,C++开发者很快就会找到关闭安全配置文件的方法并付诸实践。
我并非认为安全配置文件可行——正如多数审视过该机制的理性人士所料,它们注定失败。但我不确定是否存在更好的替代方案。标准委员会在此问题上确实被逼入绝境。
或许该彻底弃用C++了,告诉人们必须重写代码。是的,这将是一项浩大工程,但这是我所知唯一的纯软件解决方案。CHERI技术能在硬件层面解决内存安全问题,但相关硬件尚未问世。
关于要求人们听从指令完成海量工作的说法:“我能召唤深渊幽灵”。还记得当时的回应吗?
你正在假设一种根本不存在的权威,无论在你自己还是任何人身上都不存在。
让我们面对现实吧,C和C++本身并不具备内存安全性,也无法被说服变得安全。所以没错,它们应该被“弃用”。
但这并不意味着任何人被迫重写任何代码。被弃用的软件(及硬件)完全可能长期存续。
但若你需要修改超过三行的代码,这个弃用标签就能派上用场——当管理者执意在陈旧库上再贴一块创可贴,而非用合理语言重写时,它能有效阻止这种行为。而重写往往能从长远看节省大量时间。
无论如何,它已被众多个人和组织宣告弃用,例如美国网络安全与基础设施安全局(CISA):
使用内存不安全的语言(如C或C++)开发用于关键基础设施或国家关键功能(NCF)的新产品线……具有危险性,将显著提升国家安全、国家经济安全以及公共卫生与安全的风险。
——美国网络安全与基础设施安全局(CISA),《产品安全不良实践》
但对于那些几乎必然导致庞大依赖集和巨量SBOM的语言,CISA又是如何评价的?
尽管你可以对C和C++抱怨连连,但在C和C++生态系统中,依赖地狱现象实属罕见。这种情况通常源于使用了少数几个“包罗万象”的流行框架,但这些框架都经过精心维护且有严格把关(例如glib、Qt)。
因此,C和C++或许确实该被普遍弃用,但依我之见,在任何涉及安全敏感的场景中,依赖地狱都绝不可接受——这远比使用精心编写的C/C++代码更严重的问题。
Rust 并不强制使用庞大依赖库,甚至不强制使用内存分配器。庞大依赖的存在,是因为开发者(及其用户与利益相关方)青睐这些依赖带来的优势,选择直接采用而非重复造轮子。
这论点毫无意义。
若某种语言既没有庞大的标准库,也没有几个成熟的大型库,开发者终将陷入依赖堆砌的困境。
> 若某种语言没有庞大的标准库
这和主题有什么关系?C、C++或Rust都没有庞大的标准库。好吧,或许可以说C++的库算得上庞大…但它仍远不及Python(而且Python的许多功能也存在于Rust的标准库中)。
> 或几个成熟的大型库,
我认为这并非Rust的问题,而是“开发者普遍不喜欢大型库”的问题。Rust完全有能力支持大型库。开发者选择不创建或使用它们,是因为Cargo让创建和使用小型库变得相对轻松。在几乎所有构建和打包系统足够便捷的编程环境中(如Go、JavaScript等,但不包括Java、C、C++等),我们都能观察到类似现象。以Python为例——尽管其打包系统存在严重缺陷,但仍勉强可用——我们能看到大型与小型库并存的局面。
现在,你或许会辩称这也不重要,毕竟它仍是依赖项众多的语言。但请思考这种观点的深层含义:你实质上是在主张“优秀”的语言必须配备“糟糕”的构建系统,以此强制开发者遵循你而非他们自身偏好的方式。我的核心观点是:市场不会屈从于你个人的偏好。若你想要大型库,可以自己编写,委托他人开发,或寻找采用单仓库模式的雇主。
> 你本质上是在主张:一种“优秀”的语言应当配备“糟糕”的构建系统,以此强制开发者遵循你偏好的方式而非他们自身选择的方式。
这听起来像是Rust的基本设计理念——它是一种立场鲜明的语言,其主张有时并不符合传统主流观点。
例如,它在编写需要大量原始指针的代码时表现“欠佳”(比如那些拥有复杂所有权关系,却无法承受Rc+RefCell运行时开销的代码)。语法比C语言更丑陋,别名规则更复杂且定义模糊,性能可能更差(因Rust指针不支持基于类型的严格别名规则),文档充斥着令人胆寒的警告,过度使用
unsafe会遭社区排斥等等。来自C语言背景的程序员常想沿用这种编程方式。他们尝试实现的首批数据结构往往是链表——C语言在这方面表现优异,而Rust却相当糟糕。但 Rust 之所以不擅长,是因为其设计者希望你避免这种编程方式。他们刻意在使用原始指针时设置显著阻力,因为你本就不该使用原始指针。这或许会短期激怒 C 程序员,但实为利于其成长,长远来看他们终将领悟其中深意。
我认为语言设计者若认定庞大的依赖树具有危险性——无论其他语言的程序员多么热衷于这种工作方式——并通过设计语言和工具来降低其便利性,这完全符合该设计哲学。要求 Cargo.toml 显式列出所有传递性依赖项的所有者白名单,迫使开发者意识到自己信任了多少随机 GitHub 用户,从而更倾向于采用共享维护权限的自包含仓库组等方案。避免重蹈C语言构建系统那般灾难性的覆辙,但仍需采取措施引导用户遵循语言设计者认定的最佳实践——正如他们在语言其他诸多方面所做的那样。
此处Rust并未作出此类决策,但我认为他们本可(甚至应当)这样做。
cargo vet
> 但请思考这种主张的深层含义。本质上你是在主张:所谓“优秀”的语言必须配备“糟糕”的构建系统,以此强制开发者遵循你偏好的方式而非其自身偏好的方式。
这听起来就像:一种“优秀”的语言应当让使用原始指针成为“糟糕”的体验,以此迫使开发者采用你偏好的(内存安全)方式,而非他们偏好的(不安全)方式。
实在忍不住了抱歉(感谢 excors https://lwn.net/Articles/1038755/)
在这个供应链攻击泛滥的时代,诸如“cargo vet”之类的工具至关重要。我无法断言“cargo vet”是否最佳解决方案,对“庞大依赖关系树”也无明确立场。但必须存在某种SBOM约束机制,迫使多数开发者放弃惯常做法——即让AI编写代码导入随机孤立的开源库,然后提前下班。
(但愿没人回复“好好培训、监督和管理开发者”这种“理想化职场”论调)
你的供应链论点(无论通过AI或其他方式)其实站不住脚——因为大型依赖项目(拥有众多提交者)反而更容易被攻击者在维护者不熟悉的角落植入随机代码,这比一堆小型依赖更危险。
至于无人维护的依赖项,这正是RUSTSEC发布弃用库公告并配套cargo-deny等工具的原因。当然我们的依赖维护状态检测机制尚有改进空间,但这本质上仍优于对大型依赖项的虚假维护——当代码库实际50%处于维护状态,50%是多年无人问津的代码时,这种做法更具现实意义。
你是不是点错了回复对象?我重新读了我的评论,完全找不到类似“方案A比方案B更容易遭受供应链攻击”的说法(你刚才毫无依据地断言了这一点)。
我只提到供应链攻击正日益猖獗,却尚未得到足够重视。依我之见,当下最关键的问题并非攻击源头何在。关键在于最佳防御方案是什么。理想情况下,这种防御应能有效应对任何来源的攻击。
核心工具是货运拒绝,它提供三大关键功能(另含SPDX许可标签检测):
坦白说,C和C++中依赖项数量看似较少,但单个依赖往往庞大——这源于其构建系统使新建项目和整合多个项目变得痛苦,而非将所有内容塞进单一巨型依赖是明智之举。
您说得对,这正是使用库数量较少的原因。但最终结果是:实际使用的库更少,这些库往往知名度更高、测试更充分,也更容易审核。
你真正关心的,是每个功能组件都经过充分测试且易于审核。若每个功能组件独立成库,这便相对简单:你可以查看测试用例和组件变更记录,确认其维护方式符合预期。
> 你真正关心的,是每个使用的功能组件都经过充分测试且易于审核。
你只专注于技术层面。
监管/合规等负担会随独特组件数量呈线性增长,而组件复杂性带来的工作量通常远不及庞大的固定基准开销。
(举个可能的例子:$dayjob-1要求每种规格的螺丝都需单独建档并追踪生产批次。因为当置于3T磁场中接受>1MW梯度脉冲时…哪怕微小的螺丝也会变成致命弹丸)
你不能仅凭“所有螺丝都来自菲利普斯公司”就免除每种规格螺丝的文书工作;每种规格仍需单独处理。软件领域亦然——即便全部采用“Qt”框架,也无法规避对每个组件的独立管理。
> 依我经验,监管机构并不关心你是从单一库获取100个功能组件,还是使用100个各含单一组件的库——他们要求你为每个使用的组件履行合规义务,而非按供应商计数。
虽不确定“监管领域”的具体范围,但在$BIGCORP公司确实存在按供应商分项的合规工作。合规流程怎么可能完全不考虑供应商因素?
你不能以“这些螺丝都来自菲利普斯螺丝公司”为由,逃避为每种独特的螺丝类型/尺寸填写文件;无论如何,你都必须为每种独特的类型/尺寸单独处理文件。
至少你可以复制粘贴供应商信息,这比研究100家不同供应商省事得多。
100个小型库并不意味着100个供应商——例如Qt就包含来自单一供应商的59个库。正因如此,当我们将Qt纳入监管系统时,无需审查全部内容,只需审核实际使用的库(可能仅2-3个),而非全部59个。
另一关键点在于:大型项目所需的软件流程与基础设施呈非线性增长。测试单个千行级库相对简单,测试十个库仍属可控。但面对千万行级项目,测试体系构建则是截然不同的挑战,因为你必须同时应对:
– 庞大的测试套件
– 耗时漫长的运行周期
– 众多相互关联的模块,微小变更可能影响海量测试
– 需要在不完全线性化的前提下排序贡献(合并列车)
最终你不得不采用基于覆盖率的测试选择、CI分片、通知规则(CODEOWNERS)、缓存管理、机器调度等方案——这些措施需在目录层级实施,而非项目层级。而代码仓库管理工具往往只专注于项目层级,这二者存在本质差异。
当然,若想在软件流程中添加新环节,将其应用于单个项目仓库远比应用于数十个项目简单得多。但我认为,软件流程的迭代升级在项目整体更迭中占比甚微(无论采用单体架构还是分离架构,单仓库还是多仓库模式)。
作为下游使用者,若需审核1000万行代码(LOC),我必须完整审核这1000万行代码;无论这1000万行代码分散在两个各含500万行的库中,还是分散在1万个各含1000行的库中,对我而言都毫无帮助。我仍需全面审核,确保所有1000万行代码都符合我的测试标准(无论标准如何设定)。
> Qt正是绝佳范例——它被拆解为众多独立的组件
当然,这里的“独立”存在一定程度的相对性。
虽然你可以在限制范围内随意提取所需组件,但仅更新需要新功能的组件,而让其余部分保持十年未变的“辉煌”(毕竟那是你审核它们的时候,既然没坏就别动……)这种做法行不通。(libboost就是这种做法的极端例子。)
当然,依赖地狱并非Qt或Boost独有……但真正独立的库往往比那些含糊要求“给我2025年9月当下的libfoo版本”的库更明确地声明所需依赖版本。
这确实意味着,当组织权衡新的软件投资时,他们不太可能重新投资于旧的C++代码库。
创建“更安全”的C++配置文件/衍生版本的真正目的,是说服CEO们向C++这个无底洞投入更多资金——提案宣称新模块将采用“安全”衍生版本编写,而投资者被强烈暗示这终将使整个系统安全(“终将”的含义如同“猪会飞”般遥不可及,但任何想保住饭碗的人都不会公开承认)。
在企业内部,只要能找到足够多的工程师用其他语言完成同等或更优质量的工作,这种情况就可控——只需下达“禁止使用新C++,否则开除”的指令即可。
> 但如何让C++过时?
若企业和政府开始禁用它,高校自然停止教学。
当简历上高调标注C++反而成为求职障碍而非加分项时,学生自然不会主动学习。
一旦如此,程序员将停止个人编写C++代码,因为工作环境已不再将其作为主力语言。
届时C++将与COBOL、Fortran等遗留语言为伍,维护人员将获得高额报酬(我们只能羡慕)来维持旧代码运行。
祝好,
Wol
那你有更好的Qt替代方案吗?因为在我看来,没有替代品的话这种情况不会发生。
而且Qt只是众多例子中的一个。
https://blog.logrocket.com/state-rust-gui-libraries/ 列出了11个图形用户界面库……其中总有一个能满足你的特定使用场景。
坦白说,Qt 是个晦涩难懂且极难调试的混乱系统(GTK 亦然)。证据:只需在控制台启动任何非简单程序,看着警告信息滚动而过便知。
话虽如此,Rust/Qt 的绑定确实存在。假设它们编写得当,能有效提升你的 GUI 代码安全性。毕竟你不必一次性替换全部代码,分百次完成每次1%的替换同样有效。
弃用不等于禁用。其含义是“我们建议你停止使用该功能,未来将减少对此的支持力度”。
任何人都可以说这种话,而你始终可以选择无视。真正重要的是当发言者是值得信赖的专家和/或对你具有某种影响力时。(若是专家,你可以认为他们提出不应使用该功能的建议有充分理由,无需浪费时间自行推敲整个逻辑链条得出相同结论。若对方掌握权力——例如能影响新编译器是否兼容你的旧软件——那么其理由便无关紧要,你应当考虑立即采纳建议以避免未来的兼容性问题。当然你仍可选择无视,但需承担相应后果。)
标准制定机构兼具专业知识与决策权,因此当其宣布某项功能已弃用时具有重要意义。但弃用声明也可能源于社区共识、企业政策或个人开发者的主张等。任何一方都可能宣布“我们认为不应使用C++,将停止编写C++规范/工具/文档等,停止在应用程序中使用C++库,停止为C++项目贡献代码”——即便并非所有人认同,这些举措仍将推动C++的逐步式微。
从长远看,欧盟《网络弹性法案》这类法规将通过禁止商业实体将安全视为外部性来推动此类整体趋势,但这将是一个极其缓慢的过程。
> 要让C++在全球范围内被弃用,意味着需要让全球大多数权威专家都宣布C++已过时
未必如此。若负责认证你那套昂贵门禁系统的政府机构(特别是其合规性认证方面)宣布“C++已弃用”,这将直接导致你续证时需额外论证/审查——即便众多C++专家坚称该语言完全可用。
铁烯是合格编译器的范例;需通过项目文档确认是否满足该编译器的资质要求; 具体而言,安全手册中列出的约束条件将说明使用Ferrocene时的注意事项。
嗯…
那么能否设置弱型和强型配置文件——最好通过pragma声明对应模块的适用类型?
弱型pragma仅检查该模块,声明“此代码无异常操作,但无法保证调用对象行为”;强型pragma则类似Java的受检查异常——“此代码无异常操作,且其调用的函数亦无异常行为”。
这个过程需要很长时间才能渗透开来,但各处可以制定政策:每次修改代码时先应用弱编译指令,确保其生效,再验证强编译指令是否同样有效。所以本质上是通过逐个模块的细致调整推动进展?
祝好,
Wol
> 也就是说,它本质上是通过逐个模块的细致调整来推动进展?
首先,必须迁移到模块化架构才能实现这种机制。
你需要一种特殊的代码块包裹元素,声明该块内所有内容(包括导入的模块)均可在安全代码中直接使用,无需在使用点添加注释——尽管这些内容未经检查且不保证安全。由于该区块覆盖内部所有内容,故包含所有通过#include引入的元素;因其是带有专属标记的特殊区块,可围绕其使用编写工具链,并要求由资深开发者进行审查。
> 显然C++开发者不希望内存安全函数仅限于调用其他内存安全函数。
> 而这恰恰是Rust的核心设计原则。
值得注意的是,Rust在2024版中对此进行了更深入的改进:不再默认要求不安全函数必须包含不安全主体。目前这仅作为默认启用的警告,但即便在不安全函数内部,工具链现在也强烈建议在调用高级功能前使用unsafe关键字。
https://doc.rust-lang.org/nightly/rustc/lints/listing/all…
这是因为允许不安全函数在其主体中执行不安全操作,从语义上讲本就不合理。“unsafe”关键字具有两种截然不同的含义:
– 作为函数签名的修饰符时,它属于 API 的一部分,表明该函数存在编译器不会强制执行的非平凡安全先决条件。按惯例,这些先决条件应在函数文档注释的“安全”标题下列出。
– 作为代码块时,它代表开发者承认该块内存在未强制执行的安全预设条件,并承诺手动维护这些条件。惯例要求该代码块需添加 SAFETY 注释,说明相关预设条件在执行该点时成立的依据。
由于编译器无法知晓程序员承诺在任何给定不安全代码块中维护哪些先决条件,也无法确定任何给定不安全函数或其他不安全操作*可能需要的精确先决条件,因此它无法对两者进行相互验证。因此,程序员刻意将所有不安全代码块尽可能缩小的情况并不罕见。若仅将单个操作包裹在unsafe中,则只需关注该操作的先决条件,便不会因误判结构安全而意外做出无关承诺。值得庆幸的是,Rust将不安全代码块(及所有代码块)视为表达式,因此可精确定位需要允许的不安全操作,同时将表达式其余部分保留在安全代码中。
早期Rust版本允许不安全函数在本体中执行不安全操作而无需单独的不安全代码块,这导致两种语义被不当混淆。更重要的是,这种设计极大增加了缩减不安全代码块范围的难度。
严格来说,编译器完全清楚原始指针必须指向正确类型的有效分配才能进行解引用,并且编译器对其他大多数不安全特性的运作原理也有类似的认知。但编译器无法感知原始指针所处的完整上下文环境,因此(例如)它无法判断当某个标志位被设置时该指针始终可读取,也无法处理更复杂的模式变体。你可以在安全代码中编写封装器来强制执行此类不变量,但该封装器的实现最终仍需在内部使用unsafe机制。对此并无变通方案,因为“unsafe”的核心价值正是为编译器无法理解的操作提供逃生通道。
我认为他们始终将其定位为这种用途,别无他意。
我真的很想知道Carbon会如何发展。正如演讲中提到的,C++已经为C代码提供了类似的进化路径,但在实践中我尚未见到这种模式真正奏效。有时开发者会花时间用std::unique_ptr和std::vector封装C API,但同样常见的是C类型和函数反而开始渗入C++代码中。理论上说,我们应该强制执行更严格的标准。但现实中人们事务繁忙,时间有限。或许Carbon能找到方法,抑制人们写“就加点C++让它运行”的冲动?
个人认为“C++是C的继承者”不过是营销噱头,实际应用中无人真正如此看待,本质上是失败的营销策略。
话说从前有个叫“cfront”的东西,能把C++转译成C代码……
随后便爆发了关于模板实例化方法和ODR的论战…
> 理论上说,我们应该强制执行更严格的标准。但实践中人们事务繁忙且时间有限。或许Carbon能找到方法,让人不再心生“写点C++就能搞定”的念头?
没错,关键在于执行力度。最“简单”的方法之一就是将奖金与“小C++”比例挂钩。你也可以像对待其他质量指标那样,在该比例未达标前阻止版本发布。还能针对该比例强制增加代码审查、测试覆盖率、流程开销等要求——让使用“小C++”的开发者苦不堪言。
手段不胜枚举——发挥你的想象力吧。但所有方法都需要管理层自上而下的强力推动。某些技术导向型企业恰恰具备这种推动力。这种安全机制的推动力足以让Carbon取得成功——正如它正在推动Rust取得成功那样。
顺便问一句,发言者供职于哪家公司?:-)
不知是否借鉴了Herb Sutter的cpp2替代性C++语法/语言?
这让我想起多年前Ben Werther和Damian Conway提出的类似方案:https://dl.acm.org/doi/pdf/10.1145/240964.240981
嗯,相似之处在于两者都是为C++设计的新语法绑定。不确定具体语法是否存在相似性。
你不必调用其他语言的函数,而是将系统构建为多个进程,每个进程可使用任意语言编写。使用protobuf等协议可避免在所有语言中重复实现通信功能,但系统整体状态的部分内容仍需在不同语言间同步,因此向此类系统添加新语言的成本并非为零。
大型单体程序本身就是糟糕的设计——尤其在内存不安全的语言中。因此将所有C++代码拆分为小型程序或许是明智之举,随后再用Rust(或Python等)添加新模块。