Files
sdk/pkg/dart2wasm/lib/js/runtime_generator.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

239 lines
7.2 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.
import 'dart:convert' show json;
import 'package:_js_interop_checks/src/js_interop.dart'
show calculateTransitiveImportsOfJsInteropIfUsed;
import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart';
import 'package:collection/collection.dart' show compareNatural;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import '../namer.dart';
import 'interop_transformer.dart';
import 'runtime_blob.dart';
import 'util.dart';
void _performJSInteropTransformations(
CoreTypes coreTypes,
ClassHierarchy classHierarchy,
Set<Library> interopDependentLibraries,
) {
// Transform kernel and generate JS methods.
final transformer = InteropTransformer(coreTypes, classHierarchy);
for (final library in interopDependentLibraries) {
transformer.visitLibrary(library);
}
// We want static types to help us specialize methods based on receivers.
// Therefore, erasure must come after the lowering.
final jsValueClass = coreTypes.index.getClass('dart:_js_helper', 'JSValue');
final staticInteropClassEraser = StaticInteropClassEraser(
coreTypes,
eraseStaticInteropType: (staticInteropType) =>
InterfaceType(jsValueClass, staticInteropType.declaredNullability),
additionalCoreLibraries: {
'_js_helper',
'_js_string_convert',
'_js_types',
'_string',
'convert',
'js_interop',
'js_interop_unsafe',
},
);
for (Library library in interopDependentLibraries) {
staticInteropClassEraser.visitLibrary(library);
}
}
class RuntimeFinalizer {
static String escape(String s) => json.encode(s);
final CoreTypes _coreTypes;
final InteropMemberNamer _interopMemberNamer;
RuntimeFinalizer(this._coreTypes, this._interopMemberNamer);
String generateJsMethods(Iterable<Procedure> translatedProcedures) {
Set<Procedure> usedProcedures = {};
final usedJSMethods = <({String importName, String jsCode})>[];
for (Procedure p in translatedProcedures) {
if (!usedProcedures.add(p)) continue;
final annotationInfo = JsInteropMemberData.fromMember(p, _coreTypes);
if (annotationInfo == null) continue;
switch (annotationInfo) {
case JsCodeData(:final jsCode):
final importName = _interopMemberNamer.getImportName(p)!.itemName;
usedJSMethods.add((importName: importName, jsCode: jsCode));
case JsTrampolineWrapperData():
final importName = _interopMemberNamer.getImportName(p)!.itemName;
usedJSMethods.add((
importName: importName,
jsCode: annotationInfo.jsCode(),
));
case JsTrampolineData():
// do nothing
}
}
// Sort so _9 comes before _11 (for example)
usedJSMethods.sort((a, b) => compareNatural(a.importName, b.importName));
final jsMethods = StringBuffer();
for (final (:importName, :jsCode) in usedJSMethods) {
jsMethods.write(' ');
jsMethods.write(importName);
jsMethods.write(': ');
final lines = _unindentJsCode(jsCode);
for (int i = 0; i < lines.length; ++i) {
if (i != 0) {
jsMethods.write(' ');
}
jsMethods.write(lines[i]);
if (i < (lines.length - 1)) {
jsMethods.writeln();
} else {
jsMethods.writeln(',');
}
}
}
return jsMethods.toString();
}
String _generateInternalizedStrings(
bool requireJsBuiltin,
List<String> constantStrings,
) {
final sb = StringBuffer();
String indent = '';
if (constantStrings.isNotEmpty) {
sb.writeln('s: [');
indent = ' ';
for (final c in constantStrings) {
sb.writeln('$indent ${escape(c)},');
}
sb.writeln('$indent],');
}
if (!requireJsBuiltin) {
sb.writeln(
'$indent"": new Proxy({}, { get(_, prop) { return prop; } }),',
);
}
return '$sb';
}
String generate(
String mainModuleName,
Iterable<Procedure> translatedProcedures,
List<String> constantStrings,
bool requireJsBuiltin,
bool supportsAdditionalModuleLoading,
) {
final jsMethods = generateJsMethods(translatedProcedures);
final builtins = [
'builtins: [\'js-string\']',
if (requireJsBuiltin) 'importedStringConstants: \'\'',
];
String internalizedStrings = _generateInternalizedStrings(
requireJsBuiltin,
constantStrings,
);
final jsStringBuiltinPolyfillImportVars = {
'JS_POLYFILL_IMPORT': requireJsBuiltin
? ''
: '"wasm:js-string": jsStringPolyfill,',
};
final moduleLoadingImportVars = {
'MODULE_LOADING_IMPORT': supportsAdditionalModuleLoading
? '"moduleLoadingHelper": moduleLoadingHelper,'
: '',
};
final moduleLoadingHelperMethods = supportsAdditionalModuleLoading
? moduleLoadingHelperTemplate.instantiate({
...jsStringBuiltinPolyfillImportVars,
'MAIN_MODULE_NAME': mainModuleName,
})
: '';
return jsRuntimeBlobTemplate.instantiate({
...jsStringBuiltinPolyfillImportVars,
...moduleLoadingImportVars,
'BUILTINS_MAP_BODY': builtins.join(', '),
'JS_METHODS': jsMethods,
'INTERNAL_IMPORTS_MODULE_NAME':
_interopMemberNamer.interopHelperModuleName,
'IMPORTED_JS_STRINGS_IN_MJS': internalizedStrings,
'JS_STRING_POLYFILL_METHODS': requireJsBuiltin ? '' : jsPolyFillMethods,
'DEFERRED_LIBRARY_HELPER_METHODS': moduleLoadingHelperMethods,
});
}
}
void performJSInteropTransformations(
List<Library> libraries,
CoreTypes coreTypes,
ClassHierarchy classHierarchy,
) {
Set<Library> transitiveImportingJSInterop = {
...calculateTransitiveImportsOfJsInteropIfUsed(
libraries,
Uri.parse("dart:_js_helper"),
),
...calculateTransitiveImportsOfJsInteropIfUsed(
libraries,
Uri.parse("dart:js_interop"),
),
};
return _performJSInteropTransformations(
coreTypes,
classHierarchy,
transitiveImportingJSInterop,
);
}
// Removes indentation common among all lines of [block] (except for first one)
List<String> _unindentJsCode(String block) {
block = block.trim();
final lines = block.split('\n');
if (lines.length == 1) return lines;
int minSpaces = -1;
for (int i = 1; i < lines.length; ++i) {
final line = lines[i];
final currentSpaces = _countLeadingSpaces(line);
if (currentSpaces == line.length) continue; // empty line
if (minSpaces == -1 || currentSpaces < minSpaces) {
minSpaces = currentSpaces;
}
}
if (minSpaces > 0) {
for (int i = 1; i < lines.length; ++i) {
final line = lines[i];
if (line.length <= minSpaces) {
lines[i] = '';
} else {
lines[i] = lines[i].substring(minSpaces);
}
}
}
return lines;
}
int _countLeadingSpaces(String line) {
int spaces = 0;
for (int i = 0; i < line.length; ++i) {
if (line.codeUnitAt(i) != ' '.codeUnitAt(0)) break;
spaces++;
}
return spaces;
}