☰
Dart SDK 中的 js_shared 包:dart2js 与 DDC 编译期/运行期共享代码的同步机制解析
2026/9/26 7:05:28 网站建设 项目流程
  • 编程语言
  • 编译器
  • 语言运行时
  • 标准库
  • 开发工具

【免费下载链接】sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.

项目地址:https://gitcode.com/gh_mirrors/sdk1/sdk
点击查看免费下载

本篇技术指南以 pkg/js_shared/README.md 为骨架,深入剖析 Dart SDK 中一个特殊的内部包js_shared:它既是 dart2js 与 DDC 两个 Web 编译器共享的编译时依赖(package:导入),又通过lib/synced子目录向dart:运行时库提供同步副本。读完本文,你将理解这套"双份代码、逐字节同步、测试守护"机制的设计意图、目录职责划分、核心常量库的作用,以及如何在 SDK 中安全地维护这两份代码。

一、js_shared 是什么:编译期与运行期之间的"契约层"

在 Dart SDK 中,pkg/js_shared是一个不面向外部发布的内部包(其 pubspec.yaml 明确标注publish_to: none,并声明This package is not intended for consumption on pub.dev. DO NOT publish.)。它的定位由其 README 的第一句话就点明了:

This code is a compile time dependency of dart2js and DDC. It is imported as apackage:import by both compilers.

也就是说,dart2js(pkg/compiler下的 JS 编译器)与 DDC(Dart Dev Compiler,位于pkg/dev_compiler)在编译 Dart 源码时,会以package:js_shared/...的形式导入该包中的代码。这些代码是编译器理解、生成 JavaScript 代码时必须共享的"契约"。

与此同时,这份契约的另一半在运行期一侧:pkg/js_shared/lib/synced目录下的库在 SDK 内部有一份逐字节相同的精确副本,位于sdk/lib/_internal/js_shared/lib/synced。运行时库(即 dart2js 与 DDC 各自生成的运行时 JS 库)会以dart:导入的方式引用sdk/lib/_internal下的这些库。

这种"编译期以package:导入、运行期以dart:导入"的双通道设计,让同一份"语法/语义约定"能够同时被编译器的前端逻辑与运行时的执行逻辑共享——例如编译器按某种编码生成类型信息,运行时按同一种编码解码类型信息,双方必须对编码规则的理解完全一致。

二、目录结构:一份逻辑、两处物理存放

pkg/js_shared的实际目录结构如下:

pkg/js_shared/ ├── lib/ │ ├── synced/ # 与 sdk/lib/_internal/js_shared/lib/synced 保持同步 │ │ ├── async_status_codes.dart │ │ ├── embedded_names.dart │ │ └── recipe_syntax.dart │ └── variance.dart # 编译期使用的类型参数方差枚举 ├── test/ │ └── in_sync_test.dart # 同步守护测试 ├── OWNERS ├── README.md ├── analysis_options.yaml └── pubspec.yaml

而运行期一侧的对应位置sdk/lib/_internal/js_shared/则包含更多内容:

sdk/lib/_internal/js_shared/ ├── lib/ │ ├── synced/ # 与 pkg/js_shared/lib/synced 逐字节一致 │ │ ├── async_status_codes.dart │ │ ├── embedded_names.dart │ │ └── recipe_syntax.dart │ ├── convert_utf_patch.dart # 非同步的运行时补丁库 │ ├── date_time_patch.dart │ ├── http_patch.dart │ ├── js_interop_patch.dart │ ├── js_interop_unsafe_patch.dart │ ├── js_types.dart │ ├── js_util_patch.dart │ └── rti.dart └── js_types_sources.gni

从对比中可以清楚地看到:只有synced子目录下的三个文件是强制双份同步的,其余如rti.dart、js_types.dart、各类*_patch.dart只存在于运行期一侧,属于运行时内部实现,不需要也不应当出现在pkg/js_shared中。js_types_sources.gni(sdk/lib/_internal/js_shared/js_types_sources.gni)则是 GN 构建文件中声明的源文件清单,当前包含lib/js_types.dart。

从pkg/js_shared/pubspec.yaml还可以读出两个环境事实:

  • SDK 约束为sdk: '^3.12.0-0',说明该包随 Dart 3.12 及之后的 SDK 版本演进;
  • 采用resolution: workspace,依赖版本由 DEPS 文件统一管控(注释明确写到 "Use 'any' constraints here; we get our versions from the DEPS file."),开发依赖包含_fe_analyzer_shared、expect(同步测试使用)与lints。

三、synced 三件套:编译器与运行时共同依赖的核心常量

lib/synced下的三个文件是这套共享机制的技术核心。它们本身不包含复杂逻辑,而是以"常量 + 谓词"的形式固化了两套系统都必须遵守的约定。

3.1 async_status_codes.dart:async 机制的状态机协议

async_status_codes.dart 定义了经编译器转换后的sync*/async/async*函数体与js_helper中的辅助函数之间通信用的状态码:

常量值语义
SUCCESS0正常完成
ERROR1出错
STREAM_WAS_CANCELED2async*对应的流已被取消

文件后半部分专门服务于sync*函数转换:转换后的函数体body会返回以下代码,向_SyncStarIterator(迭代器)报告最新状态:

常量值语义
SYNC_STAR_DONE0sync*函数体已终止,不应再次调用
SYNC_STAR_YIELD1函数体已把yield的值写入迭代器的_current字段
SYNC_STAR_YIELD_STAR2函数体更新了迭代器所持有的 Iterable,其元素即迭代器后续的值
SYNC_STAR_UNCAUGHT_EXCEPTION3函数体抛出异常,异常已保存在迭代器的某个字段上

这份协议之所以要"同步"存放,是因为编译器负责生成返回这些状态码的 JS 代码,而运行时(js_helper)负责解读这些状态码并驱动迭代器——任何一端的修改都必须同步到另一端,否则就会出现"编译器发出SYNC_STAR_YIELD_STAR而运行时按旧语义处理"的隐性 bug。

3.2 embedded_names.dart:嵌入式全局名与类型系统钩子

embedded_names.dart 定义了编译器在生成的 JS 中嵌入的全局名字与符号,供运行时的类型系统(dart:_rti)使用。主要包括:

  • 嵌入式全局常量:RTI_UNIVERSE = 'typeUniverse'(dart:_rti使用的 Universe 对象)、ARRAY_RTI_PROPERTY = 'arrayRti'(在 JS Array 实例上存放类型信息的属性,在 IE11 之外均为 Symbol)、TYPES = 'types'(程序用到的类型列表,用于反射或函数类型编码;注释明确建议通过JsBuiltin.getType而非直接访问该全局)。

  • JsGetName枚举:列出JS_GET_NAME所支持的名字,例如 getter/setter 前缀(GETTER_PREFIX、SETTER_PREFIX)、调用前缀(CALL_PREFIX至CALL_PREFIX5、CALL_CATCH_ALL)、参数属性(REQUIRED_PARAMETER_PROPERTY、DEFAULT_VALUES_PROPERTY、CALL_NAME_PROPERTY、DEFERRED_ACTION_PROPERTY)、生成类型测试属性前缀OPERATOR_IS_PREFIX、函数类型签名名SIGNATURE_NAME,以及在参数化类实例上存放运行时类型信息的属性名RTI_NAME;还包括Future、null、Object、List类的类型名字符串、记录(record)原型上的RECORD_SHAPE_TAG_PROPERTY与RECORD_SHAPE_TYPE_PROPERTY,以及Rti._as/Rti._is字段属性名等。

  • JsBuiltin枚举:用于JS_BUILTIN内建调用,包括获取 DartObject构造函数(可用于obj instanceof constructor式类型测试)、获取运行时Closure基类构造函数、判断某类型是否为 js-interop 类型实参(isJsInteropTypeArgument)、按索引取元数据(getMetadata)与取类型(getType)。文件中的注释直接给出了示例调用形式,例如:

    var constructor = JS_BUILTIN('', JsBuiltin.dartObjectConstructor); if (JS('bool', '# instanceof #', obj, constructor)) ...
  • RtiUniverseFieldNames类:固化RtiUniverse 对象各字段的短名字,如evalCache = 'eC'、typeRules = 'tR'、erasedTypes = 'eT'、typeParameterVariances = 'tPV'、sharedEmptyArray = 'sEA'。这些字段名同时被编译器生成的代码与运行时读写,是典型的"必须一致"的约定。

3.3 recipe_syntax.dart:类型 recipe 的编解码文法

recipe_syntax.dart 的库注释直接说明:"Constants and predicates used for encoding and decoding type recipes",并且 "This library is synchronized between the compiler and the runtime system."

所谓 "type recipe",是 Dart 编译器把类型表达式编码成紧凑字符序列的方案。abstract class Recipe定义了这套编码的全部操作符:

  • 分隔与转换:librarySeparator(|)、separator(,)、toType(;);
  • 入栈操作:pushErased(#)、pushDynamic(@)、pushVoid(~);
  • 包装操作:wrapQuestion(?)、wrapFutureOr(/);
  • 类型实参边界:startTypeArguments(<)、endTypeArguments(>);
  • 函数类型实参边界:startFunctionArguments(()、endFunctionArguments());
  • 可选/命名参数分组:startOptionalGroup([)、endOptionalGroup(])、startNamedGroup({)、endNamedGroup(});
  • 命名参数分隔:nameSeparator(:)、requiredNameSeparator(!,表示必填命名参数);
  • 泛型函数类型参数索引:genericFunctionTypeParameterIndex(^);
  • 记录类型起始:startRecord(+);
  • 扩展操作:extensionOp(&),配合pushNeverExtension = 0、pushAnyExtension = 1。

文件同时提供数字与名称组件的谓词:isDigit、digitValue、isIdentifierStart(识别标识符起始字符,含字母、_、$、|),以及period常量。所有底层字符码(从_formfeed = 0x0C到_tilde = 0x7E)都有对应的 int 码与字符串常量,并最终通过testEquivalence()方法逐项断言String.fromCharCode(charCode) == str,确保数字编码与字符编码永远等价。

值得注意的是注释中保留的 TODO:JsGetName枚举条目"应改为小写(如同字段)并寻找更合适的命名",说明该文件仍在持续演进中。

四、variance.dart:编译期的类型参数方差枚举

pkg/js_shared/lib/variance.dart(variance.dart)不属于synced目录,是仅供编译器在编译期使用的库。它定义了类型参数的方差(variance)枚举:

enum Variance { legacyCovariant, covariant, contravariant, invariant }

其注释强调:"This needs to be kept in sync with values ofVarianceindart:_rti."——即该枚举的取值顺序必须与运行时dart:_rti中的Variance保持一致。由于 Dart 枚举是按声明顺序编号的,编译器(编译期)与运行时(dart:_rti)各自维护一份顺序完全相同的枚举,才能保证类型参数方差信息在编译产物与运行时解读之间不错位。

五、in_sync_test.dart:逐字节守护的同步机制

README 用加粗的*Important*强调:

all code underpkg/js_shared/lib/syncedmust be kept in sync with the runtime (insdk/lib/_internal/js_shared/lib/synced) at all times. Thetest/in_sync_test.darttest verifies this.

in_sync_test.dart 给出了这套守护机制的具体实现,其校验逻辑清晰且严格:

  1. 定位两份目录:以测试脚本自身路径为锚点,解析出../lib/synced/(包侧)与../../../sdk/lib/_internal/js_shared/lib/synced/(SDK 侧)两个绝对目录;

  2. 遍历 SDK 侧目录:对其中每一个文件,要求包侧存在同名文件(Expect.isTrue(packageFile.existsSync(), ...));

  3. 逐字节比对:将两份文件各自readAsBytesSync()后做Expect.listEquals,任何差异都会导致测试失败;

  4. 给出修复指引:失败信息中直接打印出修复命令模板:

    cp <sdk侧目录>/<filename> <包侧目录>/<filename>

也就是说,每次改动synced中的任何一个文件,都必须同时把改动复制到另一份,然后运行in_sync_test.dart验证两份内容逐字节一致。测试从 SDK 侧读取文件清单、再向包侧对齐,意味着"SDK 侧synced目录是权威源",包侧只是它的精确镜像。

从实现还可以推断:该测试以字节(而非 AST 或语义)为单位做等价判断,因此对两份文件的要求是完全一致的文本内容,任何格式化、注释或空白差异都会让同步测试红灯——这保证了编译器拿到的常量定义与运行时拿到的是"同一个字面文本"。

六、维护实践:如何安全地改动共享代码

结合 README 的说明与测试实现,SDK 贡献者在修改js_shared共享代码时应遵循以下流程:

  1. 判断改动是否涉及共享约定:只有lib/synced/下的三个文件(async_status_codes.dart、embedded_names.dart、recipe_syntax.dart)是双份同步的。改动它们时,需要同步更新sdk/lib/_internal/js_shared/lib/synced/下的对应文件;反之亦然。variance.dart虽在包内但不同步,但它与dart:_rti中的Variance枚举存在"顺序一致性"的隐式约定,改动时同样需要核对运行时侧。
  2. 修改权威源并复制:按测试的约定,以sdk/lib/_internal/js_shared/lib/synced/为权威源,改动后将其复制到pkg/js_shared/lib/synced/覆盖对应文件。
  3. 运行同步测试:执行pkg/js_shared下的in_sync_test.dart(该测试以字节为单位比对全部文件,可借助dart test或直接运行脚本)。测试失败时按输出的cp命令修正。
  4. 回归验证编译器与运行时:由于这两份代码同时被 dart2js/DDC 的编译路径与各自运行时库使用,任何语义级改动都应在编译产物与运行时测试两个方向做验证(相关测试位于pkg/compiler、pkg/dev_compiler与sdk/lib/_internal/js_shared的运行时库中)。

七、总结:一张常量表如何支撑两个编译器

js_shared的整套设计可以浓缩为一句话:用"双份逐字节一致的常量库 + 一个同步测试"把 dart2js 与 DDC 的编译前端和它们各自的 JS 运行时牢牢绑定在同一套约定上。类型 recipe 的编解码符号、async 机制的状态码、嵌入式全局名、Rti 字段名、方差枚举顺序——这些看似琐碎的常量,恰恰是编译器"生成什么"与运行时"解读什么"之间不能有半点偏差的接缝。理解了pkg/js_shared的结构、synced三件套的职责与in_sync_test.dart的守护逻辑,你就掌握了在 Dart SDK 中安全维护这条"编译期—运行期契约"的完整方法。

关键路径速查

  • 包级说明文档:pkg/js_shared/README.md
  • 共享常量库:async_status_codes.dart、embedded_names.dart、recipe_syntax.dart
  • 编译期方差枚举:variance.dart
  • 同步守护测试:pkg/js_shared/test/in_sync_test.dart
  • 运行时侧副本:sdk/lib/_internal/js_shared/lib/synced(含rti.dart、js_types.dart及各*_patch.dart等运行时库)
  • 编程语言
  • 编译器
  • 语言运行时
  • 标准库
  • 开发工具

【免费下载链接】sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.

项目地址:https://gitcode.com/gh_mirrors/sdk1/sdk
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询