Dart SDK 中的 C++ 嵌入示例:用 Dart 引擎 API 从 C++ 启动 Dart 快照

原创2026-09-26 12:20:25663 阅读
文章标签:编程语言编译器语言运行时标准库开发工具

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();

要点有三个:

  1. 任意函数调用,而非仅限 main:isolate1 调用的不是 main,而是 getValue;isolate2 调用的是 printValue。这印证了 README 中"calls a function from one Dart snapshot and then passes the returned string to another Dart snapshot"的描述。
  2. 字符串返回值的跨边界转换:StringFromHandle(helpers.h)先用 Dart_IsString 校验句柄类型,再用 Dart_StringToCString 取出 C 字符串并转成 std::string。
  3. 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 强调的边界条件汇总如下,这些都是实际嵌入工程中容易踩坑的点:

  1. 快照版本必须匹配:快照文件格式不稳定,dart 二进制与嵌入方必须来自同一版本。最简单的方式是从同一 checkout 构建完整 SDK(docs/Building.md),再构建 samples/embedder 下的目标。
  2. 异步程序必须有消息调度器:run_main 因不处理 isolate 消息,无法运行含 async 函数的 Dart 程序;需要 Timer/Future 等异步能力时,必须通过 DartEngine_SetDefaultMessageScheduler 或 DartEngine_SetMessageScheduler 提供调度。
  3. 多线程进出 isolate 必须用引擎 API:DartEngine_AcquireIsolate / DartEngine_ReleaseIsolate 是带锁的替代方案,与 Dart_EnterIsolate / Dart_ExitIsolate 不能混用。
  4. 主动调用 Dart 后要手动排空微任务:DartEngine_DrainMicrotasksQueue 用于引擎主动调用 Dart 的场景,否则微任务(如 Future.microtask)可能得不到执行。
  5. 入口点必须标注:AOT 模式下只有带 @pragma('vm:entry-point', ...) 的函数会被保留为可调用入口点,嵌入方可调用的函数集合由这些标注决定。
  6. 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 的完整构建目标定义。

登录后查看全文
sdk