- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
本篇技术指南以 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 a
package: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中的辅助函数之间通信用的状态码:
| 常量 | 值 | 语义 |
|---|---|---|
SUCCESS | 0 | 正常完成 |
ERROR | 1 | 出错 |
STREAM_WAS_CANCELED | 2 | async*对应的流已被取消 |
文件后半部分专门服务于sync*函数转换:转换后的函数体body会返回以下代码,向_SyncStarIterator(迭代器)报告最新状态:
| 常量 | 值 | 语义 |
|---|---|---|
SYNC_STAR_DONE | 0 | sync*函数体已终止,不应再次调用 |
SYNC_STAR_YIELD | 1 | 函数体已把yield的值写入迭代器的_current字段 |
SYNC_STAR_YIELD_STAR | 2 | 函数体更新了迭代器所持有的 Iterable,其元素即迭代器后续的值 |
SYNC_STAR_UNCAUGHT_EXCEPTION | 3 | 函数体抛出异常,异常已保存在迭代器的某个字段上 |
这份协议之所以要"同步"存放,是因为编译器负责生成返回这些状态码的 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 under
pkg/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 给出了这套守护机制的具体实现,其校验逻辑清晰且严格:
定位两份目录:以测试脚本自身路径为锚点,解析出
../lib/synced/(包侧)与../../../sdk/lib/_internal/js_shared/lib/synced/(SDK 侧)两个绝对目录;遍历 SDK 侧目录:对其中每一个文件,要求包侧存在同名文件(
Expect.isTrue(packageFile.existsSync(), ...));逐字节比对:将两份文件各自
readAsBytesSync()后做Expect.listEquals,任何差异都会导致测试失败;给出修复指引:失败信息中直接打印出修复命令模板:
cp <sdk侧目录>/<filename> <包侧目录>/<filename>
也就是说,每次改动synced中的任何一个文件,都必须同时把改动复制到另一份,然后运行in_sync_test.dart验证两份内容逐字节一致。测试从 SDK 侧读取文件清单、再向包侧对齐,意味着"SDK 侧synced目录是权威源",包侧只是它的精确镜像。
从实现还可以推断:该测试以字节(而非 AST 或语义)为单位做等价判断,因此对两份文件的要求是完全一致的文本内容,任何格式化、注释或空白差异都会让同步测试红灯——这保证了编译器拿到的常量定义与运行时拿到的是"同一个字面文本"。
六、维护实践:如何安全地改动共享代码
结合 README 的说明与测试实现,SDK 贡献者在修改js_shared共享代码时应遵循以下流程:
- 判断改动是否涉及共享约定:只有
lib/synced/下的三个文件(async_status_codes.dart、embedded_names.dart、recipe_syntax.dart)是双份同步的。改动它们时,需要同步更新sdk/lib/_internal/js_shared/lib/synced/下的对应文件;反之亦然。variance.dart虽在包内但不同步,但它与dart:_rti中的Variance枚举存在"顺序一致性"的隐式约定,改动时同样需要核对运行时侧。 - 修改权威源并复制:按测试的约定,以
sdk/lib/_internal/js_shared/lib/synced/为权威源,改动后将其复制到pkg/js_shared/lib/synced/覆盖对应文件。 - 运行同步测试:执行
pkg/js_shared下的in_sync_test.dart(该测试以字节为单位比对全部文件,可借助dart test或直接运行脚本)。测试失败时按输出的cp命令修正。 - 回归验证编译器与运行时:由于这两份代码同时被 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.
相关推荐
Dart SDK 中 dart2js 编译器架构、编译阶段与源码代码组织全解析
Dart SDK 中 dart2js 编译器架构、编译阶段与源码代码组织全解析 dart2js 是 Dart SDK 中把 Dart 代码编译成可部署 Java
编程语言编译器语言运行时标准库开发工具Dart SDK 中 dart2js Pragma 注解完全指南:内联、运行时检查与代码优化控制
Dart SDK 中 dart2js Pragma 注解完全指南:内联、运行时检查与代码优化控制 导读 本文基于 Dart SDK 中 pkg/compiler
编程语言编译器语言运行时标准库开发工具google_maps_flutter_ios_sdk9 贡献指南:掌握 iOS SDK 9 包与共享代码同步机制
google_maps_flutter_ios_sdk9 贡献指南:掌握 iOS SDK 9 包与共享代码同步机制 本指南以 google_maps_flutt
跨平台移动开发UI组件开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考