- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
在 Dart AOT(预编译)模式下,嵌入宿主(embedder)或 Native 代码需要通过 Native Dart API 反向调用 Dart 中的类、成员或闭包,但预编译器在执行 tree shaking 与 TFA(Type Flow Analysis)时会把“编译器看不见的调用”全部裁掉。@pragma("vm:entry-point", ...)就是为解决这一矛盾而设计的强制标注:它显式声明哪些 Dart 实体可作为入口点(root)被 Native/VM 代码解析、分配或调用,并在启用混淆(obfuscation)时保证对应符号名得以保留。读完本文,你将掌握该 pragma 在类、getter、setter、构造器、普通过程与字段上的完整语法与语义,并能从 precompiler.cc 等源码层面理解标注是如何被解析、校验并转化为“保留原因”(retain reason)的。
一、为什么 AOT 模式必须声明入口点
Dart VM 的预编译器(AOT compiler)会对整个程序做全程序优化,核心手段包括:
- Tree shaking(死代码消除):凡是从可达性分析角度看不到调用者的函数与类,都不会进入最终镜像;
- TFA(Type Flow Analysis):基于类型流分析推断各调用点/分配点的精确类型,以生成更紧凑的代码。
这类优化隐含一个前提:编译器能看到完整的 Dart 程序,能发现并分析所有运行时“可能执行”的函数与成员。Dart 侧代码确实全部可见,但嵌入宿主的 C++ 代码与 native 方法对编译器而言是不可达的黑盒——它们通过 Native Dart API 回调 Dart。于是预编译器无法知道哪些 Dart 实体会被这些外部代码触达。
这带来两条硬性规则(来自 官方文档):
- 标注是强制的(must):只要程序中定义了会回调 Dart 的 native 方法,就必须显式列出被 Native 代码访问的类与成员,否则编译结果在运行时可能出错(例如解析不到类名、调用到已被裁剪的函数)。它不是可选的“优化提示”,而是正确性前提。
- 混淆场景的符号保留:启用 obfuscation 时,预编译器还需要知道哪些符号不能被改名,否则 Native 代码按名字解析会失败。
另外,为了缩小 JIT 与 AOT 的行为差异,JIT 模式同样会检查入口点标注,唯一的例外是dart:mirrors的用法以及通过 VM service 进行的调试访问——这两种通道在 JIT 下天然拥有“全程序可见”的能力,不需要 pragma 背书。
二、标注语法与语义
@pragma("vm:entry-point", ...)必须放在目标类或成员上。所有位置都支持同一组“第二参数”形式,表示入口点的有效性条件:
@pragma("vm:entry-point") // 无条件生效 @pragma("vm:entry-point", true/false) // 常量布尔 @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product")) // 仅非 product(调试)模式由于第二参数是常量布尔表达式,!const bool.fromEnvironment("dart.vm.product")这种写法可以在product(发布)构建中整体剔除该入口点,从而在发布镜像中不保留仅用于开发期调试的回调函数。
以下按文档的六类承载对象逐一说明(以下代码块均继承自 entry_point_pragma.md,可逐字复用)。
2.1 类:允许从 Native 代码分配
@pragma("vm:entry-point") @pragma("vm:entry-point", true/false) @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product")) class C { ... }- 第二参数缺失、为
null或true时,该类可以被 Native 或 VM 代码直接分配(例如通过Dart_New系列 API 按名解析类并创建实例)。 - 该标注可以加在抽象类上:此时类名会在混淆后幸存(符号保留),但不会生成任何分配桩(allocation stubs),即只保留“名字”,不可实例化。
2.2 Getter:允许通过 Dart_GetField 取值
@pragma("vm:entry-point") @pragma("vm:entry-point", true/false) @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product")) @pragma("vm:entry-point", "get") void get foo { ... }"get"表示允许通过Dart_GetField获取 getter 的值。- 注意一个易错点:
Dart_Invoke只能用于返回闭包值的 getter。此时实际流程等价于先用Dart_GetField取出闭包、再用Dart_InvokeClosure调用它——因此这类用法同样需要"get"标注,单独用"call"是不正确的。 - 第二参数缺失/
null/true时,语义与"get"相同。 - getter不能被 closurize(取不出它的闭包对象)。
2.3 Setter:允许通过 Dart_SetField 赋值
@pragma("vm:entry-point") @pragma("vm:entry-point", true/false) @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product")) @pragma("vm:entry-point", "set") void set foo(int value) { ... }"set"表示允许通过Dart_SetField写入。- 第二参数缺失/
null/true时,语义与"set"相同。 - setter 同样不能被 closurize。
2.4 构造器:允许被调用(生成式构造器需连带类标注)
@pragma("vm:entry-point") @pragma("vm:entry-point", true/false) @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product")) @pragma("vm:entry-point", "call") C(this.foo) { ... }"call"表示构造器可被直接调用(经Dart_Invoke按名解析并调用)。- 第二参数缺失/
null/true时,语义与"call"相同。 - 关键约束:如果构造器是生成式(generative)的,其所在类必须同时带有入口点标注,使类可从 Native 或 VM 代码分配。
- 构造器不能被 closurize。
2.5 其他过程:区分“取闭包”与“直接调用”
@pragma("vm:entry-point") @pragma("vm:entry-point", true/false) @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product")) @pragma("vm:entry-point", "get") @pragma("vm:entry-point", "call") void foo(int value) { ... }对普通函数/方法,语义与 getter/setter 不同,需要区分两种访问方式:
"get":仅允许 closurization,即通过Dart_GetField取出该函数的闭包对象(后续可用Dart_InvokeClosure调用);"call":仅允许通过Dart_Invoke直接调用;- 第二参数缺失/
null/true:两种访问方式都允许。
2.6 字段:get / set / init 三种粒度
对非静态字段,以下形式都允许;其中前三种形式(无条件、null、布尔)同样可用于静态字段:
@pragma("vm:entry-point") @pragma("vm:entry-point", null) @pragma("vm:entry-point", true/false) @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product")) @pragma("vm:entry-point", "get"/"set"/"init") int foo;语义分三种情况:
- 缺省 /
null/true:字段整体标记为可供 Native 访问;对非静态字段,其所在类接口上对应的 getter 和 setter同时被标记为可供 Native 调用。 "get"或"set":只标记对应的 getter 或 setter 之一。"init":将该字段(可以是final)标记为“由 Native 代码初始化”(例如在external构造器中完成赋值),不标记其 getter/setter。- 对静态字段,只要字段被标记为可访问,其隐式 getter 就总是被一并标记。
还有一个与闭包交互的细节:一个持有闭包的字段,只有在其 getter 被标记后,才能用Dart_Invoke调用其中的闭包——这等价于先用Dart_GetField从字段取出闭包,再用Dart_InvokeClosure调用。
三、源码实现:标注如何被解析为“入口点”
3.1 符号与枚举定义
pragma 名字vm:entry-point本身注册在 VM 的符号表中,见 symbol_list.h:
V(vm_entry_point, "vm:entry-point")解析结果被归约为一个枚举,并配有静态判断工具类,定义在 object.h:
enum class EntryPointPragma { kAlways, // 无条件生效 kNever, // 不生效(含显式 false) kGetterOnly, // "get" kSetterOnly, // "set" kCallOnly, // "call" kTypeOnly // 仅类型可访问(不产生访问桩) }; struct EntryPointPragmaUtils : public AllStatic { static constexpr bool AllowsCall(EntryPointPragma pragma) { ... } static constexpr bool AllowsGet(EntryPointPragma pragma) { ... } static constexpr bool AllowsSet(EntryPointPragma pragma) { ... } static constexpr bool AllowsTypeAccess(EntryPointPragma pragma) { ... } static constexpr bool AllowsAccess(EntryPointPragma pragma) { // kNever 与 kTypeOnly 不允许实例级访问 return pragma != EntryPointPragma::kNever && pragma != EntryPointPragma::kTypeOnly; } };这里的语义矩阵与文档完全对应:kAlways允许 call/get/set 三种访问,kGetterOnly/kSetterOnly/kCallOnly各自放行一种,kTypeOnly只放行“类型访问”(对应文档中抽象类“名字幸存但无分配桩”的行为),kNever全部拒绝。
3.2 解析函数 FindEntryPointPragma
把@pragma("vm:entry-point", X)的第二参数映射到上述枚举的核心函数是 object.cc 中的 FindEntryPointPragma。从源码结构看,它遍历目标实体 metadata 上的所有@pragma,识别vm:entry-point(以及与动态模块相关的同类标注),然后按第二参数取值返回:
| 第二参数取值 | 返回枚举 | 对应文档语义 |
|---|---|---|
缺失 /null/true | kAlways | 完全可用 |
"get" | kGetterOnly | 仅允许取值/取闭包 |
"set" | kSetterOnly | 仅允许赋值 |
"call" | kCallOnly | 仅允许调用 |
其余(如false) | kNever | 入口点被禁用 |
也就是说,文档中@pragma("vm:entry-point", false)这种写法在实现层的含义是:该成员显式声明“不是入口点”(解析为kNever),可用于表达“此标注在某个变体下失效”的条件组合。
3.3 预编译器的入口点收集流程
真正的“根”收集发生在 AOT 预编译器里。precompiler.cc 的 MarkEntryPointsFromNativePragmas 相关逻辑 遍历每个库中的每个类,按顺序处理三类带 pragma 的成员:
- 类级标注:若
AllowsAccess成立,则AddInstantiatedClass+AddApiUse——把类加入实例化清单并保留其 API 可见性(混淆时保名);若只是AllowsTypeAccess(即kTypeOnly),则只保留 API 与类型,不产生分配桩。 - 字段级标注:被标记的字段本身通过
AddField/AddApiUse保留;对非静态字段,按AllowsGet/AllowsSet把该字段记入隐式 getter / 隐式 setter 清单;静态字段记入隐式静态 getter 清单(与文档“静态字段被标记时隐式 getter 总是被标记”一一对应)。 - 函数级标注:
- 隐式 getter/setter只有在其对应字段被标记时才补标(
mark_implicit_method_if_field_saved)——这解释了为什么字段标注会“连带”接口的访问器; - 显式 getter 需
AllowsGet、显式 setter 需AllowsSet、构造器需AllowsCall,且生成式构造器会触发AddInstantiatedClass,强制把所在类加入可分配清单——这正是文档中“生成式构造器要求类也带标注”的实现保证; - 普通过程:
AllowsCall时保留函数本体;AllowsGet时则对其ImplicitClosureFunction()(隐式闭包函数)调用mark_entry_point_and_api_methods。代码注释明确写道:Dart_GetField是通过原函数取得隐式闭包函数的,因此 API 保留记在原函数上,而实际被保留的代码实体是闭包函数。
- 隐式 getter/setter只有在其对应字段被标记时才补标(
被标记的函数最终写入functions_with_entry_point_pragmas_集合,并以RetainReasons::kEntryPointPragma作为保留原因(即不会被 tree shaking 裁掉)。
3.4 混淆下的符号保留
启用 obfuscation 后,光保留代码还不够,符号名也必须幸存。precompiler.cc 中,凡是命中functions_with_entry_point_pragmas_的函数会额外追加RetainReasons::kEntryPointPragmaSignature保留原因,确保其签名(可被 Native 侧按名解析的符号)在混淆后不变。这就是文档 Background 一节所说“precompiler 需要知道哪些符号需要被保留”的落点。
3.5 JIT 模式下的运行时校验
文档提到 JIT 模式也会检查入口点标注(mirrors 与 VM service 调试除外)。从源码结构看,VM 运行时在解析/调用路径上同样调用EntryPointPragmaUtils::AllowsGet/AllowsSet/AllowsCall来校验函数是否被允许以某种方式访问(参见 object.cc 中对 expected 访问方式的判定)。这意味着即使你在 JIT 下“侥幸能跑”,一旦切到 AOT,缺少正确标注的入口点会直接失效——以 AOT 能正确解析和调用为准是标注完整性的验收标准。
四、真实用例:从 samples 到测试代码
仓库内的 embedder 示例提供了可直接对照的最小实践。samples/embedder/timer.dart 展示了一个典型的“Native 驱动 Dart 定时器”入口面:
@pragma('vm:entry-point', 'call') void startTimer(int millis) { ... } @pragma('vm:entry-point', 'call') void stopTimer() { ... } @pragma('vm:entry-point', 'call') void resetTimer() { ... } @pragma('vm:entry-point', 'get') int get ticks => _tickCount;四个函数全部使用精确的单一权限标注:需要被Dart_Invoke调用的用'call',需要被Dart_GetField读值的 getter 用'get'——没有一处滥用缺省的全能标注。samples/embedder/hello.dart 与 program1.dart、program2.dart 则是最简形式,对main级别的入口函数统一标注'call'。dart_api_impl_test.cc 还演示了类级标注(@pragma("vm:entry-point"))配合嵌套类内'call'/'get'函数标注的组合用法,可作为编写自己的 Native API 面时参考的“标注模板”。
五、实践建议与常见陷阱
综合文档与源码,归纳出以下可直接执行的规则:
- 权限最小化:优先使用
"call"/"get"/"set"精确标注,而不是裸@pragma("vm:entry-point")(后者等价kAlways,三种访问全放行)。精确标注在 JIT 下能更早暴露标注错误。 - 生成式构造器连带类标注:只标注构造器而不标注类是无效的——预编译器要求类可实例化,类名与分配路径必须同时保留。
- “调用闭包”不等于
"call":对返回闭包的 getter、或持有闭包的字段,其“调用”路径本质是Dart_GetField+Dart_InvokeClosure,必须标"get"。 - 静态字段与 getter 的连带关系:静态字段被标记后其隐式 getter 自动保留,无需(也不应)再单独标注。
"init"专用于 final 字段:当final字段在external构造器或 Native 初始化路径中赋值时,用"init"标记“该字段由 Native 初始化”,避免误伤 getter/setter 的保留。- 调试专用入口用条件标注:
!const bool.fromEnvironment("dart.vm.product")可让仅开发期使用的回调函数在 product 构建中整体消失,减小镜像体积。 - 验收标准:用 AOT 预编译产物验证——Native 侧按名解析、分配、调用均成功,且(若启用 obfuscation)符号名未被改写。
六、延伸阅读
- pragmas_recognized_by_compiler.md:编译器(含 AOT 预编译器)识别的全部 pragma 一览,
vm:entry-point是其中与 Native 互操作最相关的一个; - pragmas.md:VM 层面 pragma 机制的总览;
- runtime/vm/compiler/aot/precompiler.cc:入口点收集、保留原因与签名保留的完整实现;
- samples/embedder 目录:一组可编译运行的 embedder 示例,展示了 pragma 在实际 C++/Dart 混合工程中的用法。
- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
相关推荐
Dart VM 方法内联 FAQ 深度解读:`@pragma("vm:prefer-inline")` 的条件、实战与 JIT/AOT 差异
Dart VM 方法内联 FAQ 深度解读: @pragma "vm:prefer inline" 的条件、实战与 JIT/AOT 差异 本指南以 docs/F
编程语言编译器语言运行时标准库开发工具Dart SDK 中 dart2js Pragma 注解完全指南:内联、运行时检查与代码优化控制
Dart SDK 中 dart2js Pragma 注解完全指南:内联、运行时检查与代码优化控制 导读 本文基于 Dart SDK 中 pkg/compiler
编程语言编译器语言运行时标准库开发工具终极指南:Ivy编译缓存安全机制如何防止恶意代码注入
终极指南:Ivy编译缓存安全机制如何防止恶意代码注入 在人工智能开发中,编译缓存机制是提升效率的关键技术,但同时也可能成为恶意代码注入的温床。Ivy作为一款强大
人工智能机器学习开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考