ITADN
medz/wasd
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

WASD

pub package Dart SDK License GitHub Stars

一个面向 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();

宿主函数导入

使用 ImportsImportExportKind.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 契约涵盖了六个稳定的 randomclocksfilesystemsocketsclihttp 包以及八个导入/执行 世界。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-bindsockets-tcp-listensockets-echosockets-tcp-connectsockets-tcp-receivesockets-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/commandwasi:http/proxy 组件的原生 Dart VM 执行,包含所需的 randomclocksioclifilesystemsocketshttp 宿主导入绑定
Preview 3针对稳定版 WASI 0.3.0 wasi:cli/commandwasi: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