恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Carbon 语言 `extern` 声明统一模型:p003980 提案的声明约束、类型一致性与实现落地
首页
资讯中心
/
Carbon 语言 `extern` 声明统一模型:p003980 提案的声明约束、类型一致性与实现落地
Carbon 语言 `extern` 声明统一模型:p003980 提案的声明约束、类型一致性与实现落地
发布时间:2026/9/10 10:05:38
Carbon 语言extern声明统一模型p003980 提案的声明约束、类型一致性与实现落地【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang导读本文基于 Carbon Language 仓库中的设计提案 p003980-singular-extern-declarations.md系统讲解 Carbon 对extern声明如extern class Foo;、extern library Bar class Foo;的全新统一模型。该提案解决了跨库extern声明与普通声明产生不同类型导致签名不匹配的类型一致性问题并重新定义了修饰符关键字何时必须书写的判定准则。读完本文你将掌握 Carbonextern三声明模型非拥有声明 拥有前向声明 拥有定义的完整规则、显式导入对类型完整性的影响、跨库重声明只做语义匹配而非语法匹配的原因以及该设计在工具链中的实际落地形态与验证方式。背景extern模型的两次演进本提案并非凭空而来它建立在两个前置提案之上p003762: Merging forward declarations即提案 #3762建立了前向声明的合并模型p003763: Matching redeclarations即提案 #3763进一步演进了extern关键字的行为引入了重声明的语法匹配机制。在 #3762 建立的旧模型中允许出现多个extern声明且假定一个类的extern声明与非extern声明构成两个不同的类型、随后再被合并。这个假设在实际使用中暴露出类型一致性问题详见下一节。提案 p003980 的定位非常明确——在Versus proposal #3762一节中它直接声明extern特性基本被重写不应假设 #3762 中的任何extern行为仍然适用。问题跨库extern声明导致的类型不一致考虑如下三库代码提案 Problem 一节的核心示例library a; class C {}library b; extern class C; extern fn F() - C*;library c; import library a; extern fn F() - C*;在旧模型下库b中extern class C与非extern的class C {}被视为两种类型而F的返回类型C*也就随之有两种不同的解释库c通过import library a拿到的C与库b中extern声明的C不是同一个类型。结果就是两个库中同名函数F的签名类型不一致破坏跨库类型一致性。该问题最早在 #packages-and-libraries 频道的 extern type coherency 讨论中出现Issue #4025 也专门追踪了extern类型间接访问的处理。本提案的目标即统一C的类型——无论从哪个库、以extern还是非extern方式看到它C始终是同一个类型从而保证F的两个声明具有完全一致的签名。提案在选择实现策略时优先选取能够支持高效编译器实现的路径通过唯一标识符如 32 位整数判等而非按名称做全量类型规范化。提案核心extern声明的三声明模型声明数量与角色对一个实体最多允许三种声明且角色各不相同可选的、非拥有的extern library owning_library声明必须位于与定义不同的库中拥有库owning library的 API 文件必须导入这个extern声明并且必须同时包含一个拥有方的声明。可选的、拥有的前向声明owning forward declaration必须出现在定义之前API 文件在编译顺序上被视为位于实现文件之前。必需的、拥有的定义owning definition把上述规则套回问题示例变化是库a必须import library b且定义处必须写extern库b的声明必须写全extern library alibrary a; // 本提案这里的 import 变为必需。 import library b; // 本提案这里的 extern 变为必需。 extern class C {}library b; // 本提案这里的 library a 变为必需。 extern library a class C; extern fn F() - C*;library c; import library a; extern fn F() - C*;拥有方extern声明的两个关键效果在拥有方声明如extern class C {}上写extern产生两个效果该声明必须被显式导入才是完整的complete。所谓显式导入指存在某条 import 路径使该名字可用于名字查找name lookup包括export import与export name形式。允许但不要求存在一个非拥有的extern library owning_library声明。还有一个对称性约束如果两个拥有方声明前向声明与定义中任何一个带extern那么两个都必须带。这一规则避免了一个带一个不带造成的歧义。一个隐含的通用规则修饰符何时必须书写提案在 Abstract 中顺带确立了一条新的经验法则rule of thumb修饰符关键字在删除此前可选声明后缺少该修饰符会改变行为时是必需的。也就是说不能因为另一个声明上已经写了就省略当前声明上的修饰符——这正是要求extern同时出现在拥有方前向声明与定义之上的理由详见后文备选方案部分。细节类型一致性Type Coherency的实现机制完整类型只在显式导入定义时产生在问题示例中无论C来自拥有方声明还是非拥有方声明它都产生同一个类型因此两个F签名类型完全一致。具体做法是仅当拥有方定义C被按名字导入时才产生完整类型要么直接import library a要么通过export import library a加export C;的链路间接导入否则一律使用不完整类型incomplete type。这带来一个副作用给拥有方声明加上extern会改变导入语义对没有显式导入该类型的 API 使用者来说属于潜在的破坏性变更breaking change。在有extern library a class C;的情况下必需的import library b保证所有拥有方extern class C声明都能看到非拥有声明并将其作为名称冲突name collision合并。编译器因此可以轻松地把同一个类型应用到所有声明上进而确保同时导入两个库的库能理解类型的相等性。对间接导入的影响标记为extern的实体只有在定义被显式导入时才完整。提案以库o中若干间接、非显式的使用为例library m; extern class C { fn Member(); }library n; import library m; fn F() - C; var c: C {}; var pc: C* c;library o; import library n; // 无效C 的返回类型不完整函数签名因此无效。 fn G() { F(); } // 无效访问成员要求 C 完整。 fn UseC() { c.Member(); } // 有效取 C 的地址不要求它完整。这可行是因为 没有扩展点extension point。 var indirect_pc: auto c; // 无效复制 C 需要完整类型。 var copy_c: auto c; // 有效指针到指针的复制没问题。 var copy_pc: auto pc;关键判据是操作是否要求类型完整成员访问、复制、按值返回都要求完整类型因而在间接导入时无效取地址c和指针复制则只涉及不完整类型即可完成。其中没有扩展点这一点保证了c的行为可预测相关讨论记录在 #typesystem 频道的 Willhave an extension point? 中。非extern类型的间接导入不受此约束上述规则明确不适用于非extern类型由 Issue #4025 决定。对普通类间接导入依然能拿到完整类型library a; class C { fn F(); }library b; import library a; fn G() - C;library c; import library b; // 有效C 在这里是完整的即使它不在名字查找中。 G().F();这里库c从未直接导入aC也不在名字查找范围内但G().F()仍可正常使用C的完整定义。使用已导入的声明允许声明前引用由于extern library a class C;必须被拥有库导入提案允许在同一文件中、实体声明之前使用已导入的名字。这是对 #3762 的一个分歧点#3762 不允许意味着下面的代码现在合法library extern; extern library use_extern class MyType;library use_extern; import library extern // 使用 extern library 声明。 fn Foo(val: MyType*); extern class MyType { fn Bar[addr self: Self*]() { Foo(self); } }在MyType的拥有定义出现之前Foo(val: MyType*)已经引用了从库extern导入的MyType而Bar的定义体内调用Foo时MyType已就绪。这消除了为引用前使用而被迫在实现文件里重复前向声明的需求这一动机也是备选方案中四声明模型被否定的原因之一。private extern的处理在 #3762 中非拥有的private extern是合法的可以在不暴露名字的前提下把某实体声明为 extern。本提案下对应写法应是为非拥有方private extern library owning_library加上一个拥有方 publicextern声明但这种语法被判定为无效——因为名字永远不可能对拥有库可见。相反extern library owning_library声明与拥有方extern声明的可见性必须一致。需要注意的是由于拥有方extern声明可以不依赖extern library owning_library独立使用API 文件中的拥有方private extern声明是合法的它没有任何特殊行为按普通方式合并即可。非拥有extern library声明的校验提案要求对extern library中指定的库提供一定的校验。当拥有库写错时大概率会在两种场景被捕获编译期错误拥有库导入非拥有库、且拥有方声明被求值时报编译错误链接期错误作为兜底。其他场景如两个库被独立导入、互不依赖是否报错取决于校验成本可能捕获也可能不捕获。这与编译器避免在完全合法的代码上做额外工作的总体取向一致见下文备选方案中的性能权衡讨论。非拥有extern library声明只做语义匹配#3763 为同库内的重声明建立了语法匹配syntactic matching机制但本提案明确非拥有的extern library声明之间的重声明只使用语义匹配semantic matching#3763 的语法匹配仅适用于同一库内的拥有声明可包括拥有方extern声明。原因在于名字查找数据的差异。同库内重声明时语法匹配实质上是语义匹配的超集这依赖于名字查找表中投毒poisoning条目、后续重声明看到相同查找数据而不同库的名字查找数据不同跨库场景下语法匹配不再是语义匹配的超集。提案给出的例子library a; class A {} namespace NS; extern library c fn NS.F() - A;library b; namespace NS; class A {}library c; import library a import library b extern fn NS.F() - NS.A {}语义上库a与库c中的NS.F完全一致但语法上不同——库c写的是NS.A。反过来在库c中直接写A会因名字查找命中库b的NS.A而非法而在库a中写A则没有问题要等跨库编译完成后才会暴露。另一个更微妙的反例说明语法匹配跨库可能失效library d; class D {} namespace NS; extern library e fn NS.G() - D;library e; namespace NS; alias NS.D D; extern fn NS.G() - D {}这里语义和语法都吻合但NS.D D的 alias 使得名字查找结果不同在普通重声明中这本来是非法的。#3763 用以论证语法匹配的那句话——只要语法匹配语义必然匹配——在跨库场景下不再成立即便写成alias NS.D i32;语法上依然会匹配。原因正如 #3763 所述我们把语法信息从 API 文件持久化到实现文件而语法信息无法跨库、跨导入持久化。结论非拥有的extern library声明不强制语法匹配只做语义匹配。语义匹配会包含参数名两种做法的分歧主要在于产生相同类型信息的不同写法是否被判定为无效。设计动机Rationale提案给出的设计动机与 docs/project/goals.md 中的项目目标直接挂钩软件与语言演进Software and language evolution统一extern实体的类型解决了类型一致性问题extern要求显式导入的行为意在帮助库作者仔细管理其 API 的依赖。快速与可扩展的开发Fast and scalable development要求拥有库导入非拥有extern library声明预期能提升编译器性能使类型可通过唯一标识符合并避免全量规范化。同时提案明确承认与与既有 C 代码的互操作与迁移这一目标存在权衡extern声明唯一化预计会给迁移带来额外工作量因为 C 的extern声明需要被合并归拢。这一权衡目前被其他收益抵消但可能导致该方面被重新评估。工具链落地源码与测试印证提案发布后extern library机制已在 Carbon 工具链中逐步实现可从当前仓库的源码与测试中印证关键规则。语义信息存储toolchain/sem_ir/entity_with_params_base.h 中每个实体函数、类等携带与extern直接相关的字段is_extern声明是否带extern修饰extern_library_id对extern library声明记录所属库名LibraryNameIdnon_owning_decl_id实体的非拥有声明一个entityDecl若存在first_owning_decl_id第一个拥有方声明可能是前向声明也可能就是定义本身definition_id在定义处{时设置的实体定义。这一数据结构正是三声明模型的直接体现非拥有声明、首个拥有声明、定义被显式区分并在语义分析过程中逐一填充。修饰符校验toolchain/check/modifiers.cpp 的RestrictExternModifierOnDecl负责限制extern的使用位置产出多个关键诊断extern只能用于文件作用域或命名空间作用域ModifierExternNotAllowedextern library不能指定当前库ExternLibraryIsCurrentLibrary即自己声明自己拥有的非法情形定义definition上不能带extern library 库ExternLibraryOnDefinition——extern library只允许出现在非拥有的前向声明上而定义只能是extern class C {}这种形式。extern library中库名文字量的解析位于 toolchain/check/handle_modifier.cpp。重声明的合并与匹配toolchain/check/merge.cpp 实现了跨库重声明的校验逻辑对应提案中的多条规则extern修饰必须匹配RedeclExternMismatch拥有方声明必须位于 API 文件ExternRequiresDeclInApiFile在导入方importer中不能声明extern libraryExternLibraryInImporterextern library指定的库必须与实际的拥有库一致ExternLibraryIncorrect。toolchain/check/check_unit.cpp 的CheckRequiredDeclarations则落实了拥有库必须包含拥有方声明的要求当存在非拥有声明extern_library_id指向当前库却缺少first_owning_decl_id时报告MissingOwningDeclarationInApiowning declaration required for non-owning declaration。导入时的类型平移toolchain/check/import_ref.cpp 在导入引用时对extern_library_id进行翻译若被导入实体带有非None的库名会将字符串字面量值转换为导入方语义 IR 中的LibraryNameId从而在跨库导入后依然能关联到正确的拥有库。decl_name_stack.htoolchain/check/decl_name_stack.h则在建立声明时根据是否存在extern_library分别记录non_owning_decl_id或作为拥有声明处理。测试用例仓库中的文件测试file_test直接覆盖了提案的核心规则toolchain/check/testdata/function/declaration/extern_library.carbon覆盖函数extern library的正反用例包括拥有库正确导入extern_library.carbonextern_library_owner.carbon、非拥有库中extern声明不匹配ExternLibraryIncorrect、extern修饰不一致RedeclExternMismatch、重复extern library声明的名字冲突NameDeclDuplicate、extern library指向当前库ExternLibraryIsCurrentLibrary、以及定义上带库名ExternLibraryOnDefinition等场景toolchain/check/testdata/class/extern_library.carbon类的extern library场景当前仍以SemanticsTodo报错说明类实体上的完整支持仍在推进中文件中标注为 TODO。这些测试可以这样运行bazel test //toolchain/testing:file_test --test_arg--file_teststoolchain/check/testdata/function/declaration/extern_library.carbon bazel run //toolchain/testing:file_test -- --dump_output --file_teststoolchain/check/testdata/function/declaration/extern_library.carbon可以看到函数形态的extern library已具备完整的诊断与语义支持而类形态仍在开发报SemanticsTodo——这正是提案逐步落地的真实写照。未来工作extern与模板的交互提案坦承模板与extern的交互只做了粗略讨论。当前的预期是当模板声明使用extern类型时实例化instantiation仍发生在调用文件中因此extern类型的名字需要在声明模板的文件和调用模板的文件两处都被导入。当模板与extern类型同属一个包时模板可以重新导出re-export该类型但跨包重新导出名字目前不受支持且类似let template ExternType:! auto OwningPackage.ExternType;的写法不会转发ExternType的完整性。提案预计这会带来不便但如果extern使用量有限则尚可接受也不排除最终的模板模型与此预期不同。备选方案与取舍提案记录了七大类备选方案及被否定的理由理解这些取舍有助于把握设计意图。允许多个非拥有声明 / 取消导入要求或两者兼有将非拥有extern library声明限制为一个继续允许多个旧状态在技术上可行不要求拥有方导入非拥有声明也可行无论是否允许多个。这些方案的问题相似编译器希望通过唯一标识符如 32 位整数判定类型相等。当一个声明通过导入直接看到另一个声明时按名字识别重声明并复用唯一标识符去重每声明只需一次间接导入可继续沿用唯一标识符。若要支持互不可见的声明合并就必须改为按名字对所有类型做规范化代价巨大。提案用五库示例说明merge库没有直接看到MyType的任何声明但Print(Make())要求两处MyType声明被判定等价此时两个声明都未进入名字查找没有理由按名字关联它们。要合并它们需要在类型被使用时包括接口查找等非显式使用比对完全限定名与结构细节例如为每个库维护使用中类型的名字查找表并逐库验证声明语义等价——这会显著增加语义分析阶段的工作量。为保持高性能编译器提案选择了更受约束、更易关联类型信息的方案。声明总数拥有 非拥有的三种选择不限制前向声明数量与 C 一致可任意多甚至允许出现在定义之后。问题在于修饰符匹配会成为维护负担全匹配则繁、不匹配则语义含糊且没有明确收益故否决。总共只允许两个声明把非拥有extern library声明当作前向声明总共两个。主要顾虑是接口实现的文件位置与定义的冲突——接口实现通常必须在 API 文件中才能被其他库看到。若强制定义也在 API 文件API 文件就得导入构造定义所需的所有库破坏构建依赖分离、难以解开库间依赖环若允许定义放在实现文件则看到非拥有extern library声明时无法确定它是否就是拥有库影响接口约束求值。允许一个拥有前向声明的目的正是让 API 文件在处理时能明确接口实现存在于拥有库中。总共允许四个声明非拥有声明 API 文件前向声明 实现文件前向声明 定义。这与 #3762 的现状一致但允许从其他文件使用未声明实体之后实现文件里重复前向声明的动机消失了而且讨论中大家觉得允许两个前向声明却不允许更多缺乏理由更流行的选择反而是不限制同样被否决。拥有声明上不要求extern修饰可以从非拥有extern library声明的存在推断拥有方意图。在 #3762 讨论中曾因拥有库无需包含extern声明、opt-in 收益低而否决但如今拥有库必须导入extern声明、关联更紧密故重新评估。最终保留extern修饰的理由能验证非拥有声明与拥有声明的关联、提供修饰符的对等性、并让工具容易发现缺少声明。只在第一个拥有声明上要求extern如extern class C;后接class C {}由前向声明推断定义也为extern。被否决的原因是前向声明是可选的若允许省略就必须在定义上重复关键字——这正是 Abstract 中经验法则删除可选声明后缺少修饰符会改变行为则该修饰符必需的具体应用。将必须直接导入与非拥有声明分离extern修饰符同时承载两个职责①指示非拥有extern library声明可以存在②指示声明必须被直接导入才完整。合并成一个语法的问题拥有声明上的extern无法用来推断非拥有声明是否存在其位置不显式开发者可能误解为不存在且被拥有库导入的库可以自由增删非拥有声明而不改动拥有库。提案倾向于单一语法承载两个目的而不是强调控制或对应关系。其他extern语法Issue #3986 讨论了has_extern/is_extern/externed等替代命名。拆解extern可得到两个可分离的特性①声明某实体在另一个库中有前向声明对应extern library owning_library②声明某实体必须被直接导入。虽然①依赖②但可设计为只提供②的独立关键字如must_import。最终取舍主要动机是提供特性①Leads 希望在拥有声明上使用对自身做正面陈述的语法即特性②认为②独立于①有价值并接受让①变为可选即extern library声明可选、可在不改动拥有库的情况下增删extern作为命名足够好且只引入一个新关键字extern library owning_library虽然冗长但冗长落在非拥有库的前向声明上恰好给读者指明寻找真正声明的方向若实践中冗长成为显著问题可考虑extern library ... { 多个前向声明 }的成组语法。让包含extern成员的类重新导出它们预期会出现拥有extern成员的类型这类类型只有成员完整时才真正完整。曾讨论让这类类型自动重新导出其extern成员可能还要求这类类型自身也是extern通过导入链d 导入 a 与 cc 加载 B、B 加载 A 的定义使var a: A;合法。提案认为这是远距离动作action-at-a-distance类型一致性要求B的成员A与名字查找中的A相同若让它们行为略异就陷入类型信息的溯源provenance追踪。最终决定回避该机制坚持A必须被刻意导入B才算完整。对extern library声明要求语法匹配不要求语法匹配但技术上可行。同库重声明时名字查找被设计为语法匹配是语义匹配的超集依赖投毒条目与一致的查找数据跨库则因查找数据不同而不成立。为消除这一分歧只要求语义匹配。语义匹配包含参数名差异主要在于产生相同类型信息的不同写法是否被判定为非法。由于语法匹配对拥有声明与非拥有声明提供的保证不同非拥有的extern library声明不强制语法匹配。总结提案 p003980 为 Carbon 的extern机制建立了一套一个非拥有声明 一个拥有前向声明 一个拥有定义的严格三声明模型并以定义必须被显式导入才产生完整类型为核心达成跨库类型一致性。它同时确立了修饰符在可省声明被删除后仍影响行为时必需的通用判定准则以及跨库重声明只做语义匹配的边界。仓库中 entity_with_params_base.h、modifiers.cpp、merge.cpp、check_unit.cpp 等实现与extern_library系列 file_test 用例已经从函数实体开始将这套设计落地为可验证的编译器行为。【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考