Files
sdk/pkg/wasm_builder/lib/src/ir/function.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

237 lines
5.7 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 '../serialize/printer.dart';
import '../serialize/serialize.dart';
import 'ir.dart';
/// A local variable defined in a function.
class Local {
final int index;
final ValueType type;
Local(this.index, this.type);
void printTo(
IrPrinter p,
Map<int, String> localNames, {
bool isParam = false,
}) {
p.write(isParam ? 'param' : 'local');
p.write(' ');
p.writeLocalIndexReference(index);
p.write(' ');
p.writeValueType(type);
}
@override
String toString() => "$index";
}
/// An (imported or defined) function.
abstract class BaseFunction with Indexable, Exportable {
@override
final FinalizableIndex finalizableIndex;
final FunctionType type;
String? functionName;
@override
final Module enclosingModule;
/// Whether this function is pure and has no effect.
///
/// If marked as pure, we'll emit metadata in the
/// `binaryen.removable.if.unused` custom section.
bool isPure = false;
/// Whether this function is called from JS.
///
/// If marked as isJSCalled, we'll emit metadata in the
/// `binaryen.js.called` custom section.
bool isJSCalled = false;
/// Inline hint for this function.
///
/// If set, we'll emit metadata in the `binaryen.inline` custom section.
/// Value must be in range [0..127].
int? inlineHint;
BaseFunction(
this.enclosingModule,
this.finalizableIndex,
this.type, [
this.functionName,
]);
@override
String get name => functionName ?? super.name;
/// Creates an export of this function in this module.
@override
Export buildExport(String name) {
return FunctionExport(name, this);
}
}
/// A function defined in a module.
class DefinedFunction extends BaseFunction implements Serializable {
late final Instructions body;
/// All local variables defined in the function, including its inputs.
List<Local> get locals => body.locals;
Map<int, String> get localNames => body.localNames;
DefinedFunction(
super.enclosingModule,
this.body,
super.finalizableIndex,
super.type, [
super.functionName,
]);
DefinedFunction.withoutBody(
super.enclosingModule,
super.finalizableIndex,
super.type, [
super.functionName,
]);
@override
void serialize(Serializer s) {
// Serialize locals internally first in order to compute the total size of
// the serialized data.
final localS = Serializer();
int paramCount = type.inputs.length;
int entries = 0;
for (int i = paramCount + 1; i <= locals.length; i++) {
if (i == locals.length || locals[i - 1].type != locals[i].type) entries++;
}
localS.writeUnsigned(entries);
int start = paramCount;
for (int i = paramCount + 1; i <= locals.length; i++) {
if (i == locals.length || locals[i - 1].type != locals[i].type) {
localS.writeUnsigned(i - start);
localS.write(locals[i - 1].type);
start = i;
}
}
// Bundle locals and body
localS.write(body);
s.writeUnsigned(localS.data.length);
s.sourceMapSerializer.copyMappings(localS.sourceMapSerializer, s.offset);
s.writeData(localS);
}
void printTo(IrPrinter p) {
if (isPure) {
p.writeln('(@binaryen.removable.if.unused)');
}
if (isJSCalled) {
p.writeln('(@binaryen.js.called)');
}
if (inlineHint != null) {
p.writeln('(@binaryen.inline $inlineHint)');
}
p.write('(func ');
p.writeFunctionReference(this);
String? exportName;
for (final f in enclosingModule.exports.exported) {
if (f is FunctionExport && f.function == this) {
exportName = f.name;
break;
}
}
if (exportName != null) {
p.write(' ');
p.writeExport(exportName);
}
p.withLocalNames(localNames, () {
if (type.inputs.isNotEmpty || type.outputs.isNotEmpty) {
p.write(' ');
type.printSignatureWithNamesTo(p, oneLine: true);
}
p.writeln('');
p.withIndent(() {
for (int i = type.inputs.length; i < locals.length; ++i) {
p.write('(');
locals[i].printTo(p, localNames);
p.writeln(')');
}
body.printTo(p);
});
});
p.write(')');
}
void printDeclarationTo(IrPrinter p) {
p.write('(func \$$functionName ');
p.withLocalNames(localNames, () {
type.printSignatureWithNamesTo(p, oneLine: true);
});
p.write(' <...>');
p.writeln(')');
}
@override
String toString() => functionName ?? "#$finalizableIndex";
}
/// An imported function.
class ImportedFunction extends BaseFunction implements Import {
@override
final String module;
@override
final String name;
ImportedFunction(
super.enclosingModule,
this.module,
this.name,
super.finalizableIndex,
super.type, [
super.functionName,
]);
@override
void serialize(Serializer s) {
s.writeName(module);
s.writeName(name);
s.writeByte(0x00);
s.writeUnsigned(type.index);
}
void printTo(IrPrinter p) {
assert(!isJSCalled);
assert(inlineHint == null);
if (isPure) {
p.writeln('(@binaryen.removable.if.unused)');
}
p.write('(func ');
p.writeFunctionReference(this);
p.write(' ');
p.writeImport(module, name);
p.write(' ');
type.printOneLineSignatureTo(p);
p.write(')');
}
@override
String toString() => "$module.$name";
}
class FunctionExport extends Export {
final BaseFunction function;
FunctionExport(super.name, this.function);
@override
void serialize(Serializer s) {
s.writeName(name);
s.writeByte(0x00);
s.writeUnsigned(function.index);
}
}