dart2wasm: Prepare standalone target

When compiled to WebAssembly, the Dart SDK needs access to external
host functions to implement regular expressions, stack traces, timers
and more. Currently, `dart2wasm` relies on `js_interop` definition to
implement these functions in JavaScript.

As discussed in https://github.com/dart-lang/sdk/issues/53884, an
alternative is to use `wasm:import` annotations to let an arbitrary
embedder that doesn't necessarily run in a JavaScript context inject
implementations for these host functions.

This would allow running `dart2wasm` apps by e.g.

  - using a runtime like wasmtime and defining host functions in Rust.
  - defining a wrapper module implementing required functions by
    delegating to WASI definitions, and then using say `wasm-merge` to
    run the app in any WASI-compatible runtime.

This prepares the `--standalone` flag on `dart2wasm` to do just that.
When enabled, the compiler uses a different SDK platform to use imports
instead of JS interop. For now, these platforms are almost identical:
I've ported the timer logic to use wasm imports as a demo, but the rest
is still based on existing patch files. We can revisit in subsequent
CLs to incrementally reduce `js_interop` dependencies before removing
that library from the `dart2wasm_standalone` target entirely.

Change-Id: I3f406afbf2dab65506094de5c3f4067f4db66f3e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/486380
Reviewed-by: Slava Egorov <vegorov@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Simon Binder
2026-03-19 01:25:57 -07:00
committed by Commit Queue
parent bf200e436e
commit b4cfeb1093
21 changed files with 454 additions and 29 deletions
+1
View File
@@ -162,6 +162,7 @@ group("dart2wasm_platform") {
":runtime_precompiled",
"utils/dart2wasm:compile_dart2wasm_js_compatibility_platform",
"utils/dart2wasm:compile_dart2wasm_platform",
"utils/dart2wasm:compile_dart2wasm_standalone_platform",
"utils/dart2wasm:dart2wasm_snapshot",
]
if (defined(is_product)) {
+2
View File
@@ -215,6 +215,8 @@ Future<CompilationResult> compile(
final wasm.Mode mode;
if (options.translatorOptions.jsCompatibility) {
mode = wasm.Mode.jsCompatibility;
} else if (options.translatorOptions.standalone) {
mode = wasm.Mode.standalone;
} else {
mode = wasm.Mode.regular;
}
+4
View File
@@ -43,6 +43,10 @@ final List<Option> options = [
o.translatorOptions.jsCompatibility = value;
o.environment['dart.wasm.js_compatibility'] = 'true';
}, defaultsTo: _d.translatorOptions.jsCompatibility),
Flag("standalone", (o, value) {
o.translatorOptions.standalone = value;
o.environment['dart.wasm.standalone'] = 'true';
}, defaultsTo: _d.translatorOptions.standalone),
Flag(
"enable-asserts", (o, value) => o.translatorOptions.enableAsserts = value,
defaultsTo: _d.translatorOptions.enableAsserts),
+5 -2
View File
@@ -42,6 +42,7 @@ import 'wasm_library_checks.dart' as wasmChecks;
enum Mode {
regular,
jsCompatibility,
standalone,
}
class Dart2WasmConstantsBackend extends ConstantsBackend {
@@ -129,14 +130,16 @@ class WasmTarget extends Target {
String get name {
return switch (mode) {
Mode.regular => 'wasm',
Mode.jsCompatibility => 'wasm_js_compatibility'
Mode.jsCompatibility => 'wasm_js_compatibility',
Mode.standalone => 'wasm_standalone',
};
}
String get platformFile {
return switch (mode) {
Mode.regular => 'dart2wasm_platform.dill',
Mode.jsCompatibility => 'dart2wasm_js_compatibility_platform.dill'
Mode.jsCompatibility => 'dart2wasm_js_compatibility_platform.dill',
Mode.standalone => 'dart2wasm_standalone_platform.dill',
};
}
+3
View File
@@ -50,6 +50,7 @@ class TranslatorOptions {
int optimizationLevel = 1;
bool? inliningOverride;
bool jsCompatibility = false;
bool standalone = false;
bool? omitImplicitTypeChecksOverride;
bool omitExplicitTypeChecks = false;
bool? omitBoundsChecksOverride;
@@ -84,6 +85,7 @@ class TranslatorOptions {
sink.writeInt(optimizationLevel);
sink.writeNullable(inliningOverride, sink.writeBool);
sink.writeBool(jsCompatibility);
sink.writeBool(standalone);
sink.writeNullable(omitImplicitTypeChecksOverride, sink.writeBool);
sink.writeBool(omitExplicitTypeChecks);
sink.writeNullable(omitBoundsChecksOverride, sink.writeBool);
@@ -112,6 +114,7 @@ class TranslatorOptions {
options.optimizationLevel = source.readInt();
options.inliningOverride = source.readNullable(source.readBool);
options.jsCompatibility = source.readBool();
options.standalone = source.readBool();
options.omitImplicitTypeChecksOverride =
source.readNullable(source.readBool);
options.omitExplicitTypeChecks = source.readBool();
+6
View File
@@ -90,6 +90,12 @@ while [ $# -gt 0 ]; do
shift
;;
--standalone)
PLATFORM_FILENAME="$BIN_DIR/dart2wasm_standalone_platform.dill"
DART2WASM_ARGS+=("--standalone")
shift
;;
--compiler-asserts)
SNAPSHOT_NAME="dart2wasm_asserts"
VM_ARGS+=("--enable-asserts")
@@ -3467,7 +3467,7 @@ const MessageCode fastaUsageLong = const MessageCode(
Read the SDK platform from <file>, which should be in Dill/Kernel IR format
and contain the Dart SDK.
--target=dart2js|dart2js_server|dart2wasm|dart2wasm_js_compatibility|dart_runner|dartdevc|flutter|flutter_runner|none|vm
--target=dart2js|dart2js_server|dart2wasm|dart2wasm_js_compatibility|dart2wasm_standalone|dart_runner|dartdevc|flutter|flutter_runner|none|vm
Specify the target configuration.
--enable-asserts
+1 -1
View File
@@ -2013,7 +2013,7 @@ fastaUsageLong:
Read the SDK platform from <file>, which should be in Dill/Kernel IR format
and contain the Dart SDK.
--target=dart2js|dart2js_server|dart2wasm|dart2wasm_js_compatibility|dart_runner|dartdevc|flutter|flutter_runner|none|vm
--target=dart2js|dart2js_server|dart2wasm|dart2wasm_js_compatibility|dart2wasm_standalone|dart_runner|dartdevc|flutter|flutter_runner|none|vm
Specify the target configuration.
--enable-asserts
@@ -762,6 +762,7 @@ dart
dart2js
dart2wasm
dart2wasm_js_compatibility
dart2wasm_standalone
dartdevc
data
date
@@ -22,5 +22,7 @@ void installAdditionalTargets() {
targets["dart2wasm"] = (TargetFlags flags) => new WasmTarget();
targets["dart2wasm_js_compatibility"] = (TargetFlags flags) =>
new WasmTarget(mode: wasm.Mode.jsCompatibility);
targets["dart2wasm_standalone"] = (TargetFlags flags) =>
new WasmTarget(mode: wasm.Mode.standalone);
vm_target_install.installAdditionalTargets();
}
+26 -3
View File
@@ -321,8 +321,30 @@ $script
""";
}
String dart2wasmHtml(
String title, String wasmPath, String mjsPath, String supportJsPath) {
String dart2wasmHtml(String title, String wasmPath, String mjsPath,
String supportJsPath, bool standalone) {
const standaloneEmbedder = """
const dartEmbedder = {
// See sdk/lib/_internal/wasm_standalone/lib/embedder.dart for required definitions.
scheduleOnce: (delayInMicros, callback, arg) => {
const timeout = setTimeout(() => callback(arg), Number(delayInMicros / 1000n));
return {timeout};
},
scheduleRepeated: (intervalMicros, callback, arg) => {
const timeout = setInterval(() => callback(arg), Number(intervalMicros / 1000));
return {timeout};
},
queueMicrotask: (callback, arg) => {
queueMicrotask(() => callback(arg));
},
clearSchedule: (schedule) => {
clearTimeout(schedule.timeout);
},
currentTime: () => BigInt(Date.now()) * 1000n,
};
""";
final additionalImports = standalone ? '{ dart: dartEmbedder }' : '{}';
return """
<!DOCTYPE html>
<html>
@@ -365,7 +387,8 @@ String dart2wasmHtml(
const response = await fetch(`\${path}/\${relativeToWasmFileUri}`);
return response.arrayBuffer();
};
const appInstance = await compiledApp.instantiate({}, {
${standalone ? standaloneEmbedder : ''}
const appInstance = await compiledApp.instantiate($additionalImports, {
loadDeferredModules: (modules, handleWasmBytes) =>
Promise.all(modules.map((m) => fetch(m).then((b) => handleWasmBytes(m, b)))),
});
+5 -2
View File
@@ -947,8 +947,11 @@ class StandardTestSuite extends TestSuite {
_createUrlPathFromFile(Path('$outputDir/$nameNoExt.mjs'));
final supportJsPath =
_createUrlPathFromFile(Path('$outputDir/$nameNoExt.support.js'));
content = dart2wasmHtml(
testFile.path.toNativePath(), wasmPath, mjsPath, supportJsPath);
final isStandalone = configuration.dart2wasmOptions
.any((e) => e.contains('--standalone'));
content = dart2wasmHtml(testFile.path.toNativePath(), wasmPath, mjsPath,
supportJsPath, isStandalone);
} else if (configuration.compiler == Compiler.ddc) {
var ddcConfig =
configuration.compilerConfiguration as DevCompilerConfiguration;
+1 -5
View File
@@ -1,11 +1,7 @@
import 'dart:_internal' show _AsyncCompleter, patch, exportWasmFunction;
import 'dart:_js_helper' show JS;
import 'dart:_internal' show _AsyncCompleter, patch;
import 'dart:_wasm';
part 'timer_patch.dart';
// Modular kernel transformer will make calls to this method be re-directed to
// call dart:core:Error._trySetStackTrace instead.
@patch
+3 -1
View File
@@ -2,7 +2,9 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of "async_patch.dart";
import 'dart:_internal' show patch, exportWasmFunction;
import 'dart:_js_helper' show JS;
// Implementation of `Timer` and `scheduleMicrotask` via the JS event loop.
+1
View File
@@ -0,0 +1 @@
file:/tools/OWNERS_WASM
@@ -0,0 +1,57 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Imported definitions that need to be provided to run `dart2wasm` apps
/// without JavaScript.
///
/// These definitions are currently incomplete. Once all externs in the Dart SDK
/// are implemented with these imports, the `dart2wasm_standalone` target can
/// run without `js_interop`. This will enable embedders without JavaScript
/// support to run `dart2wasm` apps by either:
///
/// - providing `dartrt` imports when instantiating the module.
/// - using an external tool like `wasm-merge` to link another module that
/// could provide implementations by e.g. delegating to WASI definitions.
library;
import 'dart:core';
import 'dart:core' as core;
import 'dart:_wasm';
/// Instructs the runtime to invoke `callback(arg)` after the delay in
/// microseconds.
///
/// Returns a handle that can be used with [clearSchedule] to abort the timer.
@pragma("wasm:import", "dart.scheduleOnce")
external WasmExternRef scheduleOnce(
WasmI64 delay,
WasmFunction<void Function(WasmAnyRef)> callback,
WasmAnyRef arg,
);
/// Instructs the runtime to invoke `callback(arg)` every `interval`
/// microseconds.
///
/// Returns a handle that can be used with [clearSchedule] to abort the timer.
@pragma("wasm:import", "dart.scheduleRepeated")
external WasmExternRef scheduleRepeated(
WasmI64 interval,
WasmFunction<void Function(WasmAnyRef)> callback,
WasmAnyRef arg,
);
/// Instructs the runtime to invoke `callback(arg)` before returning to the
/// event loop.
@pragma("wasm:import", "dart.queueMicrotask")
external void queueMicrotask(
WasmFunction<void Function(WasmAnyRef)> callback,
WasmAnyRef arg,
);
/// Cancels a schedule created through [scheduleOnce] or [scheduleRepeated].
@pragma("wasm:import", "dart.clearSchedule")
external void clearSchedule(WasmExternRef? schedule);
@pragma("wasm:import", "dart.currentTime")
external WasmI64 currentTimeMicros();
@@ -0,0 +1,124 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:_internal' show patch;
import 'dart:_embedder' as embedder;
import 'dart:_wasm';
@patch
class Timer {
@patch
static Timer _createTimer(Duration duration, void callback()) {
return _OneShotTimer(duration, callback);
}
@patch
static Timer _createPeriodicTimer(
Duration duration,
void callback(Timer timer),
) {
return _PeriodicTimer(duration, callback);
}
}
abstract class _Timer implements Timer {
final int _microseconds;
int _tick;
WasmExternRef? _handle;
@override
int get tick => _tick;
@override
bool get isActive => _handle != null;
_Timer(Duration duration)
: _microseconds = duration.inMicroseconds,
_tick = 0,
_handle = null {
_schedule();
}
void _schedule();
void _processTick();
@override
void cancel() {
if (!_handle.isNull) {
embedder.clearSchedule(_handle);
_handle = WasmExternRef.nullRef;
}
}
static void runtimeCallback(WasmAnyRef timer) {
final dartTimer = timer.toObject() as _Timer;
dartTimer._processTick();
}
}
class _OneShotTimer extends _Timer {
final void Function() _callback;
_OneShotTimer(Duration duration, this._callback) : super(duration);
@override
void _schedule() {
_handle = embedder.scheduleOnce(
_microseconds.toWasmI64(),
WasmFunction.fromFunction(_Timer.runtimeCallback),
WasmAnyRef.fromObject(this),
);
}
_processTick() {
_tick++;
_handle = null;
_callback();
}
}
class _PeriodicTimer extends _Timer {
final void Function(Timer) _callback;
int _start = 0;
_PeriodicTimer(Duration duration, this._callback) : super(duration);
@override
void _schedule() {
_start = embedder.currentTimeMicros().toInt();
_handle = embedder.scheduleRepeated(
_microseconds.toWasmI64(),
WasmFunction.fromFunction(_Timer.runtimeCallback),
WasmAnyRef.fromObject(this),
);
}
@override
void _processTick() {
_tick++;
if (_microseconds > 0) {
final int duration = embedder.currentTimeMicros().toInt() - _start;
if (duration > _tick * _microseconds) {
_tick = duration ~/ _microseconds;
}
}
_callback(this);
}
}
@patch
class _AsyncRun {
@patch
static void _scheduleImmediate(void callback()) {
embedder.queueMicrotask(
WasmFunction.fromFunction(_runtimeCallback),
WasmAnyRef.fromObject(callback),
);
}
static void _runtimeCallback(WasmAnyRef callbackFunction) {
final function = callbackFunction.toObject() as void Function();
function();
}
}
+86 -8
View File
@@ -134,7 +134,7 @@
"wasm": {
"include": [
{
"target": "wasm_common"
"target": "wasm_js_common"
}
],
"libraries": {
@@ -192,7 +192,7 @@
"wasm_js_compatibility": {
"include": [
{
"target": "wasm_common"
"target": "wasm_js_common"
}
],
"libraries": {
@@ -244,6 +244,90 @@
}
}
},
"wasm_standalone": {
"include": [
{
"target": "wasm_common"
}
],
"libraries": {
"_embedder": {
"uri": "_internal/wasm_standalone/lib/embedder.dart"
},
"async": {
"uri": "async/async.dart",
"patches": [
"_internal/wasm/lib/async_patch.dart",
"_internal/wasm_standalone/lib/timer_patch.dart"
]
},
"core": {
"uri": "core/core.dart",
"patches": [
"_internal/vm_shared/lib/bigint_patch.dart",
"_internal/vm_shared/lib/bool_patch.dart",
"_internal/vm_shared/lib/date_patch.dart",
"_internal/vm_shared/lib/map_patch.dart",
"_internal/vm_shared/lib/null_patch.dart",
"_internal/wasm/lib/array_patch.dart",
"_internal/wasm/lib/bigint_patch_patch.dart",
"_internal/wasm/lib/core_patch.dart",
"_internal/wasm/lib/date_patch_patch.dart",
"_internal/wasm/lib/int_common_patch.dart",
"_internal/wasm/lib/int_patch.dart",
"_internal/wasm/lib/string_buffer_patch.dart",
"_internal/wasm/lib/string_patch.dart",
"_internal/wasm/lib/sync_star_patch.dart",
"_internal/wasm/lib/weak_patch.dart"
]
},
"convert": {
"uri": "convert/convert.dart",
"patches": [
"_internal/wasm/lib/convert_patch.dart"
]
},
"typed_data": {
"uri": "typed_data/typed_data.dart",
"patches": [
"_internal/wasm/lib/simd_patch.dart",
"_internal/wasm/lib/typed_data_patch.dart"
]
},
"_boxed_int": {
"uri": "_internal/wasm/lib/boxed_int.dart",
"patches": "_internal/wasm/lib/boxed_int_to_string.dart"
},
"_string": {
"uri": "_internal/wasm/lib/js_string.dart"
},
"_typed_data": {
"uri": "_internal/wasm/lib/typed_data.dart"
},
"_js_helper": {
"uri": "_internal/wasm/lib/js_helper.dart",
"patches": [
"_internal/wasm/lib/js_helper_patch.dart"
]
}
}
},
"wasm_js_common": {
"include": [
{
"target": "wasm_common"
}
],
"libraries": {
"async": {
"uri": "async/async.dart",
"patches": [
"_internal/wasm/lib/async_patch.dart",
"_internal/wasm/lib/timer_patch.dart"
]
}
}
},
"wasm_common": {
"libraries": {
"core": {
@@ -301,12 +385,6 @@
"uri": "_wasm/wasm_types.dart",
"patches": "_internal/wasm/lib/wasm_types_patch.dart"
},
"async": {
"uri": "async/async.dart",
"patches": [
"_internal/wasm/lib/async_patch.dart"
]
},
"collection": {
"uri": "collection/collection.dart",
"patches": [
+65 -6
View File
@@ -125,7 +125,7 @@ vm:
wasm:
include:
- target: "wasm_common"
- target: "wasm_js_common"
libraries:
core:
uri: core/core.dart
@@ -169,7 +169,7 @@ wasm:
wasm_js_compatibility:
include:
- target: "wasm_common"
- target: "wasm_js_common"
libraries:
convert:
uri: convert/convert.dart
@@ -209,6 +209,69 @@ wasm_js_compatibility:
patches:
- _internal/wasm_js_compatibility/lib/js_helper_patch.dart
wasm_standalone:
include:
- target: "wasm_common"
libraries:
_embedder:
uri: _internal/wasm_standalone/lib/embedder.dart
async:
uri: async/async.dart
patches:
- _internal/wasm/lib/async_patch.dart
- _internal/wasm_standalone/lib/timer_patch.dart
# Not yet migrated
core:
uri: core/core.dart
patches:
- _internal/vm_shared/lib/bigint_patch.dart
- _internal/vm_shared/lib/bool_patch.dart
- _internal/vm_shared/lib/date_patch.dart
- _internal/vm_shared/lib/map_patch.dart
- _internal/vm_shared/lib/null_patch.dart
- _internal/wasm/lib/array_patch.dart
- _internal/wasm/lib/bigint_patch_patch.dart
- _internal/wasm/lib/core_patch.dart
- _internal/wasm/lib/date_patch_patch.dart
- _internal/wasm/lib/int_common_patch.dart
- _internal/wasm/lib/int_patch.dart
- _internal/wasm/lib/string_buffer_patch.dart
- _internal/wasm/lib/string_patch.dart
- _internal/wasm/lib/sync_star_patch.dart
- _internal/wasm/lib/weak_patch.dart
convert:
uri: convert/convert.dart
patches:
- _internal/wasm/lib/convert_patch.dart
typed_data:
uri: typed_data/typed_data.dart
patches:
- _internal/wasm/lib/simd_patch.dart
- _internal/wasm/lib/typed_data_patch.dart
_boxed_int:
uri: _internal/wasm/lib/boxed_int.dart
patches:
_internal/wasm/lib/boxed_int_to_string.dart
_string:
uri: _internal/wasm/lib/js_string.dart
_typed_data:
uri: _internal/wasm/lib/typed_data.dart
_js_helper:
uri: _internal/wasm/lib/js_helper.dart
patches:
- _internal/wasm/lib/js_helper_patch.dart
wasm_js_common:
include:
- target: "wasm_common"
libraries:
async:
uri: async/async.dart
patches:
- _internal/wasm/lib/async_patch.dart
- _internal/wasm/lib/timer_patch.dart
wasm_common:
libraries:
core:
@@ -248,10 +311,6 @@ wasm_common:
_wasm:
uri: _wasm/wasm_types.dart
patches: _internal/wasm/lib/wasm_types_patch.dart
async:
uri: async/async.dart
patches:
- _internal/wasm/lib/async_patch.dart
collection:
uri: collection/collection.dart
patches:
+44
View File
@@ -54,6 +54,8 @@
"out/ReleaseX64/dart2wasm_platform.dill",
"out/ReleaseX64/dart2wasm_js_compatibility_outline.dill",
"out/ReleaseX64/dart2wasm_js_compatibility_platform.dill",
"out/ReleaseX64/dart2wasm_standalone_outline.dill",
"out/ReleaseX64/dart2wasm_standalone_platform.dill",
"out/ReleaseX64/dart-sdk/",
"out/ReleaseX64/wasm/",
"out/ReleaseX64/wasm-opt",
@@ -527,6 +529,17 @@
"timeout": 60
}
},
"dart2wasm-(linux|mac|win)-standalone-(chrome|firefox|safari)": {
"options": {
"dart2wasm-options": [
"-O0",
"--standalone"
],
"host-asserts": true,
"use-sdk": false,
"timeout": 60
}
},
"vm-aot-android-(debug|product|release)-arm_x64": {
"options": {
"builder-tag": "crossword",
@@ -2889,6 +2902,37 @@
}
]
},
{
"builders": [
"dart2wasm-linux-standalone-chrome"
],
"meta": {
"description": "dart2wasm standalone tests"
},
"steps": [
{
"name": "build dart",
"script": "tools/build.py",
"arguments": [
"runtime",
"dart2wasm",
"dartaotruntime"
]
},
{
"name": "dart2wasm tests",
"arguments": [
"-ndart2wasm-${system}-standalone-${runtime}",
"co19",
"corelib",
"language",
"lib"
],
"shards": 8,
"fileset": "dart2wasm_hostasserts"
}
]
},
{
"builders": [
"analyzer-linux-release",
+16
View File
@@ -75,6 +75,22 @@ compile_platform("compile_dart2wasm_js_compatibility_platform") {
]
}
compile_platform("compile_dart2wasm_standalone_platform") {
single_root_scheme = "org-dartlang-sdk"
single_root_base = rebase_path("$sdk_root/")
libraries_specification_uri = "org-dartlang-sdk:///lib/libraries.json"
outputs = [
"$root_out_dir/dart2wasm_standalone_platform.dill",
"$root_out_dir/dart2wasm_standalone_outline.dill",
]
args = [
"--target=dart2wasm_standalone",
"dart:core",
]
}
wasm_module("ffi_native_test_wasm_module") {
module_name = "ffi_native_test_module"
}