From d66befd34722dbcd80cbc92d73cc0dcfefd6cce1 Mon Sep 17 00:00:00 2001 From: Martin Kustermann Date: Fri, 14 Mar 2025 01:31:10 -0700 Subject: [PATCH] [dart2wasm] Add support for `@pragma('wasm:weak-export', '')` We explicitly & unconditionally export functions to JS via the `@pragma('wasm:export', '')` annotation. This is mainly useful for external APIs that the JavaScript side can invoke - for example `$invokeMain()`. Though we currently use the same mechanism also in other places where a Dart function `A` (if used) calls to JS which calls back into Dart via calling exported Dart function `A*`. The issue is that this mechanism doesn't work very well with tree shaking: If the function `A` is not used it will be tree shaken. We will then also not emit the JS code, but we still compile the exported function `A*` as it's a root due to `@pragma('wasm:export', '` functions that call out to JS which call back into a dart-exported `_` function. The former may be unused & tree shaken (as well as their JS code) but the ladder are force-exported and therefore treated as entrypoints. This CL solves problem by * Mark function `A*` as weakly exported via `@pragma('wasm:weak-export', '')` => TFA will not consider such functions as entrypoints => TFA will only retain such functions if they are referenced by other functions that aren't tree shaken. => The backend will export such functions as `` if they are referenced by any other code that's compiled. * Making the code that calls function `A` also reference (but not use) `A*`. => This will make TFA retain function `A*` if it retains `A`. => In core libraries we manually reference `A*` in code that uses `A` and mark `A*` as weakly exported => In JS interop transformer we emit similar code for callbacks => We refer `A*` by using `exportWasmFunction()` which is an opaque external function that prevents TFA and backend from optimizing it away - so the function will be generated & exported. Overall this CL ensures we only keep the exported functions if we actually need them (i.e. we call to JS and JS calls those exported functions). This shrinks stripped dart2wasm hello world file in -O4 from 28 KB to 12 KB. It also enables tree shaking of callback using JS interop code. Change-Id: Ie81eac49cbcb574d569ea95a90538e8f417e2a12 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/415220 Reviewed-by: Srujan Gaddam Commit-Queue: Martin Kustermann --- pkg/dart2wasm/lib/code_generator.dart | 12 ++ pkg/dart2wasm/lib/functions.dart | 107 ++++++++++-------- pkg/dart2wasm/lib/intrinsics.dart | 23 ++++ .../lib/js/callback_specializer.dart | 81 ++++++++----- pkg/dart2wasm/lib/js/util.dart | 10 +- pkg/dart2wasm/lib/kernel_nodes.dart | 2 + sdk/lib/_internal/wasm/lib/async_patch.dart | 2 +- .../_internal/wasm/lib/internal_patch.dart | 23 +--- .../_internal/wasm/lib/js_helper_patch.dart | 51 +++++++-- sdk/lib/_internal/wasm/lib/timer_patch.dart | 66 ++++++++--- 10 files changed, 255 insertions(+), 122 deletions(-) diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart index 3c5087d1470..bbb925c0955 100644 --- a/pkg/dart2wasm/lib/code_generator.dart +++ b/pkg/dart2wasm/lib/code_generator.dart @@ -1700,6 +1700,18 @@ abstract class AstCodeGenerator } Member? singleTarget = translator.singleTarget(node); + + // Custom devirtualization because TFA doesn't correctly devirtualize index + // accesses on constant lists (see https://dartbug.com/60313) + if (singleTarget == null && + target.kind == ProcedureKind.Operator && + target.name.text == '[]') { + final receiver = node.receiver; + if (receiver is ConstantExpression && receiver.constant is ListConstant) { + singleTarget = translator.listBaseIndexOperator; + } + } + if (singleTarget != null) { final target = translator.getFunctionEntry(singleTarget.reference, uncheckedEntry: useUncheckedEntry); diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart index bb643fc5991..5826201aa53 100644 --- a/pkg/dart2wasm/lib/functions.dart +++ b/pkg/dart2wasm/lib/functions.dart @@ -23,8 +23,6 @@ class FunctionCollector { final Map _functions = {}; // Wasm function for each function expression and local function. final Map _lambdas = {}; - // Names of exported functions - final Map _exports = {}; // Selector IDs that are invoked via GDT. final Set _calledSelectors = {}; final Set _calledUncheckedSelectors = {}; @@ -42,14 +40,13 @@ class FunctionCollector { void _collectImportsAndExports() { for (Library library in translator.libraries) { library.procedures.forEach(_importOrExport); - library.fields.forEach(_importOrExport); for (Class cls in library.classes) { cls.procedures.forEach(_importOrExport); } } } - void _importOrExport(Member member) { + void _importOrExport(Procedure member) { String? importName = translator.getPragma(member, "wasm:import", member.name.text); if (importName != null) { @@ -58,31 +55,34 @@ class FunctionCollector { assert(!member.isInstanceMember); String module = importName.substring(0, dot); String name = importName.substring(dot + 1); - if (member is Procedure) { - w.FunctionType ftype = _makeFunctionType( - translator, member.reference, null, - isImportOrExport: true); - _functions[member.reference] = translator - .moduleForReference(member.reference) - .functions - .import(module, name, ftype, "$importName (import)"); - } + final ftype = _makeFunctionType(translator, member.reference, null, + isImportOrExport: true); + _functions[member.reference] = translator + .moduleForReference(member.reference) + .functions + .import(module, name, ftype, "$importName (import)"); } } + + // Ensure any procedures marked as exported are enqueued. String? exportName = translator.getPragma(member, "wasm:export", member.name.text); if (exportName != null) { - if (member is Procedure) { - _makeFunctionType(translator, member.reference, null, - isImportOrExport: true); - } - _exports[member.reference] = exportName; + getFunction(member.reference); } } /// If the member with the reference [target] is exported, get the export /// name. - String? getExportName(Reference target) => _exports[target]; + String? getExportName(Reference target) { + final member = target.asMember; + if (member.reference == target) { + final text = member.name.text; + return translator.getPragma(member, "wasm:export", text) ?? + translator.getPragma(member, "wasm:weak-export", text); + } + return null; + } w.BaseFunction importFunctionToDynamicModule(w.BaseFunction fun) { assert(translator.isDynamicModule); @@ -110,31 +110,6 @@ class FunctionCollector { } } - // Add exports to the module and add exported functions to the - // compilationQueue. - for (var export in _exports.entries) { - Reference target = export.key; - Member node = target.asMember; - if (node is Procedure) { - assert(!node.isInstanceMember); - assert(!node.isGetter); - w.FunctionType ftype = - _makeFunctionType(translator, target, null, isImportOrExport: true); - final module = translator.moduleForReference(target); - w.FunctionBuilder function = module.functions.define(ftype, "$node"); - _functions[target] = function; - module.exports.export(export.value, function); - translator.compilationQueue.add(AstCompilationTask(function, - getMemberCodeGenerator(translator, function, target), target)); - } else if (node is Field) { - final module = translator.moduleForReference(target); - w.Table? table = translator.getTable(module, node); - if (table != null) { - module.exports.export(export.value, table); - } - } - } - // Value classes are always implicitly allocated. recordClassAllocation( translator.classInfo[translator.boxedBoolClass]!.classId); @@ -150,9 +125,49 @@ class FunctionCollector { w.BaseFunction getFunction(Reference target) { return _functions.putIfAbsent(target, () { + final member = target.asMember; + + // If this function is a `@pragma('wasm:import', ':')` we + // import the function and return it. + if (member.reference == target && member.annotations.isNotEmpty) { + final importName = + translator.getPragma(member, 'wasm:import', member.name.text); + if (importName != null) { + assert(!member.isInstanceMember); + int dot = importName.indexOf('.'); + if (dot != -1) { + final module = importName.substring(0, dot); + final name = importName.substring(dot + 1); + final ftype = _makeFunctionType(translator, member.reference, null, + isImportOrExport: true); + return _functions[member.reference] = translator + .moduleForReference(member.reference) + .functions + .import(module, name, ftype, "$importName (import)"); + } + } + } + + // If this function is exported via + // * `@pragma('wasm:export', '')` or + // * `@pragma('wasm:weak-export', '')` + // we export it under the given `` + String? exportName; + if (member.reference == target && member.annotations.isNotEmpty) { + exportName = translator.getPragma( + member, 'wasm:export', member.name.text) ?? + translator.getPragma(member, 'wasm:weak-export', member.name.text); + assert(exportName == null || member is Procedure && member.isStatic); + } + + final w.FunctionType ftype = exportName != null + ? _makeFunctionType(translator, target, null, isImportOrExport: true) + : translator.signatureForDirectCall(target); + final module = translator.moduleForReference(target); - final function = module.functions.define( - translator.signatureForDirectCall(target), getFunctionName(target)); + final function = module.functions.define(ftype, getFunctionName(target)); + if (exportName != null) module.exports.export(exportName, function); + translator.compilationQueue.add(AstCompilationTask(function, getMemberCodeGenerator(translator, function, target), target)); diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart index ca9512cad70..d51273d4872 100644 --- a/pkg/dart2wasm/lib/intrinsics.dart +++ b/pkg/dart2wasm/lib/intrinsics.dart @@ -164,6 +164,7 @@ enum StaticIntrinsic { intBitsToFloat('dart:_internal', null, 'intBitsToFloat'), doubleToIntBits('dart:_internal', null, 'doubleToIntBits'), intBitsToDouble('dart:_internal', null, 'intBitsToDouble'), + exportWasmFunction('dart:_internal', null, 'exportWasmFunction'), getID('dart:_internal', 'ClassID', 'getID'), loadInt8('dart:ffi', null, '_loadInt8'), loadUint8('dart:ffi', null, '_loadUint8'), @@ -1162,6 +1163,28 @@ class Intrinsifier { node.arguments.positional.single, w.NumType.i64); b.f64_reinterpret_i64(); return w.NumType.f64; + case StaticIntrinsic.exportWasmFunction: + const error = + 'The `dart:_internal:exportWasmFunction` expects its argument ' + 'to be a tear-off of a `@pragma(\'wasm:weak-export\', ...)` ' + 'annotated function'; + + // Sanity check argument. + final argument = node.arguments.positional.single; + if (argument is! ConstantExpression) throw error; + final constant = argument.constant; + if (constant is! StaticTearOffConstant) throw error; + final target = constant.target; + if (translator.getPragma(target, 'wasm:weak-export', '') == null) { + throw error; + } + + // Ensure we compile the target function & export it. + translator.functions.getFunction(target.reference); + + final topType = translator.topInfo.nullableType; + codeGen.translateExpression(NullLiteral(), topType); + return topType; case StaticIntrinsic.getID: ClassInfo info = translator.topInfo; codeGen.translateExpression( diff --git a/pkg/dart2wasm/lib/js/callback_specializer.dart b/pkg/dart2wasm/lib/js/callback_specializer.dart index 838188fccb4..ca7f2263fc9 100644 --- a/pkg/dart2wasm/lib/js/callback_specializer.dart +++ b/pkg/dart2wasm/lib/js/callback_specializer.dart @@ -63,9 +63,8 @@ class CallbackSpecializer { /// returned value. [node] is the conversion function that was called to /// convert the callback. /// - /// Returns a [String] function name representing the name of the wrapping - /// function. - String _createFunctionTrampoline(Procedure node, FunctionType function, + /// Returns the created trampoline [Procedure]. + Procedure _createFunctionTrampoline(Procedure node, FunctionType function, {required bool boxExternRef}) { // Create arguments for each positional parameter in the function. These // arguments will be JS objects. The generated wrapper will cast each @@ -156,7 +155,7 @@ class CallbackSpecializer { // returned from the supplied callback will be converted with `jsifyRaw` to // a native JS value before being returned to JS. final functionTrampolineName = _methodCollector.generateMethodName(); - _methodCollector.addInteropProcedure( + return _methodCollector.addInteropProcedure( functionTrampolineName, functionTrampolineName, FunctionNode(functionTrampolineBody, @@ -169,9 +168,8 @@ class CallbackSpecializer { returnType: _util.nullableWasmExternRefType) ..fileOffset = node.fileOffset, node.fileUri, - AnnotationType.export, + AnnotationType.weakExport, isExternal: false); - return functionTrampolineName; } /// Create a [Procedure] that will wrap a Dart callback in a JS wrapper. @@ -189,12 +187,14 @@ class CallbackSpecializer { /// function's arguments' length, the cast closure if needed, and the JS /// function's arguments as arguments. /// - /// Returns the created [Procedure]. - Procedure _getJSWrapperFunction(Procedure node, FunctionType type, + /// Returns the created JS wrapper [Procedure] which will call out to JS + /// and the trampoline [Procedure] which will be invoked by the JS code. + (Procedure, Procedure) _getJSWrapperFunction( + Procedure node, FunctionType type, {required bool boxExternRef, required bool needsCastClosure, required bool captureThis}) { - final functionTrampolineName = + final functionTrampoline = _createFunctionTrampoline(node, type, boxExternRef: boxExternRef); List jsParameters = []; var jsParametersLength = type.positionalParameters.length; @@ -220,7 +220,7 @@ class CallbackSpecializer { } // Create Dart procedure stub. - final jsMethodName = functionTrampolineName; + final jsMethodName = functionTrampoline.name.text; Procedure dartProcedure = _methodCollector.addInteropProcedure( '|$jsMethodName', 'dart2wasm.$jsMethodName', @@ -246,10 +246,10 @@ class CallbackSpecializer { dartProcedure, jsMethodName, "$jsMethodParams => finalizeWrapper(f, function($jsWrapperParams) {" - " return dartInstance.exports.$functionTrampolineName($dartArguments) " + " return dartInstance.exports.${functionTrampoline.name.text}($dartArguments) " "})"); - return dartProcedure; + return (dartProcedure, functionTrampoline); } /// Lowers an invocation of `allowInterop(foo)` to: @@ -272,7 +272,7 @@ class CallbackSpecializer { Expression allowInterop(StaticInvocation staticInvocation) { final argument = staticInvocation.arguments.positional.single; final type = argument.getStaticType(_staticTypeContext) as FunctionType; - final jsWrapperFunction = _getJSWrapperFunction( + final (jsWrapperFunction, exportedFunction) = _getJSWrapperFunction( staticInvocation.target, type, boxExternRef: false, needsCastClosure: false, captureThis: false); final v = VariableDeclaration('#var', @@ -287,12 +287,24 @@ class CallbackSpecializer { _util.wrapDartFunctionTarget, Arguments([ VariableGet(v), - StaticInvocation( - jsWrapperFunction, - Arguments([ - StaticInvocation(_util.jsObjectFromDartObjectTarget, - Arguments([VariableGet(v)])) - ])), + BlockExpression( + Block([ + // This ensures TFA will retain the function which the + // JS code will call. The backend in return will export + // the function due to `@pragma('wasm:weak-export', ...)` + ExpressionStatement(StaticInvocation( + _util.exportWasmFunctionTarget, + Arguments([ + ConstantExpression( + StaticTearOffConstant(exportedFunction)) + ]))) + ]), + StaticInvocation( + jsWrapperFunction, + Arguments([ + StaticInvocation(_util.jsObjectFromDartObjectTarget, + Arguments([VariableGet(v)])) + ]))), ], types: [ type ])), @@ -359,19 +371,30 @@ class CallbackSpecializer { final argument = staticInvocation.arguments.positional.single; final type = argument.getStaticType(_staticTypeContext) as FunctionType; final castClosure = _createCastClosure(type); - final jsWrapperFunction = _getJSWrapperFunction( + final (jsWrapperFunction, exportedFunction) = _getJSWrapperFunction( staticInvocation.target, type, boxExternRef: true, needsCastClosure: castClosure != null, captureThis: captureThis); - return _createJSValue(StaticInvocation( - jsWrapperFunction, - Arguments([ - StaticInvocation( - _util.jsObjectFromDartObjectTarget, Arguments([argument])), - if (castClosure != null) - StaticInvocation( - _util.jsObjectFromDartObjectTarget, Arguments([castClosure])) - ]))); + return _createJSValue(BlockExpression( + Block([ + // This ensures TFA will retain the function which the + // JS code will call. The backend in return will export + // the function due to `@pragma('wasm:weak-export', ...)` + ExpressionStatement(StaticInvocation( + _util.exportWasmFunctionTarget, + Arguments([ + ConstantExpression(StaticTearOffConstant(exportedFunction)) + ]))) + ]), + StaticInvocation( + jsWrapperFunction, + Arguments([ + StaticInvocation( + _util.jsObjectFromDartObjectTarget, Arguments([argument])), + if (castClosure != null) + StaticInvocation(_util.jsObjectFromDartObjectTarget, + Arguments([castClosure])) + ])))); } } diff --git a/pkg/dart2wasm/lib/js/util.dart b/pkg/dart2wasm/lib/js/util.dart index d2008c11c29..5e1d4f56686 100644 --- a/pkg/dart2wasm/lib/js/util.dart +++ b/pkg/dart2wasm/lib/js/util.dart @@ -7,7 +7,7 @@ import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart' import 'package:kernel/ast.dart'; import 'package:kernel/core_types.dart'; -enum AnnotationType { import, export } +enum AnnotationType { import, export, weakExport } /// A utility wrapper for [CoreTypes]. class CoreTypesUtil { @@ -30,6 +30,7 @@ class CoreTypesUtil { final Class wasmArrayClass; final Class wasmArrayRefClass; final Procedure wrapDartFunctionTarget; + final Procedure exportWasmFunctionTarget; CoreTypesUtil(this.coreTypes, this._extensionIndex) : allowInteropTarget = coreTypes.index @@ -70,7 +71,9 @@ class CoreTypesUtil { wasmArrayRefClass = coreTypes.index.getClass('dart:_wasm', 'WasmArrayRef'), wrapDartFunctionTarget = coreTypes.index - .getTopLevelProcedure('dart:_js_helper', '_wrapDartFunction'); + .getTopLevelProcedure('dart:_js_helper', '_wrapDartFunction'), + exportWasmFunctionTarget = coreTypes.index + .getTopLevelProcedure('dart:_internal', 'exportWasmFunction'); DartType get nonNullableObjectType => coreTypes.objectRawType(Nullability.nonNullable); @@ -96,7 +99,8 @@ class CoreTypesUtil { Procedure procedure, String pragmaOptionString, AnnotationType type) { String pragmaNameType = switch (type) { AnnotationType.import => 'import', - AnnotationType.export => 'export' + AnnotationType.export => 'export', + AnnotationType.weakExport => 'weak-export', }; procedure.addAnnotation(ConstantExpression( InstanceConstant(coreTypes.pragmaClass.reference, [], { diff --git a/pkg/dart2wasm/lib/kernel_nodes.dart b/pkg/dart2wasm/lib/kernel_nodes.dart index 72ced03892c..e87191812bf 100644 --- a/pkg/dart2wasm/lib/kernel_nodes.dart +++ b/pkg/dart2wasm/lib/kernel_nodes.dart @@ -39,6 +39,8 @@ mixin KernelNodes { index.getClass("dart:_boxed_int", "BoxedInt"); late final Class closureClass = index.getClass("dart:core", "_Closure"); late final Class listBaseClass = index.getClass("dart:_list", "WasmListBase"); + late final Procedure listBaseIndexOperator = + index.getProcedure("dart:_list", "WasmListBase", "[]"); late final Class fixedLengthListClass = index.getClass("dart:_list", "ModifiableFixedLengthList"); late final Class growableListClass = diff --git a/sdk/lib/_internal/wasm/lib/async_patch.dart b/sdk/lib/_internal/wasm/lib/async_patch.dart index c2d77ea9890..ef313408e45 100644 --- a/sdk/lib/_internal/wasm/lib/async_patch.dart +++ b/sdk/lib/_internal/wasm/lib/async_patch.dart @@ -1,4 +1,4 @@ -import 'dart:_internal' show _AsyncCompleter, patch; +import 'dart:_internal' show _AsyncCompleter, patch, exportWasmFunction; import 'dart:_js_helper' show JS; diff --git a/sdk/lib/_internal/wasm/lib/internal_patch.dart b/sdk/lib/_internal/wasm/lib/internal_patch.dart index 458e49675ba..adb2c9080d0 100644 --- a/sdk/lib/_internal/wasm/lib/internal_patch.dart +++ b/sdk/lib/_internal/wasm/lib/internal_patch.dart @@ -89,6 +89,11 @@ external T unsafeCastOpaque(Object? v); // This function can be used to keep an object alive till that point. void reachabilityFence(Object? object) {} +// Used for exporting wasm functions that are annotated via +// `@pragma('wasm:weak-export', '') +@pragma("wasm:intrinsic") +external void exportWasmFunction(Function object); + // This function can be used to encode native side effects. @pragma("wasm:intrinsic") external void _nativeEffect(Object object); @@ -115,24 +120,6 @@ external int doubleToIntBits(double value); @pragma("wasm:intrinsic") external double intBitsToDouble(int value); -/// Used to invoke a Dart closure from JS (for microtasks and other callbacks), -/// printing any exceptions that escape. -@pragma("wasm:export", "\$invokeCallback") -void _invokeCallback(void Function() callback) { - try { - callback(); - } catch (e, s) { - print(e); - print(s); - // FIXME: Chrome/V8 bug makes errors from `rethrow`s not being reported to - // `window.onerror`. Please change this back to `rethrow` once the chrome - // bug is fixed. - // - // https://g-issues.chromium.org/issues/327155548 - throw e; - } -} - // Will be patched in `pkg/dart2wasm/lib/compile.dart` right before TFA. external Function get mainTearOff; diff --git a/sdk/lib/_internal/wasm/lib/js_helper_patch.dart b/sdk/lib/_internal/wasm/lib/js_helper_patch.dart index 9952d52cb5b..bfd124e2ace 100644 --- a/sdk/lib/_internal/wasm/lib/js_helper_patch.dart +++ b/sdk/lib/_internal/wasm/lib/js_helper_patch.dart @@ -2,7 +2,8 @@ // 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:_internal' show patch, unsafeCast, unsafeCastOpaque; +import 'dart:_internal' + show patch, unsafeCast, unsafeCastOpaque, exportWasmFunction; import 'dart:_js_helper' show JS; import 'dart:_js_types' show JSArrayBase, JSDataViewImpl; import 'dart:js_interop'; @@ -31,6 +32,10 @@ void _copyFromWasmI8Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmI8ArrayGet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmI8ArrayGet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const getValue = dartInstance.exports.\$wasmI8ArrayGet; @@ -54,6 +59,10 @@ void copyToWasmI8Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmI8ArraySet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmI8ArraySet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const setValue = dartInstance.exports.\$wasmI8ArraySet; @@ -123,6 +132,10 @@ void _copyFromWasmI32Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmI32ArrayGet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmI32ArrayGet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const getValue = dartInstance.exports.\$wasmI32ArrayGet; @@ -146,6 +159,10 @@ void copyToWasmI32Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmI32ArraySet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmI32ArraySet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const setValue = dartInstance.exports.\$wasmI32ArraySet; @@ -169,6 +186,10 @@ void _copyFromWasmF32Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmF32ArrayGet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmF32ArrayGet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const getValue = dartInstance.exports.\$wasmF32ArrayGet; @@ -192,6 +213,10 @@ void copyToWasmF32Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmF32ArraySet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmF32ArraySet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const setValue = dartInstance.exports.\$wasmF32ArraySet; @@ -215,6 +240,10 @@ void _copyFromWasmF64Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmF64ArrayGet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmF64ArrayGet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const getValue = dartInstance.exports.\$wasmF64ArrayGet; @@ -238,6 +267,10 @@ void copyToWasmF64Array( int wasmOffset, int length, ) { + // This will make TFA retain [_wasmF64ArraySet] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_wasmF64ArraySet); + JS( """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { const setValue = dartInstance.exports.\$wasmF64ArraySet; @@ -421,7 +454,7 @@ WasmExternRef jsDataViewFromDartByteData(ByteData l, int length) { return (JSDataView(jsArrayBuffer, 0, length) as JSValue).toExternRef!; } -@pragma("wasm:export", "\$wasmI8ArrayGet") +@pragma("wasm:weak-export", "\$wasmI8ArrayGet") WasmI32 _wasmI8ArrayGet(WasmExternRef? ref, WasmI32 index) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), @@ -429,7 +462,7 @@ WasmI32 _wasmI8ArrayGet(WasmExternRef? ref, WasmI32 index) { return array.readUnsigned(index.toIntUnsigned()).toWasmI32(); } -@pragma("wasm:export", "\$wasmI8ArraySet") +@pragma("wasm:weak-export", "\$wasmI8ArraySet") void _wasmI8ArraySet(WasmExternRef? ref, WasmI32 index, WasmI32 value) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), @@ -453,7 +486,7 @@ void _wasmI16ArraySet(WasmExternRef? ref, WasmI32 index, WasmI32 value) { array.write(index.toIntUnsigned(), value.toIntUnsigned()); } -@pragma("wasm:export", "\$wasmI32ArrayGet") +@pragma("wasm:weak-export", "\$wasmI32ArrayGet") WasmI32 _wasmI32ArrayGet(WasmExternRef? ref, WasmI32 index) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), @@ -461,7 +494,7 @@ WasmI32 _wasmI32ArrayGet(WasmExternRef? ref, WasmI32 index) { return array.readUnsigned(index.toIntUnsigned()).toWasmI32(); } -@pragma("wasm:export", "\$wasmI32ArraySet") +@pragma("wasm:weak-export", "\$wasmI32ArraySet") void _wasmI32ArraySet(WasmExternRef? ref, WasmI32 index, WasmI32 value) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), @@ -469,7 +502,7 @@ void _wasmI32ArraySet(WasmExternRef? ref, WasmI32 index, WasmI32 value) { array.write(index.toIntUnsigned(), value.toIntUnsigned()); } -@pragma("wasm:export", "\$wasmF32ArrayGet") +@pragma("wasm:weak-export", "\$wasmF32ArrayGet") WasmF32 _wasmF32ArrayGet(WasmExternRef? ref, WasmI32 index) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), @@ -477,7 +510,7 @@ WasmF32 _wasmF32ArrayGet(WasmExternRef? ref, WasmI32 index) { return array[index.toIntUnsigned()]; } -@pragma("wasm:export", "\$wasmF32ArraySet") +@pragma("wasm:weak-export", "\$wasmF32ArraySet") void _wasmF32ArraySet(WasmExternRef? ref, WasmI32 index, WasmF32 value) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), @@ -485,7 +518,7 @@ void _wasmF32ArraySet(WasmExternRef? ref, WasmI32 index, WasmF32 value) { array[index.toIntUnsigned()] = value; } -@pragma("wasm:export", "\$wasmF64ArrayGet") +@pragma("wasm:weak-export", "\$wasmF64ArrayGet") WasmF64 _wasmF64ArrayGet(WasmExternRef? ref, WasmI32 index) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), @@ -493,7 +526,7 @@ WasmF64 _wasmF64ArrayGet(WasmExternRef? ref, WasmI32 index) { return array[index.toIntUnsigned()]; } -@pragma("wasm:export", "\$wasmF64ArraySet") +@pragma("wasm:weak-export", "\$wasmF64ArraySet") void _wasmF64ArraySet(WasmExternRef? ref, WasmI32 index, WasmF64 value) { final array = unsafeCastOpaque>( unsafeCast(ref).internalize(), diff --git a/sdk/lib/_internal/wasm/lib/timer_patch.dart b/sdk/lib/_internal/wasm/lib/timer_patch.dart index b63dd9416b4..1f53d76901d 100644 --- a/sdk/lib/_internal/wasm/lib/timer_patch.dart +++ b/sdk/lib/_internal/wasm/lib/timer_patch.dart @@ -10,43 +10,77 @@ part of "async_patch.dart"; /// definitions to users. class _JSEventLoop { /// Schedule a callback from JS via `setTimeout`. - static int _setTimeout(double ms, dynamic Function() callback) => - JS( - r"""(ms, c) => + static int _setTimeout(double ms, dynamic Function() callback) { + // This will make TFA retain [_invokeCallback] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_invokeCallback); + + return JS( + r"""(ms, c) => setTimeout(() => dartInstance.exports.$invokeCallback(c),ms)""", - ms, - callback, - ).toInt(); + ms, + callback, + ).toInt(); + } /// Cancel a callback scheduled with `setTimeout`. static void _clearTimeout(int handle) => JS(r"""(handle) => clearTimeout(handle)""", handle.toDouble()); /// Schedule a periodic callback from JS via `setInterval`. - static int _setInterval(double ms, dynamic Function() callback) => - JS( - r"""(ms, c) => + static int _setInterval(double ms, dynamic Function() callback) { + // This will make TFA retain [_invokeCallback] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_invokeCallback); + + return JS( + r"""(ms, c) => setInterval(() => dartInstance.exports.$invokeCallback(c), ms)""", - ms, - callback, - ).toInt(); + ms, + callback, + ).toInt(); + } /// Cancel a callback scheduled with `setInterval`. static void _clearInterval(int handle) => JS(r"""(handle) => clearInterval(handle)""", handle.toDouble()); /// Schedule a callback from JS via `queueMicrotask`. - static void _queueMicrotask(dynamic Function() callback) => JS( - r"""(c) => + static void _queueMicrotask(dynamic Function() callback) { + // This will make TFA retain [_invokeCallback] which will then cause the + // backend to export it to JS (due to `@pragma('wasm:weak-export', ...)`) + exportWasmFunction(_invokeCallback); + + return JS( + r"""(c) => queueMicrotask(() => dartInstance.exports.$invokeCallback(c))""", - callback, - ); + callback, + ); + } /// JS `Date.now()`, returns the number of milliseconds elapsed since the /// epoch. static int _dateNow() => JS('() => Date.now()').toInt(); } +/// Used to invoke a Dart closure from JS (for microtasks and other callbacks), +/// printing any exceptions that escape. +@pragma("wasm:weak-export", "\$invokeCallback") +void _invokeCallback(void Function() callback) { + try { + callback(); + } catch (e, s) { + print(e); + print(s); + // FIXME: Chrome/V8 bug makes errors from `rethrow`s not being reported to + // `window.onerror`. Please change this back to `rethrow` once the chrome + // bug is fixed. + // + // https://g-issues.chromium.org/issues/327155548 + throw e; + } +} + @patch class Timer { @patch