[dart2wasm] Add support for `@pragma('wasm:weak-export', '<name>')`

We explicitly & unconditionally export functions to JS via the
`@pragma('wasm:export', '<name>')` 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', '<name')`. We then also have to compile everything reachable from `A*`.

This is the case for a few functions in the core libraries but even more
pronounced in code that the modular JS interop transformer generates for
callbacks: It generates `|_<num>` functions that call out to JS which
call back into a dart-exported `_<num>` 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', '<name>')`

    => 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 `<name>` 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 <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index 3c5087d..bbb925c 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -1700,6 +1700,18 @@
     }
 
     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 bb643fc..5826201 100644
--- a/pkg/dart2wasm/lib/functions.dart
+++ b/pkg/dart2wasm/lib/functions.dart
@@ -23,8 +23,6 @@
   final Map<Reference, w.BaseFunction> _functions = {};
   // Wasm function for each function expression and local function.
   final Map<Lambda, w.BaseFunction> _lambdas = {};
-  // Names of exported functions
-  final Map<Reference, String> _exports = {};
   // Selector IDs that are invoked via GDT.
   final Set<int> _calledSelectors = {};
   final Set<int> _calledUncheckedSelectors = {};
@@ -42,14 +40,13 @@
   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 @@
         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 @@
       }
     }
 
-    // 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 @@
 
   w.BaseFunction getFunction(Reference target) {
     return _functions.putIfAbsent(target, () {
+      final member = target.asMember;
+
+      // If this function is a `@pragma('wasm:import', '<module>:<name>')` 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', '<name>')` or
+      //   * `@pragma('wasm:weak-export', '<name>')`
+      // we export it under the given `<name>`
+      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 ca9512c..d51273d 100644
--- a/pkg/dart2wasm/lib/intrinsics.dart
+++ b/pkg/dart2wasm/lib/intrinsics.dart
@@ -164,6 +164,7 @@
   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 @@
             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 838188f..ca7f226 100644
--- a/pkg/dart2wasm/lib/js/callback_specializer.dart
+++ b/pkg/dart2wasm/lib/js/callback_specializer.dart
@@ -63,9 +63,8 @@
   /// 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 @@
     // 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 @@
             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 @@
   /// 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<String> jsParameters = [];
     var jsParametersLength = type.positionalParameters.length;
@@ -220,7 +220,7 @@
     }
 
     // Create Dart procedure stub.
-    final jsMethodName = functionTrampolineName;
+    final jsMethodName = functionTrampoline.name.text;
     Procedure dartProcedure = _methodCollector.addInteropProcedure(
         '|$jsMethodName',
         'dart2wasm.$jsMethodName',
@@ -246,10 +246,10 @@
         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<type>(foo)` to:
@@ -272,7 +272,7 @@
   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 @@
                 _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 @@
     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 d2008c1..5e1d4f5 100644
--- a/pkg/dart2wasm/lib/js/util.dart
+++ b/pkg/dart2wasm/lib/js/util.dart
@@ -7,7 +7,7 @@
 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 @@
   final Class wasmArrayClass;
   final Class wasmArrayRefClass;
   final Procedure wrapDartFunctionTarget;
+  final Procedure exportWasmFunctionTarget;
 
   CoreTypesUtil(this.coreTypes, this._extensionIndex)
       : allowInteropTarget = coreTypes.index
@@ -70,7 +71,9 @@
         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 @@
       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 72ced03..e871918 100644
--- a/pkg/dart2wasm/lib/kernel_nodes.dart
+++ b/pkg/dart2wasm/lib/kernel_nodes.dart
@@ -39,6 +39,8 @@
       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 c2d77ea..ef31340 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 458e496..adb2c90 100644
--- a/sdk/lib/_internal/wasm/lib/internal_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/internal_patch.dart
@@ -89,6 +89,11 @@
 // 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', '<name>')
+@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 @@
 @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 9952d52..bfd124e 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 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const getValue = dartInstance.exports.\$wasmI8ArrayGet;
@@ -54,6 +59,10 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const setValue = dartInstance.exports.\$wasmI8ArraySet;
@@ -123,6 +132,10 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const getValue = dartInstance.exports.\$wasmI32ArrayGet;
@@ -146,6 +159,10 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const setValue = dartInstance.exports.\$wasmI32ArraySet;
@@ -169,6 +186,10 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const getValue = dartInstance.exports.\$wasmF32ArrayGet;
@@ -192,6 +213,10 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const setValue = dartInstance.exports.\$wasmF32ArraySet;
@@ -215,6 +240,10 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const getValue = dartInstance.exports.\$wasmF64ArrayGet;
@@ -238,6 +267,10 @@
   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<WasmExternRef?>(
     """(jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => {
           const setValue = dartInstance.exports.\$wasmF64ArraySet;
@@ -421,7 +454,7 @@
   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<WasmArray<WasmI8>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
@@ -429,7 +462,7 @@
   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<WasmArray<WasmI8>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
@@ -453,7 +486,7 @@
   array.write(index.toIntUnsigned(), value.toIntUnsigned());
 }
 
-@pragma("wasm:export", "\$wasmI32ArrayGet")
+@pragma("wasm:weak-export", "\$wasmI32ArrayGet")
 WasmI32 _wasmI32ArrayGet(WasmExternRef? ref, WasmI32 index) {
   final array = unsafeCastOpaque<WasmArray<WasmI32>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
@@ -461,7 +494,7 @@
   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<WasmArray<WasmI32>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
@@ -469,7 +502,7 @@
   array.write(index.toIntUnsigned(), value.toIntUnsigned());
 }
 
-@pragma("wasm:export", "\$wasmF32ArrayGet")
+@pragma("wasm:weak-export", "\$wasmF32ArrayGet")
 WasmF32 _wasmF32ArrayGet(WasmExternRef? ref, WasmI32 index) {
   final array = unsafeCastOpaque<WasmArray<WasmF32>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
@@ -477,7 +510,7 @@
   return array[index.toIntUnsigned()];
 }
 
-@pragma("wasm:export", "\$wasmF32ArraySet")
+@pragma("wasm:weak-export", "\$wasmF32ArraySet")
 void _wasmF32ArraySet(WasmExternRef? ref, WasmI32 index, WasmF32 value) {
   final array = unsafeCastOpaque<WasmArray<WasmF32>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
@@ -485,7 +518,7 @@
   array[index.toIntUnsigned()] = value;
 }
 
-@pragma("wasm:export", "\$wasmF64ArrayGet")
+@pragma("wasm:weak-export", "\$wasmF64ArrayGet")
 WasmF64 _wasmF64ArrayGet(WasmExternRef? ref, WasmI32 index) {
   final array = unsafeCastOpaque<WasmArray<WasmF64>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
@@ -493,7 +526,7 @@
   return array[index.toIntUnsigned()];
 }
 
-@pragma("wasm:export", "\$wasmF64ArraySet")
+@pragma("wasm:weak-export", "\$wasmF64ArraySet")
 void _wasmF64ArraySet(WasmExternRef? ref, WasmI32 index, WasmF64 value) {
   final array = unsafeCastOpaque<WasmArray<WasmF64>>(
     unsafeCast<WasmExternRef>(ref).internalize(),
diff --git a/sdk/lib/_internal/wasm/lib/timer_patch.dart b/sdk/lib/_internal/wasm/lib/timer_patch.dart
index b63dd94..1f53d76 100644
--- a/sdk/lib/_internal/wasm/lib/timer_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/timer_patch.dart
@@ -10,43 +10,77 @@
 /// definitions to users.
 class _JSEventLoop {
   /// Schedule a callback from JS via `setTimeout`.
-  static int _setTimeout(double ms, dynamic Function() callback) =>
-      JS<double>(
-        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<double>(
+      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<void>(r"""(handle) => clearTimeout(handle)""", handle.toDouble());
 
   /// Schedule a periodic callback from JS via `setInterval`.
-  static int _setInterval(double ms, dynamic Function() callback) =>
-      JS<double>(
-        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<double>(
+      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<void>(r"""(handle) => clearInterval(handle)""", handle.toDouble());
 
   /// Schedule a callback from JS via `queueMicrotask`.
-  static void _queueMicrotask(dynamic Function() callback) => JS<void>(
-    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<void>(
+      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<double>('() => 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