WASD
一个面向 Dart 和 Flutter 生态系统的纯 Dart WebAssembly 运行时。
WASD 提供 Dart 原生的 WebAssembly 执行能力,具备纯 Dart 核心运行时层,因此您可以直接从 Dart 代码中嵌入并运行 Wasm 模块,而无需在核心库中依赖原生运行时。
概述
WASD 是一个 Dart 包,用于:
- 解码和验证 WebAssembly 二进制文件
- 从字节或流编译和实例化模块
- 使用宿主导入实例化模块
- 从 Dart 执行导出的函数
- 运行 WASI Preview1 命令模块
- 在 Dart VM 上运行稳定的 WASI 0.2.12 命令和 HTTP 代理组件
- 在 Dart VM 上运行稳定的 WASI 0.3.0 命令和 HTTP 服务组件
- 检查模块的导入/导出/自定义段
为什么选择 WASD
- 纯 Dart 核心运行时,与 Dart/Flutter 嵌入工作流保持一致
- 镜像 WebAssembly 风格操作的公共 API(
compile,instantiate,validate) - 通过导入映射和类型化包装器实现显式宿主集成
- 内置 WASI Preview1 宿主,以及通过
WASI提供的 Preview2 和 Preview3 运行器 - 仓库内包含面向回归的测试和一致性工具
安装
dart pub add wasd
或在 pubspec.yaml 中手动添加:
dependencies:
wasd: ^0.5.0
快速开始
运行内置示例:
dart run example/wasm_cli.dart
dart run example/wasm_cli.dart 3 9
Flutter DOOM 示例在 GitHub 仓库中有自己的指南。
最小模块调用:
import 'dart:typed_data';
import 'package:wasd/wasd.dart';
Future<void> main() async {
final Uint8List wasmBytes = loadYourModuleBytes();
final runtime = await WebAssembly.instantiate(wasmBytes.buffer);
final addExport = runtime.instance.exports['add'];
if (addExport is! FunctionImportExportValue) {
throw StateError('Expected `add` export to be a function.');
}
final result = (addExport.ref([20, 22]) as num).toInt();
print(result); // 42
}
Uint8List loadYourModuleBytes() => throw UnimplementedError();
宿主函数导入
使用 Imports 和 ImportExportKind.function 提供宿主回调:
import 'dart:typed_data';
import 'package:wasd/wasd.dart';
Future<void> main() async {
final wasmBytes = loadYourModuleBytes();
final imports = <String, ModuleImports>{
'env': {
'plus': ImportExportKind.function((args) {
final a = args[0] as int;
final b = args[1] as int;
return a + b;
}),
},
};
final runtime = await WebAssembly.instantiate(wasmBytes.buffer, imports);
final usePlus = runtime.instance.exports['use_plus'];
if (usePlus is! FunctionImportExportValue) {
throw StateError('Expected `use_plus` export to be a function.');
}
print(usePlus.ref([4, 5])); // 9
}
Uint8List loadYourModuleBytes() => throw UnimplementedError();
WASI Preview1
使用 WASI 并通过 wasi.start(instance) 调用 _start。
import 'package:wasd/wasd.dart';
Future<void> main() async {
final wasmBytes = loadWasiModuleBytes();
final wasi = WASI(
args: const ['demo'],
env: const {'FOO': 'bar'},
);
final runtime = await WebAssembly.instantiate(wasmBytes.buffer, wasi.imports);
final exitCode = wasi.start(runtime.instance);
print('exitCode=$exitCode');
}
Uint8List loadWasiModuleBytes() => throw UnimplementedError();
要捕获来宾输出而不是将其转发到宿主进程流, 请提供按实例划分的字节接收器。接收器同步接收原始字节,因此 宿主可选择缓冲、流式传输或限制输出。
import 'dart:typed_data';
import 'package:wasd/wasd.dart';
final stdout = BytesBuilder();
final stderr = BytesBuilder();
final wasi = WASI(stdoutSink: stdout.add, stderrSink: stderr.add);
WASI Preview2 (Dart VM)
解码一个稳定的 WASI 0.2.12
wasi:cli/command 组件,并使用原生 Preview2 主机运行它:
import 'dart:io';
import 'package:wasd/wasd.dart';
Future<void> main() async {
final bytes = await File('app.component.wasm').readAsBytes();
final component = WasmComponent.decode(bytes);
final host = WASI.preview2(args: const ['app.component.wasm']);
final result = await WASIPreview2CommandRunner(host).run(component);
print('exitCode=${result.exitCode}');
}
WASIPreview2ProxyRunner 针对 WASIPreview2HttpIncomingRequest 执行稳定的 wasi:http/proxy 传入处理器。
Preview2 执行目前仅支持原生 Dart VM,并针对这些稳定的 WASI 0.2.12 世界所要求的同步 Canonical ABI。
final proxyComponent = WasmComponent.decode(
await File('proxy.component.wasm').readAsBytes(),
);
final proxyHost = WASI.preview2();
final request = WASIPreview2HttpIncomingRequest(
method: const WASIPreview2HttpMethod.standard('get'),
headers: WASIPreview2HttpFields(),
pathWithQuery: '/',
scheme: const WASIPreview2HttpScheme.standard('HTTP'),
authority: 'example.test',
);
final response = await WASIPreview2ProxyRunner(proxyHost).handle(
proxyComponent,
request,
);
WASI Preview3 (Dart VM)
解码一个稳定的 WASI 0.3.0
wasi:cli/command 组件,并使用原生
Preview3 主机运行其异步入口点:
import 'dart:io';
import 'package:wasd/wasd.dart';
Future<void> main() async {
final bytes = await File('app.component.wasm').readAsBytes();
final component = WasmComponent.decode(bytes);
final host = WASI.preview3(args: const ['app.component.wasm']);
try {
final result = await WASIPreview3CommandRunner(host).run(component);
print('exitCode=${result.exitCode}');
} finally {
host.close(force: true);
}
}
WASIPreview3ServiceRunner 执行稳定的 wasi:http/service 组件。
每个请求直接传递给组件处理器;将其与
HTTP 服务器集成仍属于应用层面的关注点。
final serviceComponent = WasmComponent.decode(
await File('service.component.wasm').readAsBytes(),
);
final serviceHost = WASI.preview3();
try {
final request = WASIPreview3HttpRequest.noTrailers(
headers: WASIPreview3HttpFields(),
)
..method = const WASIPreview3HttpMethod.standard('get')
..pathWithQuery = '/';
final result = await WASIPreview3ServiceRunner(serviceHost).handle(
serviceComponent,
request,
);
if (!result.isOk) {
throw StateError('service failed: ${result.errorCode}');
}
final response = result.value!;
try {
print('status=${response.statusCode}');
// Forward response.contents and response trailers to the client here.
} catch (_) {
await response.cancel();
rethrow;
}
await response.completeTransmission(
const WASIPreview3HttpResult<void>.ok(null),
);
} finally {
serviceHost.close(force: true);
}
每个成功的服务响应在传输其主体和尾随数据期间,都会保留其组件资源作用域。在客户端观察到响应后调用 completeTransmission,或在放弃响应时调用 cancel,以便确定性地释放该作用域。
冻结的 Preview3 契约涵盖了六个稳定的 random、clocks、
filesystem、sockets、cli 和 http 包以及八个导入/执行
世界。wasi:clocks/timezone 不属于该契约的一部分。
模块元数据
import 'dart:typed_data';
import 'package:wasd/wasd.dart';
Future<void> main() async {
final wasmBytes = loadYourModuleBytes();
final module = await WebAssembly.compile(wasmBytes.buffer);
final imports = Module.imports(module);
final exports = Module.exports(module);
print('imports=${imports.length} exports=${exports.length}');
}
Uint8List loadYourModuleBytes() => throw UnimplementedError();
验证
dart analyze
dart test test/wasi_test.dart test/wasm_test.dart
dart test test/wasi_preview2_conformance_test.dart test/wasi_preview2_http_proxy_toolchain_test.dart
dart test test/wasi_preview3_async_runtime_test.dart test/wasi_preview3_service_runner_test.dart test/wasi_preview3_standard_wit_test.dart
dart run tool/wasi_testsuite_preview3_runner.dart \
--testsuite-dir=/path/to/wasi-testsuite \
--runner-dir=/path/to/wasi-testsuite/test-runner \
--python=/path/to/venv/bin/python
冻结的官方 wasm32-wasip3 门控通过了 39/45 个测试用例。剩余的
六个(sockets-tcp-bind、sockets-tcp-listen、sockets-echo、
sockets-tcp-connect、sockets-tcp-receive 和 sockets-tcp-send)需要
dart:io 无法表示的显式原生 TCP bind/listen 拆分;它们
以 not-supported 失败。没有跳过、预期失败或意外
通过。
冻结的 Component Model 异步门控目前记录三种不同种类 的证据:
- WASD 严格解码:解码了
37/37个组件文件。 wasm-tools验证:验证了31/31个异步 WAST 文件。- Wasmtime
48.0.0 (e8ac8c27f)参考执行:31/31个异步 WAST 文件通过。
wasm-tools 和 Wasmtime 结果验证了冻结的上游输入和
参考行为。它们不通过 WASD 执行这些 WAST 断言;
WASD 在此门控中的结果是上述严格解码结果。
兼容性快照
WebAssembly 实现版本
| 项目 | 版本 | 状态 |
|---|---|---|
| 核心 Wasm 模块二进制 | 0x01 0x00 0x00 0x00 | 受支持 |
WASI 版本
| WASI 版本 | 状态 |
|---|---|
| Preview 1 | 支持 wasi_snapshot_preview1 命令模块 |
| Preview 2 | 针对稳定版 WASI 0.2.12 wasi:cli/command 和 wasi:http/proxy 组件的原生 Dart VM 执行,包含所需的 random、clocks、io、cli、filesystem、sockets 和 http 宿主导入绑定 |
| Preview 3 | 针对稳定版 WASI 0.3.0 wasi:cli/command 和 wasi:http/service 组件的原生 Dart VM 执行,覆盖冻结的六包、八世界契约 |
运行时支持
| 运行时 | Preview1 宿主 | Preview2 运行器 | Preview3 运行器 | 文件系统模型 |
|---|---|---|---|---|
| Dart VM | 仓库内 wasi_snapshot_preview1;通过官方 wasm32-wasip1 wasi-testsuite 命令模块 | 稳定的 WASI 0.2.12 命令和 HTTP 代理组件 | 稳定的 WASI 0.3.0 命令和 HTTP 服务组件 | 真实宿主预打开加上可移植的内存 VFS |
| Node.js | 仓库内 wasi_snapshot_preview1,非 node:wasi | 不支持 | 不支持 | 真实宿主预打开加上可移植的内存 VFS |
| 浏览器 JS | 仓库内 wasi_snapshot_preview1 | 不支持 | 不支持 | 可移植的内存 VFS |
Preview2 和 Preview3 运行器刻意不声称支持通用的 Component Model 执行。Preview3 支持仅限于冻结的稳定 WASI 0.3.0 契约;该契约之外的实验性提案包和功能 不在范围内。
原生 Preview3 文件系统预打开在解析时拒绝来宾绝对路径、..
遍历以及符号链接逃逸。Dart 暴露基于路径的
文件系统 API 而非描述符相对遍历,因此如果另一个进程可以并发
替换预打开路径组件,WASD 无法
关闭检查时/使用时(time-of-check/time-of-use)竞态。当需要文件系统隔离时,请使用不受信任
参与者无法修改的预打开目录。
当 dart:io 无法忠实地实现 Preview2 套接字操作时,
原生适配器会返回 not-supported,而不是报告模拟成功。
Preview2 原生 TCP bind/listen 目前不受支持;Preview2 原生 TCP
connect 和 UDP bind/connect 仍然可用。
Preview3 同步套接字导入可能会返回待处理的 Dart 回调;
组件运行器在返回给来宾之前会等待它们。显式原生
TCP bind 返回 not-supported,因为 dart:io 仅暴露
ServerSocket.bind,它在单独的 WASI listen
转换之前就开始监听。未绑定的 TCP listen 和 connect 仍然可用,并在报告成功之前等待真实的
OS 端点。原生 UDP bind 和隐式 UDP
connect 同样等待真实的 RawDatagramSocket。TCP 和 UDP 选项值
在存在活动端点时通过原生原始套接字选项应用。
RawDatagramSocket 没有仅 IPv6 的 bind 选项,因此 IPv6 通配符 UDP 套接字
也可能保留匹配的 IPv4 端口;IPv4 和 IPv4 映射的数据报在到达该 IPv6 来宾套接字之前被
丢弃。具有非零 IPv6 流信息或数字范围 ID 的原生地址返回 not-supported,因为
dart:io 无法保留这些字段。
Dart HttpClient 不暴露 HTTP 尾标。带有尾标的原生出站处理程序
请求以及声明了尾标的入站响应因此
报告 HTTP-protocol-error;代理响应尾标对
主机调用者仍然可用。Preview3 原生客户端同样报告
HTTP-protocol-error 用于出站请求尾标和声明的入站
响应尾标。
原生出站 HTTP 保留编码后的响应体和重定向响应; 它不会透明地解压内容或跟随重定向。
套接字解析器执行无依赖的 IDNA ToASCII 转换,针对保守的规范化 Unicode 子集,并验证现有的 A-label。
不允许的符号、格式错误的 A-label,以及需要 Unicode 规范化表或 ContextJ/ContextO 处理的标签,将被拒绝并返回
invalid-argument,而不是生成非规范化的 DNS 名称。
贡献
欢迎通过拉取请求和问题提交贡献。
- 遵循现有的 lint/style 规则(
dart format .、dart analyze) - 为行为变更添加聚焦的回归测试
- 保持变更范围明确,并可通过命令输出复现
许可证
WASD 采用 MIT 许可证。参见 LICENSE。