Files
sdk/pkg/dart2wasm/lib/js/runtime_blob.dart
T
Martin Kustermann 2d78883f27 [dart2wasm] Simplify handling of JS interop callbacks
Right now a JS interop callback works like this:

* Each wasm module that gets instantiated will be given it's module
  instance (JS calls Dart to set it) via `setThisModule`

* When Dart code calls JS and gives it a callback to invoke, it gave it
  this module instance. It will also make the callback wasm function
  weakly exported.

* The JS trampoline code, when invoked, would then call the weakly
  exported wasm function from the module instance.

We simplify this now by making the Dart code simply give the wasm
function reference to JS, then JS can later on invoke it. No need to
weakly export a function and call back via
`module.exports.<weaklyExportedCallback>`

To ensure binaryen is aware that the wasm function may be called from
JS, we annotate it via the `(@binaryen.js.called)` annotation.

Change-Id: I828dd0cf8d3b36db338792c4e277a4bb94c76faf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/511080
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-06-11 12:24:16 -07:00

252 lines
8.1 KiB
Dart

// Copyright (c) 2023, 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.
final jsRuntimeBlobTemplate = Template(r'''
// Compiles a dart2wasm-generated main module from `source` which can then
// be instantiated 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_MAP_BODY>>};
return new CompiledApp(
await WebAssembly.compileStreaming(source, builtins), builtins);
}
// Compiles a dart2wasm-generated wasm module from `bytes` which is then
// instantiable via the `instantiate` method.
export async function compile(bytes) {
const builtins = {<<BUILTINS_MAP_BODY>>};
return new CompiledApp(await WebAssembly.compile(bytes, builtins), builtins);
}
class CompiledApp {
constructor(module, builtins) {
this.module = module;
this.builtins = builtins;
}
// The second argument is an options object containing:
// `loadDeferredModules` is a JS function that takes an array of module names
// matching wasm files produced by the dart2wasm compiler. It also takes a
// callback that should be invoked for each loaded module with 2 arguments:
// (1) the module name, (2) the loaded module in a format supported by
// `WebAssembly.compile` or `WebAssembly.compileStreaming`. The callback
// returns a Promise that resolves when the module is instantiated.
// loadDeferredModules should return a Promise that resolves when all the
// modules have been loaded and the callback promises have resolved.
// `loadDeferredId` is a JS function that takes load ID produced by the
// compiler when the `use-load-ids` option is passed. Each load ID maps to
// one or more wasm files as specified in the emitted JSON file. It also
// takes a callback that should be invoked for each loaded module with 2
// arguments: (1) the module name, (2) the loaded module in a format
// supported by `WebAssembly.compile` or `WebAssembly.compileStreaming`.
// The callback returns a Promise that resolves when the module is
// instantiated.
// loadDeferredId should return a Promise that resolves when all the
// modules have been loaded and the callback promises have resolved.
async instantiate(additionalImports, {loadDeferredModules, loadDeferredId} = {}) {
let dartInstance;
// Prints to the console
function printToConsole(value) {
if (typeof dartPrint == "function") {
dartPrint(value);
return;
}
if (typeof console == "object" && typeof console.log != "undefined") {
console.log(value);
return;
}
if (typeof print == "function") {
print(value);
return;
}
throw "Unable to print message: " + value;
}
// A special symbol attached to functions that wrap Dart functions.
const jsWrappedDartFunctionSymbol = Symbol("JSWrappedDartFunction");
function finalizeWrapper(dartFunction, wrapped) {
wrapped.dartFunction = dartFunction;
wrapped[jsWrappedDartFunctionSymbol] = true;
return wrapped;
}
// Imports
const dart2wasm = {
<<JS_METHODS>>
};
const baseImports = {
<<INTERNAL_IMPORTS_MODULE_NAME>>: dart2wasm,
Math: Math,
Date: Date,
Object: Object,
Array: Array,
Reflect: Reflect,
WebAssembly: {
JSTag: WebAssembly.JSTag,
},
<<IMPORTED_JS_STRINGS_IN_MJS>>
};
<<JS_STRING_POLYFILL_METHODS>>
<<DEFERRED_LIBRARY_HELPER_METHODS>>
dartInstance = await WebAssembly.instantiate(this.module, {
...baseImports,
...additionalImports,
<<MODULE_LOADING_IMPORT>>
<<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;
if (s1 > s2) return 1;
return 0;
},
"concat": (s1, s2) => s1 + s2,
"equals": (s1, s2) => s1 === s2,
"fromCharCode": (i) => String.fromCharCode(i),
"length": (s) => s.length,
"substring": (s, a, b) => s.substring(a, b),
"fromCharCodeArray": (a, start, end) => {
if (end <= start) return '';
const read = dartInstance.exports.$wasmI16ArrayGet;
let result = '';
let index = start;
const chunkLength = Math.min(end - index, 500);
let array = new Array(chunkLength);
while (index < end) {
const newChunkLength = Math.min(end - index, 500);
for (let i = 0; i < newChunkLength; i++) {
array[i] = read(a, index++);
}
if (newChunkLength < chunkLength) {
array = array.slice(0, newChunkLength);
}
result += String.fromCharCode(...array);
}
return result;
},
"intoCharCodeArray": (s, a, start) => {
if (s === '') return 0;
const write = dartInstance.exports.$wasmI16ArraySet;
for (var i = 0; i < s.length; ++i) {
write(a, start++, s.charCodeAt(i));
}
return s.length;
},
"test": (s) => typeof s == "string",
};
''';
final moduleLoadingHelperTemplate = Template(r'''
async function handleDeferredModuleBytes(moduleName, source) {
const builtins = this.builtins;
const module = await ((source instanceof Response)
? WebAssembly.compileStreaming(source, builtins)
: WebAssembly.compile(source, builtins));
let moduleInstance = await WebAssembly.instantiate(module, {
...baseImports,
...additionalImports,
<<JS_POLYFILL_IMPORT>>
"<<MAIN_MODULE_NAME>>": dartInstance.exports,
});
}
const moduleLoadingHelper = {
"loadDeferredModules": async (moduleNames) => {
if (!loadDeferredModules) {
throw "No implementation of loadDeferredModules provided.";
}
await loadDeferredModules(moduleNames, handleDeferredModuleBytes.bind(this));
},
"loadDeferredId": async (loadId) => {
if (!loadDeferredId) {
throw "No implementation of loadDeferredId provided.";
}
await loadDeferredId(loadId, handleDeferredModuleBytes.bind(this));
},
};
''');
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';
}
}