From 567bf3337be9fcbe08282e442df47a60aaa2d003 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 2 Jun 2026 00:56:35 -0700 Subject: [PATCH] [dart2wasm] Document standalone platform and include it in SDK Given that the standalone target for dart2wasm is feature-complete now, it makes sense to include it in released SDKs. This adds the platform and outline files to built SDKs and exposes the `--standalone` flag in `dart compile wasm`. It also documents the standalone target in `pkg/dart2wasm/doc`, which should be helpful as a starting point to use these compiled modules. Change-Id: I5bd86e9670f03f2955e31789095dd5c462bf149e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/506920 Reviewed-by: Slava Egorov Reviewed-by: Martin Kustermann Commit-Queue: Martin Kustermann --- pkg/dart2native/lib/sdk.dart | 4 +++ pkg/dart2wasm/docs/standalone.md | 34 +++++++++++++++++++ pkg/dartdev/lib/src/commands/compile.dart | 36 ++++++++++++++++++--- pkg/dartdev/test/commands/compile_test.dart | 29 +++++++++++++++++ sdk/BUILD.gn | 3 ++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 pkg/dart2wasm/docs/standalone.md diff --git a/pkg/dart2native/lib/sdk.dart b/pkg/dart2native/lib/sdk.dart index d0972760f12..44a47196777 100644 --- a/pkg/dart2native/lib/sdk.dart +++ b/pkg/dart2native/lib/sdk.dart @@ -144,6 +144,10 @@ class Sdk { 'dart2wasm_platform.dill', ); + String get wasmStandalonePlatformDill => _dillPathFor( + 'dart2wasm_standalone_platform.dill', + ); + String _dillPathFor(String dillName) => path.absolute( runFromBuildRoot ? sdkPath diff --git a/pkg/dart2wasm/docs/standalone.md b/pkg/dart2wasm/docs/standalone.md new file mode 100644 index 00000000000..c81268b44a7 --- /dev/null +++ b/pkg/dart2wasm/docs/standalone.md @@ -0,0 +1,34 @@ +# Standalone mode + +By default, dart2wasm generates WebAssembly modules meant to run on platforms +with JavaScript support (mainly, browsers). +Both the SDK and application code can rely on `dart:js_interop` libraries to +access external JavaScript APIs. + +For WebAssembly targets without a JavaScript engine (like wasmtime or similar +runtimes), dart2wasm supports a standalone target too. This target is enabled +with the `--standalone` compiler flag, and makes `dart:js_interop` unavailable. + +Even in standalone mode, the SDK needs host imports for functionality like +timers, stack traces, regular expressions, `dart:math` or number formatting. +These imports are referenced in [this file](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/wasm/standalone/embedder.dart). +Note that these imports are not stable, and might change in future Dart +versions. + +Additional definitions can be imported via [imports and exports](./imports_and_exports.md). + +## Running standalone modules + +There are two plausible ways to run standalone modules: + +1. By using APIs from a WebAssembly engine to define definitions for imports in + native code. There is no example for this in the SDK, but test definitions + for browser tests (`dart2WasmStandaloneHtml` in `pkg/test_runner/lib/src/browser.dart`) + might help as a starting point. +2. By re-implementing a subset of imports in WebAssembly, and then using tools + like `wasm-merge` from Binaryen to link two modules into a module with fewer + or no dependencies. Implementing Dart imports by delegating to WASI + definitions would allow running these modules without custom native imports, + for example. + +Examples for both options are discussed in the [tracking issue](https://github.com/dart-lang/sdk/issues/53884). diff --git a/pkg/dartdev/lib/src/commands/compile.dart b/pkg/dartdev/lib/src/commands/compile.dart index 0414b08e854..cdd04e89614 100644 --- a/pkg/dartdev/lib/src/commands/compile.dart +++ b/pkg/dartdev/lib/src/commands/compile.dart @@ -894,6 +894,14 @@ class CompileWasmCommand extends CompileSubcommandCommand { negatable: false, help: enableAssertsOption.help, ) + ..addFlag( + 'standalone', + help: + 'Compile to a WebAssembly module without JavaScript interop. ' + 'Dart-specific host imports are necessary to load these modules.', + negatable: false, + hide: !verbose, + ) ..addOption( 'shared-memory', help: @@ -1046,10 +1054,14 @@ class CompileWasmCommand extends CompileSubcommandCommand { final generateSourceMap = args.flag('source-maps'); final enabledExperiments = args.enabledExperiments; + final standalone = args.flag('standalone'); + final platform = standalone + ? sdk.wasmStandalonePlatformDill + : sdk.wasmPlatformDill; final dart2wasmCommand = [ sdk.dartAotRuntime, sdk.dart2wasmSnapshot, - '--platform=${sdk.wasmPlatformDill}', + '--platform=$platform', if (verbose) '--verbose', if (packages != null) '--packages=$packages', if (args.flag('print-wasm')) '--print-wasm', @@ -1060,6 +1072,7 @@ class CompileWasmCommand extends CompileSubcommandCommand { if (args.flag('minify')) '--minify', if (!args.flag('strip-wasm')) '--no-strip-wasm', if (args.flag('enable-deferred-loading')) '--enable-deferred-loading', + if (standalone) '--standalone', if (args.option(recordedUsesOption.flag) != null) '--recorded-uses=${args.option(recordedUsesOption.flag)}', for (final define in defines) '-D$define', @@ -1091,10 +1104,23 @@ class CompileWasmCommand extends CompileSubcommandCommand { if (isDryRun) return 0; - final mjsFile = '$outputFileBasename.mjs'; - log.stdout( - "Generated wasm module '$outputFile', and JS init file '$mjsFile'.", - ); + if (standalone) { + log.stdout( + "Generated wasm module '$outputFile'. See " + 'https://github.com/dart-lang/sdk/blob/main/pkg/dart2wasm/docs/standalone.md ' + 'for usage instructions.', + ); + log.stdout( + 'The standalone option is experimental, and used imports may change' + 'across Dart SDK releases.', + ); + } else { + final mjsFile = '$outputFileBasename.mjs'; + log.stdout( + "Generated wasm module '$outputFile', and JS init file '$mjsFile'.", + ); + } + return 0; } } diff --git a/pkg/dartdev/test/commands/compile_test.dart b/pkg/dartdev/test/commands/compile_test.dart index beae048bf4b..175e619cff7 100644 --- a/pkg/dartdev/test/commands/compile_test.dart +++ b/pkg/dartdev/test/commands/compile_test.dart @@ -883,6 +883,35 @@ void main() { ); }, skip: isRunningOnIA32); + test('Compile wasm standalone', () async { + final p = project( + mainSrc: ''' +void main() { + print('hello world'); +} +''', + ); + final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); + final outFile = path.canonicalize(path.join(p.dirPath, 'standalone.wasm')); + + final result = await p.run([ + 'compile', + 'wasm', + '--standalone', + '-o', + outFile, + inFile, + ]); + + expect(result.stdout, isNot(contains('.mjs'))); + expect(result.stdout, contains('standalone option is experimental')); + expect( + File(outFile).existsSync(), + true, + reason: 'Expected output file to exist: $outFile', + ); + }, skip: isRunningOnIA32); + test('Compile JS with sound null safety', () async { final p = project(mainSrc: '''void main() {}'''); final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn index 8983904a1ff..bf5151bec8f 100644 --- a/sdk/BUILD.gn +++ b/sdk/BUILD.gn @@ -670,12 +670,15 @@ copy("copy_dart2wasm_platform") { "../:dart2wasm_platform", "../utils/dart2wasm:compile_dart2wasm_js_compatibility_platform", "../utils/dart2wasm:compile_dart2wasm_platform", + "../utils/dart2wasm:compile_dart2wasm_standalone_platform", ] sources = [ "$root_out_dir/dart2wasm_js_compatibility_outline.dill", "$root_out_dir/dart2wasm_js_compatibility_platform.dill", "$root_out_dir/dart2wasm_outline.dill", "$root_out_dir/dart2wasm_platform.dill", + "$root_out_dir/dart2wasm_standalone_outline.dill", + "$root_out_dir/dart2wasm_standalone_platform.dill", ] outputs = [ "$root_out_dir/$dart_sdk_output/lib/_internal/{{source_file_part}}" ]