[dart2wasm] Generate *.support.js feature detection files

The file will contain a JS expression that evaluates to a boolean.
If it (at runtime) evalutes to

  * `true` it means that all required features are supported by the JS
    environment and the dart2wasm-compiled app can be used

  * `false` it means some features were not present in the JS
    environment and the dart2wasm-compiled app shouldn't be used,
    instead a dart2js fallback may be used

We introduce this mechanism to allow users, at compilation time, to tell
dart2wasm to take advantage of new spec features and allow the runtime
to self-detect whether they are available and fallback to dart2js if
not.

The first feature we introduce (already in this PR) is
`--require-js-string-builtin` that will tell dart2wasm it can assume the
`js-string` builtin is available (and emit corresponding `*.support.js`
code to detect it).

If the flag was passed, we take advantage of the `js-string` import
mechanism for string constants that doesn't require emitting them in the
mjs file (which significantly reduces code size and improves startup
time - compared with emitting JS strings in the mjs file).

We enable `--require-js-string-builtin` on one CI configuration for
testing that if we don't use any polyfill, the imports of the builtin
functions as well as magical utf8-encoded wasm imports work.

We also use a template mechanism to generate `*.mjs` as the code
becomes more readable (e.g. to conditionally include the js string
polyfill)

Issue https://github.com/dart-lang/sdk/issues/59951

Change-Id: Ic7e7818a2d5269095935022941352beeb9fed731
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/408781
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Martin Kustermann
2025-02-10 06:17:49 -08:00
committed by Commit Queue
parent 0246cc613f
commit 3b750c5545
9 changed files with 210 additions and 82 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ def list_imports(uri, exec_root, package_config):
]:
continue
# Imports must happen before definitions.
if tokens[0] in ['const', 'class', 'enum']:
if tokens[0] in ['const', 'class', 'enum', 'final']:
break
if 2 <= len(tokens
) and tokens[0] == 'if' and tokens[1] == '(dart.library.io)':
+48 -7
View File
@@ -48,8 +48,9 @@ sealed class CompilationResult {}
class CompilationSuccess extends CompilationResult {
final Map<String, ({Uint8List moduleBytes, String? sourceMap})> wasmModules;
final String jsRuntime;
final String supportJs;
CompilationSuccess(this.wasmModules, this.jsRuntime);
CompilationSuccess(this.wasmModules, this.jsRuntime, this.supportJs);
}
class CompilationError extends CompilationResult {}
@@ -238,11 +239,8 @@ Future<CompilationResult> compileToModule(
String? depFile = options.depFile;
if (depFile != null) {
writeDepfile(
compilerOptions.fileSystem,
component.uriToSource.keys,
options.outputFile,
depFile);
writeDepfile(compilerOptions.fileSystem, component.uriToSource.keys,
options.outputFile, depFile);
}
final generateSourceMaps = options.translatorOptions.generateSourceMaps;
@@ -262,7 +260,50 @@ Future<CompilationResult> compileToModule(
String jsRuntime = jsRuntimeFinalizer.generate(
translator.functions.translatedProcedures,
translator.internalizedStringsForJSRuntime,
translator.options.requireJsStringBuiltin,
mode);
return CompilationSuccess(wasmModules, jsRuntime);
final supportJs = _generateSupportJs(options.translatorOptions);
return CompilationSuccess(wasmModules, jsRuntime, supportJs);
}
String _generateSupportJs(TranslatorOptions options) {
// Copied from
// https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js
//
// Uses WasmGC types and will only validate correctly if the engine supports
// WasmGC:
// ```
// (module
// (type $type0 (struct (field $field0 i8)))
// )
// ```
//
// NOTE: Once we support more feature detections we may use
// `package:wasm_builder` to create the module instead of having a fixed one
// here.
const String supportsWasmGC =
'WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,95,1,120,0]))';
// Imports a `js-string` builtin spec function *with wrong signature*. An engine
//
// * *without* knowledge about `js-string` builtin would accept such an import at
// validation time.
//
// * *with* knowledge about `js-string` would refuse it as the signature
// used to import the `cast` function is not according to `js-string` spec
//
// ```
// (module
// (func $wasm:js-string.cast (;0;) (import "wasm:js-string" "cast"))
// )
// ```
const String supportsJsStringBuiltins =
'!WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,23,1,14,119,97,115,109,58,106,115,45,115,116,114,105,110,103,4,99,97,115,116,0,0]),{"builtins":["js-string"]})';
final requiredFeatures = [
supportsWasmGC,
if (options.requireJsStringBuiltin) supportsJsStringBuiltins
];
return '(${requiredFeatures.join('&&')})';
}
-1
View File
@@ -15,7 +15,6 @@ class WasmCompilerOptions {
Uri mainUri;
String outputFile;
String? depFile;
String? outputJSRuntimeFile;
Uri? dynamicModuleMainUri;
Uri? dynamicInterfaceUri;
Uri? dynamicModuleMetadataFile;
+3 -2
View File
@@ -78,8 +78,6 @@ final List<Option> options = [
StringMultiOption("delete-tostring-package-uri",
(o, values) => o.deleteToStringPackageUri = values),
StringOption("depfile", (o, value) => o.depFile = value),
StringOption(
"js-runtime-output", (o, value) => o.outputJSRuntimeFile = value),
StringOption(
"dump-kernel-after-cfe", (o, value) => o.dumpKernelAfterCfe = value,
hide: true),
@@ -99,6 +97,9 @@ final List<Option> options = [
Flag("enable-deferred-loading",
(o, value) => o.translatorOptions.enableDeferredLoading = value,
defaultsTo: _d.translatorOptions.enableDeferredLoading),
Flag("require-js-string-builtin",
(o, value) => o.translatorOptions.requireJsStringBuiltin = value,
defaultsTo: _d.translatorOptions.requireJsStringBuiltin),
Flag("enable-multi-module-stress-test-mode",
(o, value) => o.translatorOptions.enableMultiModuleStressTestMode = value,
defaultsTo: _d.translatorOptions.enableMultiModuleStressTestMode),
+4 -2
View File
@@ -113,9 +113,11 @@ Future<int> generateWasm(WasmCompilerOptions options,
});
await Future.wait(writeFutures);
final jsFile = options.outputJSRuntimeFile ??
path.setExtension(options.outputFile, '.mjs');
final jsFile = path.setExtension(options.outputFile, '.mjs');
await File(jsFile).writeAsString(result.jsRuntime);
final supportJsFile = path.setExtension(options.outputFile, '.support.js');
await File(supportJsFile).writeAsString(result.supportJs);
return 0;
}
+100 -55
View File
@@ -2,15 +2,14 @@
// 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.
const jsRuntimeBlobPart1 = r'''
final jsRuntimeBlobTemplate = Template(r'''
// Compiles a dart2wasm-generated main module from `source` which can then
// instantiatable via the `instantiate` method.
//
// `source` needs to be a `Response` object (or promise thereof) e.g. created
// via the `fetch()` JS API.
export async function compileStreaming(source) {
const builtins = {builtins: ['js-string']};
const builtins = {<<BUILTINS_MAP_BODY>>};
return new CompiledApp(
await WebAssembly.compileStreaming(source, builtins), builtins);
}
@@ -18,7 +17,7 @@ export async function compileStreaming(source) {
// Compiles a dart2wasm-generated wasm modules from `bytes` which is then
// instantiatable via the `instantiate` method.
export async function compile(bytes) {
const builtins = {builtins: ['js-string']};
const builtins = {<<BUILTINS_MAP_BODY>>};
return new CompiledApp(await WebAssembly.compile(bytes, builtins), builtins);
}
@@ -97,27 +96,65 @@ class CompiledApp {
// Imports
const dart2wasm = {
''';
// We break inside the 'dart2wasm' object to enable injection of methods. We
// could use interpolation, but then we'd have to escape characters.
const jsRuntimeBlobPart2 = r'''
<<JS_METHODS>>
};
const baseImports = {
dart2wasm: dart2wasm,
''';
// We break inside of `baseImports` to inject internalized strings.
const jsRuntimeBlobPart3 = r'''
Math: Math,
Date: Date,
Object: Object,
Array: Array,
Reflect: Reflect,
<<IMPORTED_JS_STRINGS_IN_MJS>>
};
const jsStringPolyfill = {
<<JS_STRING_POLYFILL_METHODS>>
const deferredLibraryHelper = {
"loadModule": async (moduleName) => {
if (!loadDeferredWasm) {
throw "No implementation of loadDeferredWasm provided.";
}
const source = await Promise.resolve(loadDeferredWasm(moduleName));
const module = await ((source instanceof Response)
? WebAssembly.compileStreaming(source, this.builtins)
: WebAssembly.compile(source, this.builtins));
return await WebAssembly.instantiate(module, {
...baseImports,
...additionalImports,
<<JS_POLYFILL_IMPORT>>
"module0": dartInstance.exports,
});
},
};
dartInstance = await WebAssembly.instantiate(this.module, {
...baseImports,
...additionalImports,
"deferredLibraryHelper": deferredLibraryHelper,
<<JS_POLYFILL_IMPORT>>
});
return new InstantiatedApp(this, dartInstance);
}
}
class InstantiatedApp {
constructor(compiledApp, instantiatedModule) {
this.compiledApp = compiledApp;
this.instantiatedModule = instantiatedModule;
}
// Call the main function with the given arguments.
invokeMain(...args) {
this.instantiatedModule.exports.$invokeMain(args);
}
}
''');
const String jsPolyFillMethods = r'''
const jsStringPolyfill = {
"charCodeAt": (s, i) => s.charCodeAt(i),
"compare": (s1, s2) => {
if (s1 < s2) return -1;
@@ -150,45 +187,53 @@ const jsRuntimeBlobPart3 = r'''
return result;
},
};
const deferredLibraryHelper = {
"loadModule": async (moduleName) => {
if (!loadDeferredWasm) {
throw "No implementation of loadDeferredWasm provided.";
}
const source = await Promise.resolve(loadDeferredWasm(moduleName));
const module = await ((source instanceof Response)
? WebAssembly.compileStreaming(source, this.builtins)
: WebAssembly.compile(source, this.builtins));
return await WebAssembly.instantiate(module, {
...baseImports,
...additionalImports,
"wasm:js-string": jsStringPolyfill,
"module0": dartInstance.exports,
});
},
};
dartInstance = await WebAssembly.instantiate(this.module, {
...baseImports,
...additionalImports,
"deferredLibraryHelper": deferredLibraryHelper,
"wasm:js-string": jsStringPolyfill,
});
return new InstantiatedApp(this, dartInstance);
}
}
class InstantiatedApp {
constructor(compiledApp, instantiatedModule) {
this.compiledApp = compiledApp;
this.instantiatedModule = instantiatedModule;
}
// Call the main function with the given arguments.
invokeMain(...args) {
this.instantiatedModule.exports.$invokeMain(args);
}
}
''';
class Template {
static final _templateVariableRegExp = RegExp(r'<<(?<varname>[A-Z_]+)>>');
final List<_TemplatePart> _parts = [];
Template(String stringTemplate) {
int offset = 0;
for (final match in _templateVariableRegExp.allMatches(stringTemplate)) {
_parts.add(
_TemplateStringPart(stringTemplate.substring(offset, match.start)));
_parts.add(_TemplateVariablePart(match.namedGroup('varname')!));
offset = match.end;
}
_parts.add(_TemplateStringPart(
stringTemplate.substring(offset, stringTemplate.length)));
}
String instantiate(Map<String, String> variableValues) {
final sb = StringBuffer();
for (final part in _parts) {
sb.write(part.instantiate(variableValues));
}
return sb.toString();
}
}
abstract class _TemplatePart {
String instantiate(Map<String, String> variableValues);
}
class _TemplateStringPart extends _TemplatePart {
final String string;
_TemplateStringPart(this.string);
@override
String instantiate(Map<String, String> variableValues) => string;
}
class _TemplateVariablePart extends _TemplatePart {
final String variable;
_TemplateVariablePart(this.variable);
@override
String instantiate(Map<String, String> variableValues) {
final value = variableValues[variable];
if (value != null) return value;
throw 'Template contains no value for variable $variable';
}
}
+19 -9
View File
@@ -54,8 +54,11 @@ class RuntimeFinalizer {
RuntimeFinalizer(this.allJSMethods);
String generate(Iterable<Procedure> translatedProcedures,
List<String> constantStrings, wasm_target.Mode mode) {
String generate(
Iterable<Procedure> translatedProcedures,
List<String> constantStrings,
bool requireJsBuiltin,
wasm_target.Mode mode) {
String escape(String s) => json.encode(s);
Set<Procedure> usedProcedures = {};
@@ -87,6 +90,11 @@ class RuntimeFinalizer {
}
}
final builtins = [
'builtins: [\'js-string\']',
if (requireJsBuiltin) 'importedStringConstants: \'S\'',
];
String internalizedStrings = '';
if (constantStrings.isNotEmpty) {
internalizedStrings = '''
@@ -95,13 +103,15 @@ class RuntimeFinalizer {
],
''';
}
return '''
$jsRuntimeBlobPart1
$jsMethods
$jsRuntimeBlobPart2
$internalizedStrings
$jsRuntimeBlobPart3
''';
return jsRuntimeBlobTemplate.instantiate({
'BUILTINS_MAP_BODY': builtins.join(', '),
'JS_METHODS': jsMethods.toString(),
'IMPORTED_JS_STRINGS_IN_MJS': internalizedStrings,
'JS_STRING_POLYFILL_METHODS': requireJsBuiltin ? '' : jsPolyFillMethods,
'JS_POLYFILL_IMPORT':
requireJsBuiltin ? '' : '"wasm:js-string": jsStringPolyfill,'
});
}
}
+33 -4
View File
@@ -55,6 +55,7 @@ class TranslatorOptions {
bool enableMultiModuleStressTestMode = false;
int inliningLimit = 0;
int? sharedMemoryMaxPages;
bool requireJsStringBuiltin = false;
List<int> watchPoints = [];
}
@@ -1601,11 +1602,39 @@ class Translator with KernelNodes {
if (internalizedString != null) {
return internalizedString;
}
final i = internalizedStringsForJSRuntime.length;
internalizedString = module.globals.import('s', '$i',
w.GlobalType(w.RefType.extern(nullable: true), mutable: false));
bool hasUnpairedSurrogate(String str) {
for (int i = 0; i < str.length; i++) {
int codeUnit = str.codeUnitAt(i);
if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF) {
if (i + 1 >= str.length ||
str.codeUnitAt(i + 1) < 0xDC00 ||
str.codeUnitAt(i + 1) > 0xDFFF) {
return true;
} else {
i++;
}
} else if (codeUnit >= 0xDC00 && codeUnit <= 0xDFFF) {
return true;
}
}
return false;
}
if (!options.requireJsStringBuiltin || hasUnpairedSurrogate(s)) {
// Unpaired surrogates can't be encoded as UTF-8, import them from JS
// runtime.
final i = internalizedStringsForJSRuntime.length;
internalizedString = module.globals.import('s', '$i',
w.GlobalType(w.RefType.extern(nullable: true), mutable: false));
internalizedStringsForJSRuntime.add(s);
} else {
internalizedString = module.globals.import(
'S',
s,
w.GlobalType(w.RefType.extern(nullable: true), mutable: false),
);
}
_internalizedStringGlobals[(module, s)] = internalizedString;
internalizedStringsForJSRuntime.add(s);
return internalizedString;
}
}
+2 -1
View File
@@ -461,7 +461,8 @@
"host-asserts": true,
"enable-asserts": true,
"dart2wasm-options": [
"-O0"
"-O0",
"--extra-compiler-option=--require-js-string-builtin"
],
"timeout": 60
}