Dart SDK 中的 C++ 嵌入示例:用 Dart 引擎 API 从 C++ 启动 Dart 快照
Dart SDK 中的 C++ 嵌入示例:用 Dart 引擎 API 从 C++ 启动 Dart 快照
本篇技术指南以 Dart SDK 仓库中的 samples/embedder/README.md 为骨架,系统讲解如何利用 Dart 引擎 API(dart_engine.h)在 C++ 二进制中嵌入 Dart VM、加载 Kernel 或 AOT 快照并调用 Dart 函数。你将掌握从构建示例、加载快照、创建 isolate、调用 main 与任意入口点,到自定义消息调度器、处理异步消息的完整实战路径,并了解底层 API 设计原理与源码佐证。
背景:Dart 引擎 API 与两种快照格式
Dart Engine 是 Dart SDK 提供的、面向"把 Dart 代码复用到非 Dart 程序中"的嵌入式 API,其头文件位于 runtime/engine/include/dart_engine.h,配套的说明文档位于 runtime/engine/README.md。
该 API 并不是一个功能完整的独立接口,而是需要与传统的 dart_api.h 配合使用。相比 dart_api.h,它主要补齐了三块能力:
- 完整的 VM 初始化,包括核心库(core libraries)的初始化;
- 更简单的 isolate 消息处理,封装了进入 isolate、进入 scope、调用
Dart_HandleMessage、退出 scope 和 isolate 的一整套流程; - 带锁保护的 isolate 进入/退出函数,避免多线程同时进入同一 isolate 时崩溃。
samples/embedder 目录下的全部示例都可以基于两种快照变体运行,取决于它们依赖的共享库(shared library)变体:
| 快照类型 | 说明 | 对应共享库 |
|---|---|---|
| Kernel snapshot | 编译器产物,需要 JIT 运行时解释执行 | dart_engine_jit_shared / dart_engine_jit_static |
| AOT snapshot | 预编译快照,需要预编译运行时 | dart_engine_aot_shared / dart_engine_aot_static |
在源码层面,快照类型由 runtime/engine/include/dart_engine.h 中的 DartEngine_SnapshotKind 枚举与 DartEngine_SnapshotData 结构体描述:Kernel 快照携带 kernel_buffer 与 kernel_buffer_size,AOT 快照则携带 snapshot_data 与 snapshot_text,二者共用同一个 union。
需要特别注意的一个前提是:快照文件格式并不稳定,因此用于生成快照的 dart 二进制必须与运行它的嵌入方版本匹配。最稳妥的做法是从同一个 checkout 中构建 Dart SDK,参见 Building Dart SDK(其中 create_sdk 目标产物输出于 out/ReleaseX64/dart-sdk)。这直接决定了 samples/embedder 中的示例必须依赖 tools/build.py 产出的同一批构建产物,而不能混用发行版快照。
示例一览与构建方式
samples/embedder 目录包含以下示例,覆盖从"最简调用"到"异步事件循环"的完整梯度:
| 示例 | 演示的核心能力 |
|---|---|
run_main.cc |
最简嵌入:加载快照,调用快照中的 main 函数 |
run_two_programs.cc |
同时创建两个 isolate,把第一个 Dart 程序的返回值作为参数传给第二个 |
run_timer.cc |
在独立线程上运行 isolate 事件循环(队列式消息处理) |
run_timer_async.cc |
使用 std::async 实现自定义消息调度器 |
run_futures.cc |
通过 FFI 回调桥接 Dart Future/Stream 与 C++ std::future/std::promise |
每个示例都通过 samples/embedder/BUILD.gn 中的 GN 模板自动生成 _kernel 与 _aot 两套目标,从而可以用同一种 C++ 源码跑两种快照。
构建 run_main 并运行
以 README 中最核心的两个命令为例,先构建并运行 Kernel 快照变体:
./tools/build.py --mode=release samples/embedder:run_main_kernel && \
out/ReleaseX64/run_main_kernel out/ReleaseX64/gen/hello_kernel.dart.snapshot.
再构建并运行 AOT 快照变体:
./tools/build.py --mode=release samples/embedder:run_main_aot && \
out/ReleaseX64/run_main_aot out/ReleaseX64/hello_aot.snapshot.
两条命令的共同点是:build.py 负责构建 C++ 可执行程序及其依赖的快照,随后可执行文件把快照路径作为唯一的命令行参数传入。Kernel 快照由 application_snapshot 目标生成(产物位于 out/ReleaseX64/gen/ 下),AOT 快照由 aot_snapshot 目标生成。由于 --mode=release 默认架构为 x64,产物目录为 out/ReleaseX64;若构建 debug 版本则去掉 --mode=release 或改用 --mode=debug。
BUILD.gn 中的两套构建配置
从 samples/embedder/BUILD.gn 可以看到,_all_configs 定义了 kernel 与 AOT 两套配置:
_kernel变体:依赖../../runtime/engine:dart_engine_jit_shared(共享版)或dart_engine_jit_static(静态版),快照类型为kernel,并通过gen_kernel_args = [ "--link-platform" ]把平台核心库链接进 Kernel 快照;_aot变体:依赖../../runtime/engine:dart_engine_aot_shared或dart_engine_aot_static,快照类型为aot_snapshot,AOT 训练参数training_args默认置空(示例场景不需要)。
BUILD.gn 同时为每个示例生成带 _static 后缀的静态链接版本(如 run_main_kernel_static、run_main_aot_static),方便把 Dart 引擎直接静态链接进宿主程序。Linux 上还会附加 -Wl,--allow-shlib-undefined 链接选项,以避免 JIT(--no-clang)或 AOT(MSAN)构建失败;macOS 上则补充 codesign entitlements 输入。
另外,BUILD.gn 中有一个平台限制值得注意:run_futures 示例(以及所有 run_futures_* 变体)只在 dart_target_arch == host_cpu 时构建,注释明确说明"FFI can't execute on the VM's simulator"——即 FFI 在 VM 模拟器上无法执行。
run_main:最小化嵌入示例
run_main.cc 是整个嵌入场景的"Hello World"。它的流程可拆为四步:加载快照 → 创建 isolate → 调用 Dart main → 关闭引擎。
1. 加载快照
char* error = nullptr;
DartEngine_SnapshotData snapshot_data = AutoSnapshotFromFile(argv[1], &error);
CheckError(error, "reading snapshot");
AutoSnapshotFromFile 定义在 helpers.h,它会根据当前运行时是否为预编译运行时自动二选一:
inline DartEngine_SnapshotData AutoSnapshotFromFile(std::string_view path, char** error) {
std::string path_string(path);
if (Dart_IsPrecompiledRuntime()) {
return DartEngine_AotSnapshotFromFile(path_string.c_str(), error);
} else {
return DartEngine_KernelFromFile(path_string.c_str(), error);
}
}
也就是说,同一个嵌入程序既可以喂 Kernel 快照(DartEngine_KernelFromFile),也可以喂 AOT 快照(DartEngine_AotSnapshotFromFile),代码无需改动,这正是 README 强调"All examples can run either AOT or Kernel snapshots"的实现基础。两个加载函数均由 runtime/engine/include/dart_engine.h 声明,返回的 DartEngine_SnapshotData 已带正确的 kind 标记,且调用方无需释放其中缓冲。
2. 创建 isolate
Dart_Isolate isolate = DartEngine_CreateIsolate(snapshot_data, &error);
CheckError(error, "starting isolate");
DartEngine_CreateIsolate(见 dart_engine.h)基于快照数据创建一个新 isolate,成功返回 isolate 句柄,失败返回 NULL 并通过 error 输出原因。
3. 进入 isolate 并调用 main
DartEngine_AcquireIsolate(isolate);
Dart_EnterScope();
std::initializer_list<Dart_Handle> main_args{ToDartStringList({"world"})};
CheckError(Dart_Invoke(Dart_RootLibrary(), Dart_NewStringFromCString("main"),
1, const_cast<Dart_Handle*>(main_args.begin())),
"calling main");
Dart_ExitScope();
DartEngine_ReleaseIsolate();
这里示范了三个 API 的配合用法:
DartEngine_AcquireIsolate/DartEngine_ReleaseIsolate:带锁的 isolate 进入/退出。与传统的Dart_EnterIsolate/Dart_ExitIsolate不同,AcquireIsolate会阻塞等待直到能进入 isolate(通过内部锁实现),而Dart_EnterIsolate在其他线程已进入同一 isolate 时会直接崩溃。这一差异在 runtime/engine/README.md 中有明确说明,也是多线程嵌入场景必须使用引擎 API 的原因。Dart_EnterScope/Dart_ExitScope:管理句柄生命周期的作用域。helpers.h中封装了 RAII 版本DartScope(helpers.h)与IsolateScope(helpers.h),并提供了组合两者的模板WithIsolate(helpers.h)。Dart_Invoke(Dart_RootLibrary(), "main", 1, args):调用根库中名为main的 Dart 函数,参数是一个长度为 1 的Dart_Handle数组。
ToDartStringList(run_main.cc)展示了如何把 C++ std::vector<std::string> 转成 Dart List<String>:先通过 Dart_LookupLibrary 找到 dart:core,再用 Dart_GetNonNullableType 取到非空 String 类型,最后用 Dart_NewListOfTypeFilled 预填充并逐个 Dart_ListSetAt 赋值。这是给 Dart main(List<String> args) 传参的标准姿势。
4. 关闭引擎
DartEngine_Shutdown();
DartEngine_Shutdown(dart_engine.h)会停止所有 isolate 并释放全部资源。
Dart 侧入口点标注
run_main 对应的 Dart 程序是 hello.dart:
@pragma('vm:entry-point', 'call')
void main(List<String> args) {
greet(args[0]);
}
关键点是 @pragma('vm:entry-point', 'call') 注解。在 AOT 编译模式下,只有被标注的顶层函数才会被保留为可调用入口点(否则会被 tree-shaking 剔除),所以嵌入方调用的每个 Dart 函数都必须显式标注。C++ 侧调用 Dart_Invoke(Dart_RootLibrary(), ...) 即可命中这个入口点;main 还可以从 args 中读取 C++ 传入的 List<String> 参数(此处传入 "world",程序输出 hi, world!)。
run_two_programs:多 isolate 与跨快照数据传递
run_two_programs.cc 演示的是"一个嵌入方同时管理多个 Dart 快照":它加载两个快照、创建两个 isolate,把第一个 Dart 程序的返回值作为字符串传给第二个 Dart 程序。
Dart_Isolate isolate1 = DartEngine_CreateIsolate(snapshot1, &error);
Dart_Isolate isolate2 = DartEngine_CreateIsolate(snapshot2, &error);
DartEngine_AcquireIsolate(isolate1);
Dart_EnterScope();
Dart_Handle invoke_result = Dart_Invoke(
Dart_RootLibrary(), Dart_NewStringFromCString("getValue"), 0, nullptr);
std::string return_value = StringFromHandle(invoke_result);
Dart_ExitScope();
DartEngine_ReleaseIsolate();
DartEngine_AcquireIsolate(isolate2);
Dart_EnterScope();
std::initializer_list<Dart_Handle> args{
Dart_NewStringFromCString(return_value.c_str())};
Dart_Handle invoke_result2 =
Dart_Invoke(Dart_RootLibrary(), Dart_NewStringFromCString("printValue"),
1, const_cast<Dart_Handle*>(args.begin()));
Dart_ExitScope();
DartEngine_ReleaseIsolate();
要点有三个:
- 任意函数调用,而非仅限
main:isolate1调用的不是main,而是getValue;isolate2调用的是printValue。这印证了 README 中"calls a function from one Dart snapshot and then passes the returned string to another Dart snapshot"的描述。 - 字符串返回值的跨边界转换:
StringFromHandle(helpers.h)先用Dart_IsString校验句柄类型,再用Dart_StringToCString取出 C 字符串并转成std::string。 - isolate 间串行进出:两次调用都遵循"
AcquireIsolate→EnterScope→Dart_Invoke→ExitScope→ReleaseIsolate"的对称结构,确保任何时刻只有一个 isolate 被进入。
对应的 Dart 程序 program1.dart 与 program2.dart 还有一个细节值得注意:它们的 main 都只是 throw 'Unimplemented',真正被调用的是被 @pragma('vm:entry-point', 'call') 标注的 getValue 与 printValue。这说明嵌入方并不依赖 Dart 侧存在可用的 main——快照中任何被标注的顶层函数都可以成为 C++ 的调用入口。运行后程序会输出:
program1 returned: program1
program2 received: program1
事件循环与消息调度器
README 明确指出 run_main 无法处理 isolate 消息,因此"cannot run Dart programs with async functions"。要运行使用异步函数(如 Timer、Future)的 Dart 程序,嵌入方必须提供消息调度机制。run_timer 与 run_timer_async 分别给出了两种调度实现,其共同点是都通过引擎 API 的消息调度器接口工作。
消息调度器的核心契约在 runtime/engine/include/dart_engine.h 中定义:
typedef void (*DartEngine_ScheduleMessageCallback)(Dart_Isolate isolate, void* context);
typedef struct DartEngine_MessageScheduler {
DartEngine_ScheduleMessageCallback schedule_callback;
void* context;
} DartEngine_MessageScheduler;
只要通过 DartEngine_SetDefaultMessageScheduler(对所有 isolate 生效)或 DartEngine_SetMessageScheduler(对单个 isolate 生效)注册回调,Dart 引擎就会在 isolate 有新消息时调用 schedule_callback,调度方只需负责安排一次 DartEngine_HandleMessage(isolate) 的执行——进出 isolate 和 scope 的内部细节已被封装(dart_engine.h)。
run_timer:独立线程 + 队列式调度器
run_timer.cc 使用了一个基于 std::condition_variable 的消息队列 ThreadedMessageHandler,在独立线程上循环处理消息:
Run()在后台线程中阻塞等待条件变量,取出一条待处理 isolate 后调用DartEngine_HandleMessage(isolate);Notify(isolate)把 isolate 压入队列并唤醒工作线程;ScheduleDartMessage(isolate, context)作为静态回调,被注册进DartEngine_SetDefaultMessageScheduler,引擎每次有新消息时都会调用它。
ThreadedMessageHandler message_handler;
std::thread message_handler_thread(&ThreadedMessageHandler::Run, &message_handler);
DartEngine_SetDefaultMessageScheduler(
{ThreadedMessageHandler::ScheduleDartMessage, &message_handler});
随后主线程加载快照、创建 isolate,并通过生成的 shim 调用 Dart 侧的定时器函数:
Call_startTimer(isolate, 1); // 启动周期 1ms 的 Dart Timer
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Call_stopTimer(isolate); // 停止定时器
std::cout << "Ticks: " << Get_ticks(isolate) << std::endl; // 读取 tick 计数
对应的 Dart 程序 timer.dart 用 Timer.periodic 周期性递增 _tickCount,并通过 @pragma('vm:entry-point', 'call') 暴露 startTimer / stopTimer / resetTimer,用 @pragma('vm:entry-point', 'get') 暴露 ticks getter。由于 Timer 依赖事件循环,这段程序只有在 run_timer 这样带消息调度的嵌入方式下才能正常工作。
run_timer_async:std::async 调度器
run_timer_async.cc 的实现更为精简,注释也明确说明"Same as run_timer.cc, but uses std::async instead of a dedicated event loop thread"。它的调度回调直接为每条消息启动一个异步任务:
void ScheduleDartMessage(Dart_Isolate isolate, void* context) {
std::ignore = std::async(DartEngine_HandleMessage, isolate);
}
这种方式不需要手写队列、锁与条件变量,std::async 每次都会在线程池上调度一次 DartEngine_HandleMessage。run_timer_async 的其余主流程与 run_timer 完全一致(Call_startTimer → sleep 100ms → Call_stopTimer → Get_ticks),正好用来对比"专用线程"与"按需派发"两种调度策略的实现差异。这两个示例正是 runtime/engine/README.md 推荐的 DartEngine_MessageScheduler 参考实现。
run_futures:桥接 Dart Future/Stream 与 C++ 异步结果
作为仓库中更进阶的示例(README 未展开介绍,但完整存在于 run_futures.cc 与 BUILD.gn),run_futures 展示了如何把 Dart 的 Future<int>、Stream<int> 异步结果桥接回 C++ 的 std::future<int64_t>,其机制是"FFI 回调 + std::promise":
- Dart 侧(futures.dart)用
dart:ffi把回调指针转成NativeFunction,在异步结果就绪后调用FulfillIntPromise(context, value)完成 C++ 侧的 promise(见returnRegularFutureC与sumIntStreamC,futures.dart); - C++ 侧(run_futures.cc)把
&FulfillIntPromise与promise指针作为整数参数传给 Dart,Dart_Invoke后立即调用DartEngine_DrainMicrotasksQueue()主动排空微任务队列——这是引擎 API 特有的关键步骤:当引擎主动调用 Dart 时,微任务队列不会像处理普通消息那样自动排空,需要手动触发(dart_engine.h)。
主流程依次验证了四种异步形态:
returnRegularFutureC(useMicrotask = false) returns: 256
returnRegularFutureC(useMicrotask = true) returns: 256
sumIntStream(useAsyncStar = false) returns: 10
sumIntStream(useAsyncStar = true) returns: 10
awaitAndMultiply(5, 20) = 100
其中 awaitAndMultiply 场景最有趣:C++ 通过持久的 Dart_PersistentHandle 保存 Dart 侧 AwaitAndMultiplyCall 对象,再分两次(CompleteB(5)、CompleteA(20))从不同时刻注入两个 Completer 的完成值,最终 Dart 侧 (await a) * (await b) 计算得到 100 并通过 FFI 回调返回。这演示了嵌入方按需驱动 Dart 异步状态的"拉式"交互模式。run_futures 仅当 dart_target_arch == host_cpu 时构建,因为 FFI 无法在 VM 模拟器上执行。
从 Dart 侧生成入口点 shim
run_timer / run_timer_async 中使用的 Call_startTimer、Call_stopTimer、Get_ticks 等函数并非手写,而是由工具生成的。在 timer.h 与 timer.cc 的头部注释中可以看到生成命令:
dart pkg/vm/tool/generate_entry_point_shims.dart \
out/ReleaseX64/gen/samples/embedder/timer_aot.dart.dill \
samples/embedder/timer
该工具读取编译后的 .dill 文件,为其中被 @pragma('vm:entry-point', ...) 标注的函数生成 C/C++ 头文件与实现。生成的实现(timer.cc)内部依然走标准的"IsolateScope + DartScope + Dart_Invoke / Dart_GetField"路径,只是把函数名、参数转换与返回值提取都自动化了。BUILD.gn 中的 shims("timer_library") 模板(BUILD.gn)负责把这些生成的 shim 编译为共享库或静态库,并作为 run_timer / run_timer_async 的 configurable_deps 依赖。对于需要频繁跨边界调用的项目,这一工作流可以显著减少手写胶水代码。
版本匹配与注意事项
最后,把 README 强调的边界条件汇总如下,这些都是实际嵌入工程中容易踩坑的点:
- 快照版本必须匹配:快照文件格式不稳定,
dart二进制与嵌入方必须来自同一版本。最简单的方式是从同一 checkout 构建完整 SDK(docs/Building.md),再构建samples/embedder下的目标。 - 异步程序必须有消息调度器:
run_main因不处理 isolate 消息,无法运行含async函数的 Dart 程序;需要Timer/Future等异步能力时,必须通过DartEngine_SetDefaultMessageScheduler或DartEngine_SetMessageScheduler提供调度。 - 多线程进出 isolate 必须用引擎 API:
DartEngine_AcquireIsolate/DartEngine_ReleaseIsolate是带锁的替代方案,与Dart_EnterIsolate/Dart_ExitIsolate不能混用。 - 主动调用 Dart 后要手动排空微任务:
DartEngine_DrainMicrotasksQueue用于引擎主动调用 Dart 的场景,否则微任务(如Future.microtask)可能得不到执行。 - 入口点必须标注:AOT 模式下只有带
@pragma('vm:entry-point', ...)的函数会被保留为可调用入口点,嵌入方可调用的函数集合由这些标注决定。 - FFI 的模拟器限制:依赖 FFI 的示例(
run_futures系列)只在dart_target_arch == host_cpu时构建。
总结
samples/embedder 以五个递进示例完整覆盖了 Dart VM 嵌入的典型路径:run_main 是最小可运行的"加载快照 + 调用 main",run_two_programs 展示了多 isolate 与跨快照数据传递,run_timer 与 run_timer_async 分别示范了独立线程队列与 std::async 两种消息调度器实现,run_futures 则通过 FFI 回调把 Dart 异步模型与 C++ std::future 打通。所有这些能力都建立在 runtime/engine/include/dart_engine.h 的引擎 API 之上,配合 helpers.h 中的 RAII 封装,可以快速搭建"用 Dart 写业务逻辑、用 C++ 写宿主外壳"的混合应用骨架。进一步阅读可参考 runtime/engine/README.md 的 API 设计说明与 samples/embedder/BUILD.gn 的完整构建目标定义。