[dart2wasm] Add Wasm function references and more Wasm ref conversions

Having first-class Wasm function references makes it possible to call
JS function objects directly from Dart, and to call some Dart functions
(static functions with no optional parameters and no type parameters)
from JS as function objects.

Change-Id: I1c788338d418c8857493ec76560d74fdd17d5dd2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/241001
Reviewed-by: Joshua Litt <joshualitt@google.com>
Reviewed-by: Jackson Gardner <jacksongardner@google.com>
Commit-Queue: Aske Simon Christensen <askesc@google.com>
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index f086339..d12efbd 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -388,6 +388,17 @@
     });
   }
 
+  /// Helper function to throw a Wasm ref downcast error.
+  void throwWasmRefError(String expected) {
+    wrap(
+        StringLiteral(expected),
+        translator
+            .translateType(translator.coreTypes.stringNonNullableRawType));
+    _call(translator.stackTraceCurrent.reference);
+    _call(translator.throwWasmRefError.reference);
+    b.unreachable();
+  }
+
   /// Generates code for an expression plus conversion code to convert the
   /// result to the expected type if needed. All expression code generation goes
   /// through this method.
@@ -1660,9 +1671,28 @@
   @override
   w.ValueType visitFunctionInvocation(
       FunctionInvocation node, w.ValueType expectedType) {
+    Expression receiver = node.receiver;
+    if (receiver is InstanceGet &&
+        receiver.interfaceTarget == translator.wasmFunctionCall) {
+      // Receiver is a WasmFunction
+      assert(receiver.name.text == "call");
+      w.RefType receiverType =
+          translator.translateType(dartTypeOf(receiver.receiver)) as w.RefType;
+      w.Local temp = addLocal(receiverType);
+      wrap(receiver.receiver, receiverType);
+      b.local_set(temp);
+      w.FunctionType functionType = receiverType.heapType as w.FunctionType;
+      assert(node.arguments.positional.length == functionType.inputs.length);
+      for (int i = 0; i < node.arguments.positional.length; i++) {
+        wrap(node.arguments.positional[i], functionType.inputs[i]);
+      }
+      b.local_get(temp);
+      b.call_ref();
+      return translator.outputOrVoid(functionType.outputs);
+    }
     int parameterCount = node.functionType?.requiredParameterCount ??
         node.arguments.positional.length;
-    return _functionCall(parameterCount, node.receiver, node.arguments);
+    return _functionCall(parameterCount, receiver, node.arguments);
   }
 
   w.ValueType _functionCall(
diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart
index 0fa9329..7fb0cf5 100644
--- a/pkg/dart2wasm/lib/intrinsics.dart
+++ b/pkg/dart2wasm/lib/intrinsics.dart
@@ -118,28 +118,45 @@
   Intrinsifier(this.codeGen);
 
   w.ValueType? generateInstanceGetterIntrinsic(InstanceGet node) {
-    DartType receiverType = dartTypeOf(node.receiver);
+    Expression receiver = node.receiver;
+    DartType receiverType = dartTypeOf(receiver);
     String name = node.name.text;
+    Member target = node.interfaceTarget;
+    Class cls = target.enclosingClass!;
+
+    // WasmAnyRef.isObject
+    if (cls == translator.wasmAnyRefClass) {
+      assert(name == "isObject");
+      w.Label succeed = b.block(const [], const [w.NumType.i32]);
+      w.Label fail = b.block(const [], const [w.RefType.any(nullable: false)]);
+      codeGen.wrap(receiver, w.RefType.any(nullable: false));
+      b.br_on_non_data(fail);
+      translator.ref_test(b, translator.topInfo);
+      b.br(succeed);
+      b.end(); // fail
+      b.drop();
+      b.i32_const(0);
+      b.end(); // succeed
+      return w.NumType.i32;
+    }
 
     // _WasmArray.length
-    if (node.interfaceTarget.enclosingClass == translator.wasmArrayBaseClass) {
+    if (cls == translator.wasmArrayBaseClass) {
       assert(name == 'length');
       DartType elementType =
           (receiverType as InterfaceType).typeArguments.single;
       w.ArrayType arrayType = translator.arrayTypeForDartType(elementType);
-      Expression array = node.receiver;
-      codeGen.wrap(array, w.RefType.def(arrayType, nullable: true));
+      codeGen.wrap(receiver, w.RefType.def(arrayType, nullable: true));
       b.array_len(arrayType);
       b.i64_extend_i32_u();
       return w.NumType.i64;
     }
 
     // int.bitlength
-    if (node.interfaceTarget.enclosingClass == translator.coreTypes.intClass &&
-        name == 'bitLength') {
+    if (cls == translator.coreTypes.intClass && name == 'bitLength') {
       w.Local temp = codeGen.function.addLocal(w.NumType.i64);
       b.i64_const(64);
-      codeGen.wrap(node.receiver, w.NumType.i64);
+      codeGen.wrap(receiver, w.NumType.i64);
       b.local_tee(temp);
       b.local_get(temp);
       b.i64_const(63);
@@ -151,28 +168,26 @@
     }
 
     // _HashAbstractImmutableBase._indexNullable
-    if (node.interfaceTarget == translator.immutableMapIndexNullable) {
+    if (target == translator.immutableMapIndexNullable) {
       ClassInfo info = translator.classInfo[translator.hashFieldBaseClass]!;
-      codeGen.wrap(node.receiver, info.nullableType);
+      codeGen.wrap(receiver, info.nullableType);
       b.struct_get(info.struct, FieldIndex.hashBaseIndex);
       return info.struct.fields[FieldIndex.hashBaseIndex].type.unpacked;
     }
 
     // _Compound._typedDataBase
-    if (node.interfaceTarget.enclosingClass == translator.ffiCompoundClass &&
-        name == '_typedDataBase') {
+    if (cls == translator.ffiCompoundClass && name == '_typedDataBase') {
       // A compound (subclass of Struct or Union) is represented by its i32
       // address. The _typedDataBase field contains a Pointer pointing to the
       // compound, whose representation is the same.
-      codeGen.wrap(node.receiver, w.NumType.i32);
+      codeGen.wrap(receiver, w.NumType.i32);
       return w.NumType.i32;
     }
 
     // Pointer.address
-    if (node.interfaceTarget.enclosingClass == translator.ffiPointerClass &&
-        name == 'address') {
+    if (cls == translator.ffiPointerClass && name == 'address') {
       // A Pointer is represented by its i32 address.
-      codeGen.wrap(node.receiver, w.NumType.i32);
+      codeGen.wrap(receiver, w.NumType.i32);
       b.i64_extend_i32_u();
       return w.NumType.i64;
     }
@@ -185,17 +200,17 @@
     DartType receiverType = dartTypeOf(receiver);
     String name = node.name.text;
     Procedure target = node.interfaceTarget;
+    Class cls = target.enclosingClass!;
 
     // _TypedListBase._setRange
-    if (target.enclosingClass == translator.typedListBaseClass &&
-        name == "_setRange") {
+    if (cls == translator.typedListBaseClass && name == "_setRange") {
       // Always fall back to alternative implementation.
       b.i32_const(0);
       return w.NumType.i32;
     }
 
     // _TypedList._(get|set)(Int|Uint|Float)(8|16|32|64)
-    if (node.interfaceTarget.enclosingClass == translator.typedListClass) {
+    if (cls == translator.typedListClass) {
       Match? match = RegExp("^_(get|set)(Int|Uint|Float)(8|16|32|64)\$")
           .matchAsPrefix(name);
       if (match != null) {
@@ -319,11 +334,24 @@
       }
     }
 
+    // WasmAnyRef.toObject
+    if (cls == translator.wasmAnyRefClass) {
+      assert(name == "toObject");
+      w.Label succeed = b.block(const [], [translator.topInfo.nonNullableType]);
+      w.Label fail = b.block(const [], const [w.RefType.any(nullable: false)]);
+      codeGen.wrap(receiver, w.RefType.any(nullable: false));
+      b.br_on_non_data(fail);
+      translator.br_on_cast(b, succeed, translator.topInfo);
+      b.end(); // fail
+      codeGen.throwWasmRefError("a Dart object");
+      b.end(); // succeed
+      return translator.topInfo.nonNullableType;
+    }
+
     // WasmIntArray.(readSigned|readUnsigned|write)
     // WasmFloatArray.(read|write)
     // WasmObjectArray.(read|write)
-    if (node.interfaceTarget.enclosingClass?.superclass ==
-        translator.wasmArrayBaseClass) {
+    if (cls.superclass == translator.wasmArrayBaseClass) {
       DartType elementType =
           (receiverType as InterfaceType).typeArguments.single;
       w.ArrayType arrayType = translator.arrayTypeForDartType(elementType);
@@ -388,10 +416,8 @@
     }
 
     // Wasm(I32|I64|F32|F64) conversions
-    if (node.interfaceTarget.enclosingClass?.superclass?.superclass ==
-        translator.wasmTypesBaseClass) {
-      w.StorageType receiverType =
-          translator.builtinTypes[node.interfaceTarget.enclosingClass]!;
+    if (cls.superclass?.superclass == translator.wasmTypesBaseClass) {
+      w.StorageType receiverType = translator.builtinTypes[cls]!;
       switch (receiverType) {
         case w.NumType.i32:
           assert(name == "toIntSigned" || name == "toIntUnsigned");
@@ -557,6 +583,7 @@
 
   w.ValueType? generateStaticIntrinsic(StaticInvocation node) {
     String name = node.name.text;
+    Class? cls = node.target.enclosingClass;
 
     // dart:core static functions
     if (node.target.enclosingLibrary == translator.coreTypes.coreLibrary) {
@@ -710,7 +737,7 @@
           b.f64_reinterpret_i64();
           return w.NumType.f64;
         case "getID":
-          assert(node.target.enclosingClass?.name == "ClassID");
+          assert(cls?.name == "ClassID");
           ClassInfo info = translator.topInfo;
           codeGen.wrap(node.arguments.positional.single, info.nullableType);
           b.struct_get(info.struct, FieldIndex.classId);
@@ -836,24 +863,58 @@
       }
     }
 
-    // Wasm(Int|Float|Object)Array constructors
-    if (node.target.enclosingClass?.superclass ==
-        translator.wasmArrayBaseClass) {
-      Expression length = node.arguments.positional[0];
-      w.ArrayType arrayType =
-          translator.arrayTypeForDartType(node.arguments.types.single);
-      codeGen.wrap(length, w.NumType.i64);
-      b.i32_wrap_i64();
-      translator.array_new_default(b, arrayType);
-      return w.RefType.def(arrayType, nullable: false);
-    }
+    if (cls != null && translator.isWasmType(cls)) {
+      // Wasm(Int|Float|Object)Array constructors
+      if (cls.superclass == translator.wasmArrayBaseClass) {
+        Expression length = node.arguments.positional[0];
+        w.ArrayType arrayType =
+            translator.arrayTypeForDartType(node.arguments.types.single);
+        codeGen.wrap(length, w.NumType.i64);
+        b.i32_wrap_i64();
+        translator.array_new_default(b, arrayType);
+        return w.RefType.def(arrayType, nullable: false);
+      }
 
-    // Wasm(I32|I64|F32|F64) constructors
-    if (node.target.enclosingClass?.superclass?.superclass ==
-        translator.wasmTypesBaseClass) {
+      // (WasmFuncRef|WasmFunction).fromRef constructors
+      if ((cls == translator.wasmFuncRefClass ||
+              cls == translator.wasmFunctionClass) &&
+          name == "fromRef") {
+        Expression ref = node.arguments.positional[0];
+        w.RefType resultType = typeOfExp(node) as w.RefType;
+        w.Label succeed = b.block(const [], [resultType]);
+        w.Label fail =
+            b.block(const [], const [w.RefType.any(nullable: false)]);
+        codeGen.wrap(ref, w.RefType.any(nullable: false));
+        b.br_on_non_func(fail);
+        if (cls == translator.wasmFunctionClass) {
+          assert(resultType.heapType is w.FunctionType);
+          translator.br_on_cast_fail(b, fail, resultType.heapType);
+        }
+        b.br(succeed);
+        b.end(); // fail
+        codeGen.throwWasmRefError("a function with the expected signature");
+        b.end(); // succeed
+        return resultType;
+      }
+
+      // WasmFunction.fromFunction constructor
+      if (cls == translator.wasmFunctionClass) {
+        assert(name == "fromFunction");
+        Expression f = node.arguments.positional[0];
+        if (f is! ConstantExpression || f.constant is! StaticTearOffConstant) {
+          throw "Argument to WasmFunction.fromFunction isn't a static function";
+        }
+        StaticTearOffConstant func = f.constant as StaticTearOffConstant;
+        w.BaseFunction wasmFunction =
+            translator.functions.getFunction(func.targetReference);
+        w.Global functionRef = translator.makeFunctionRef(wasmFunction);
+        b.global_get(functionRef);
+        return functionRef.type.type;
+      }
+
+      // Wasm(AnyRef|FuncRef|EqRef|DataRef|I32|I64|F32|F64) constructors
       Expression value = node.arguments.positional[0];
-      w.StorageType targetType =
-          translator.builtinTypes[node.target.enclosingClass]!;
+      w.StorageType targetType = translator.builtinTypes[cls]!;
       switch (targetType) {
         case w.NumType.i32:
           codeGen.wrap(value, w.NumType.i64);
@@ -869,6 +930,10 @@
         case w.NumType.f64:
           codeGen.wrap(value, w.NumType.f64);
           return w.NumType.f64;
+        default:
+          w.RefType valueType = targetType as w.RefType;
+          codeGen.wrap(value, valueType);
+          return valueType;
       }
     }
 
diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart
index 1eacf81..2a61d3c 100644
--- a/pkg/dart2wasm/lib/translator.dart
+++ b/pkg/dart2wasm/lib/translator.dart
@@ -67,8 +67,10 @@
   late final Class wasmTypesBaseClass;
   late final Class wasmArrayBaseClass;
   late final Class wasmAnyRefClass;
+  late final Class wasmFuncRefClass;
   late final Class wasmEqRefClass;
   late final Class wasmDataRefClass;
+  late final Class wasmFunctionClass;
   late final Class boxedBoolClass;
   late final Class boxedIntClass;
   late final Class boxedDoubleClass;
@@ -91,11 +93,13 @@
   late final Class typedListViewClass;
   late final Class byteDataViewClass;
   late final Class typeErrorClass;
+  late final Procedure wasmFunctionCall;
   late final Procedure stackTraceCurrent;
   late final Procedure stringEquals;
   late final Procedure stringInterpolate;
   late final Procedure throwNullCheckError;
   late final Procedure throwAsCheckError;
+  late final Procedure throwWasmRefError;
   late final Procedure mapFactory;
   late final Procedure mapPut;
   late final Procedure immutableMapIndexNullable;
@@ -130,7 +134,7 @@
   final Map<int, w.StructType> functionTypeCache = {};
   final Map<w.StructType, int> functionTypeParameterCount = {};
   final Map<int, w.DefinedGlobal> functionTypeRtt = {};
-  final Map<w.DefinedFunction, w.DefinedGlobal> functionRefCache = {};
+  final Map<w.BaseFunction, w.DefinedGlobal> functionRefCache = {};
   final Map<Procedure, w.DefinedFunction> tearOffFunctionCache = {};
 
   ClassInfo get topInfo => classes[0];
@@ -161,8 +165,10 @@
     wasmTypesBaseClass = lookupWasm("_WasmBase");
     wasmArrayBaseClass = lookupWasm("_WasmArray");
     wasmAnyRefClass = lookupWasm("WasmAnyRef");
+    wasmFuncRefClass = lookupWasm("WasmFuncRef");
     wasmEqRefClass = lookupWasm("WasmEqRef");
     wasmDataRefClass = lookupWasm("WasmDataRef");
+    wasmFunctionClass = lookupWasm("WasmFunction");
     boxedBoolClass = lookupCore("_BoxedBool");
     boxedIntClass = lookupCore("_BoxedInt");
     boxedDoubleClass = lookupCore("_BoxedDouble");
@@ -185,6 +191,8 @@
     typedListClass = lookupTypedData("_TypedList");
     typedListViewClass = lookupTypedData("_TypedListView");
     byteDataViewClass = lookupTypedData("_ByteDataView");
+    wasmFunctionCall =
+        wasmFunctionClass.procedures.firstWhere((p) => p.name.text == "call");
     stackTraceCurrent =
         stackTraceClass.procedures.firstWhere((p) => p.name.text == "current");
     stringEquals =
@@ -195,6 +203,8 @@
         .firstWhere((p) => p.name.text == "_throwNullCheckError");
     throwAsCheckError = typeErrorClass.procedures
         .firstWhere((p) => p.name.text == "_throwAsCheckError");
+    throwWasmRefError = typeErrorClass.procedures
+        .firstWhere((p) => p.name.text == "_throwWasmRefError");
     mapFactory = lookupCollection("LinkedHashMap").procedures.firstWhere(
         (p) => p.kind == ProcedureKind.Factory && p.name.text == "_default");
     mapPut = lookupCollection("_CompactLinkedCustomHashMap")
@@ -208,9 +218,10 @@
       coreTypes.boolClass: w.NumType.i32,
       coreTypes.intClass: w.NumType.i64,
       coreTypes.doubleClass: w.NumType.f64,
-      wasmAnyRefClass: w.RefType.any(nullable: false),
-      wasmEqRefClass: w.RefType.eq(nullable: false),
-      wasmDataRefClass: w.RefType.data(nullable: false),
+      wasmAnyRefClass: const w.RefType.any(nullable: false),
+      wasmFuncRefClass: const w.RefType.func(nullable: false),
+      wasmEqRefClass: const w.RefType.eq(nullable: false),
+      wasmDataRefClass: const w.RefType.data(nullable: false),
       boxedBoolClass: w.NumType.i32,
       boxedIntClass: w.NumType.i64,
       boxedDoubleClass: w.NumType.f64,
@@ -421,6 +432,28 @@
         return w.RefType.def(arrayTypeForDartType(elementType),
             nullable: false);
       }
+      if (type.classNode == wasmFunctionClass) {
+        DartType functionType = type.typeArguments.single;
+        if (functionType is! FunctionType) {
+          throw "The type argument of a WasmFunction must be a function type";
+        }
+        if (functionType.typeParameters.isNotEmpty ||
+            functionType.namedParameters.isNotEmpty ||
+            functionType.requiredParameterCount !=
+                functionType.positionalParameters.length) {
+          throw "A WasmFunction can't have optional/type parameters";
+        }
+        List<w.ValueType> inputs = [
+          for (DartType type in functionType.positionalParameters)
+            translateType(type)
+        ];
+        List<w.ValueType> outputs = [
+          if (functionType.returnType != const VoidType())
+            translateType(functionType.returnType)
+        ];
+        w.FunctionType wasmType = this.functionType(inputs, outputs);
+        return w.RefType.def(wasmType, nullable: type.isPotentiallyNullable);
+      }
       return typeForInfo(
           classInfo[type.classNode]!, type.isPotentiallyNullable);
     }
@@ -498,7 +531,7 @@
     return functionTypeParameterCount[heapType]!;
   }
 
-  w.DefinedGlobal makeFunctionRef(w.DefinedFunction f) {
+  w.DefinedGlobal makeFunctionRef(w.BaseFunction f) {
     return functionRefCache.putIfAbsent(f, () {
       w.DefinedGlobal global = m.addGlobal(
           w.GlobalType(w.RefType.def(f.type, nullable: false), mutable: false));
@@ -605,11 +638,16 @@
         b.ref_as_non_null();
       } else {
         // Downcast
-        var heapType = (to as w.RefType).heapType;
-        ClassInfo? info = classForHeapType[heapType];
         if (from.nullable && !to.nullable) {
           b.ref_as_non_null();
         }
+        var heapType = (to as w.RefType).heapType;
+        if (heapType is w.FunctionType) {
+          b.ref_as_func();
+          ref_cast(b, heapType);
+          return;
+        }
+        ClassInfo? info = classForHeapType[heapType];
         if (!(from as w.RefType).heapType.isSubtypeOf(w.HeapType.data)) {
           b.ref_as_data();
         }
@@ -711,7 +749,7 @@
   // The [type] parameter taken by the methods is either a [ClassInfo] (to use
   // the RTT for the class), an [int] (to use the RTT for the closure struct
   // corresponding to functions with that number of parameters) or a
-  // [w.DataType] (to use the canonical RTT for the type).
+  // [w.DefType] (to use the canonical RTT for the type).
 
   void struct_new(w.Instructions b, Object type) {
     if (options.runtimeTypes) {
@@ -823,7 +861,7 @@
       }
       return struct;
     } else {
-      b.rtt_canon(type as w.DataType);
+      b.rtt_canon(type as w.DefType);
       return type;
     }
   }
diff --git a/pkg/wasm_builder/lib/src/instructions.dart b/pkg/wasm_builder/lib/src/instructions.dart
index 10df25a..6c04e05 100644
--- a/pkg/wasm_builder/lib/src/instructions.dart
+++ b/pkg/wasm_builder/lib/src/instructions.dart
@@ -1234,7 +1234,8 @@
   /// Emit a `br_on_cast_static_fail` instruction.
   void br_on_cast_static_fail(Label label, DefType targetType) {
     assert(_verifyBranchTypes(label, 1, [_topOfStack]));
-    assert(_verifyCast((inputs) => [RefType.def(targetType, nullable: false)],
+    assert(_verifyCastStatic(
+        (inputs) => [RefType.def(targetType, nullable: false)],
         trace: ['br_on_cast_static_fail', label, targetType]));
     writeBytes(const [0xFB, 0x47]);
     _writeLabel(label);
diff --git a/sdk/lib/_internal/wasm/lib/errors_patch.dart b/sdk/lib/_internal/wasm/lib/errors_patch.dart
index d560ac0..666f279 100644
--- a/sdk/lib/_internal/wasm/lib/errors_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/errors_patch.dart
@@ -65,4 +65,11 @@
         stackTrace);
     return _throwObjectWithStackTrace(typeError, stackTrace);
   }
+
+  @pragma("wasm:entry-point")
+  static Never _throwWasmRefError(String expected, StackTrace stackTrace) {
+    final typeError = _TypeError.fromMessageAndStackTrace(
+        "The Wasm reference is not $expected", stackTrace);
+    return _throwObjectWithStackTrace(typeError, stackTrace);
+  }
 }
diff --git a/sdk/lib/wasm/wasm_types.dart b/sdk/lib/wasm/wasm_types.dart
index 288deaa..ea09211 100644
--- a/sdk/lib/wasm/wasm_types.dart
+++ b/sdk/lib/wasm/wasm_types.dart
@@ -23,17 +23,49 @@
 
 /// The Wasm `anyref` type.
 @pragma("wasm:entry-point")
-class WasmAnyRef extends _WasmBase {}
+class WasmAnyRef extends _WasmBase {
+  /// Upcast Dart object to `anyref`.
+  external factory WasmAnyRef.fromObject(Object o);
+
+  /// Whether this reference is a Dart object.
+  external bool get isObject;
+
+  /// Downcast `anyref` to a Dart object.
+  ///
+  /// Will throw if the reference is not a Dart object.
+  external Object toObject();
+}
+
+/// The Wasm `funcref` type.
+@pragma("wasm:entry-point")
+class WasmFuncRef extends WasmAnyRef {
+  /// Upcast typed function reference to `funcref`
+  external factory WasmFuncRef.fromWasmFunction(WasmFunction<Function> fun);
+
+  /// Downcast `anyref` to `funcref`.
+  ///
+  /// Will throw if the reference is not a `funcref`.
+  external factory WasmFuncRef.fromRef(WasmAnyRef ref);
+}
 
 /// The Wasm `eqref` type.
 @pragma("wasm:entry-point")
-class WasmEqRef extends WasmAnyRef {}
+class WasmEqRef extends WasmAnyRef {
+  /// Upcast Dart object to `eqref`.
+  external factory WasmEqRef.fromObject(Object o);
+}
 
 /// The Wasm `dataref` type.
 @pragma("wasm:entry-point")
-class WasmDataRef extends WasmEqRef {}
+class WasmDataRef extends WasmEqRef {
+  /// Upcast Dart object to `dataref`.
+  external factory WasmDataRef.fromObject(Object o);
+}
 
 abstract class _WasmArray extends WasmDataRef {
+  /// Dummy factory to silence error about missing superclass constructor.
+  external factory _WasmArray._dummy();
+
   external int get length;
 }
 
@@ -102,6 +134,25 @@
   external void write(int index, T value);
 }
 
+/// Wasm typed function reference.
+@pragma("wasm:entry-point")
+class WasmFunction<F extends Function> extends WasmFuncRef {
+  /// Create a typed function reference referring to the given function.
+  ///
+  /// The argument must directly name a static function with no optional
+  /// parameters and no type parameters.
+  external factory WasmFunction.fromFunction(F f);
+
+  /// Downcast `anyref` to a typed function reference.
+  ///
+  /// Will throw if the reference is not a function with the expected signature.
+  external factory WasmFunction.fromRef(WasmAnyRef ref);
+
+  /// Call the function referred to by this typed function reference.
+  @pragma("wasm:entry-point")
+  external F get call;
+}
+
 extension IntToWasmInt on int {
   WasmI32 toWasmI32() => WasmI32.fromInt(this);
   WasmI64 toWasmI64() => WasmI64.fromInt(this);
diff --git a/tests/web/wasm/wasm_types_test.dart b/tests/web/wasm/wasm_types_test.dart
new file mode 100644
index 0000000..5538605
--- /dev/null
+++ b/tests/web/wasm/wasm_types_test.dart
@@ -0,0 +1,110 @@
+// Copyright (c) 2022, 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:wasm';
+
+import 'package:expect/expect.dart';
+
+@pragma("wasm:import", "Object.create")
+external WasmAnyRef createObject(WasmAnyRef? prototype);
+
+@pragma("wasm:import", "Array.of")
+external WasmAnyRef singularArray(WasmAnyRef element);
+
+@pragma("wasm:import", "Reflect.apply")
+external WasmAnyRef apply(
+    WasmAnyRef target, WasmAnyRef thisArgument, WasmAnyRef argumentsList);
+
+WasmAnyRef? anyRef;
+WasmEqRef? eqRef;
+WasmDataRef? dataRef;
+
+int funCount = 0;
+
+void fun(WasmEqRef arg) {
+  funCount++;
+  Expect.equals("Dart object", arg.toObject());
+}
+
+test() {
+  // Some test objects
+  Object dartObject1 = "1";
+  Object dartObject2 = true;
+  Object dartObject3 = Object();
+  WasmAnyRef jsObject1 = createObject(null);
+
+  // A JS object is not a Dart object.
+  Expect.isFalse(jsObject1.isObject);
+
+  // A Wasm ref can be null and can be checked for null.
+  WasmAnyRef? jsObject2 = null;
+  Expect.isTrue(jsObject2 == null);
+
+  // Upcast Dart objects to Wasm refs and put them in fields.
+  anyRef = WasmAnyRef.fromObject(dartObject1);
+  eqRef = WasmEqRef.fromObject(dartObject2);
+  dataRef = WasmDataRef.fromObject(dartObject3);
+
+  // Dart objects are Dart objects.
+  Expect.isTrue(anyRef!.isObject);
+  Expect.isTrue(eqRef!.isObject);
+  Expect.isTrue(dataRef!.isObject);
+
+  // Casting back yields the original objects.
+  Expect.identical(dartObject1, anyRef!.toObject());
+  Expect.identical(dartObject2, eqRef!.toObject());
+  Expect.identical(dartObject3, dataRef!.toObject());
+
+  // Casting a JS object to a Dart object throws.
+  Object o;
+  Expect.throws(() {
+    o = jsObject1.toObject();
+  }, (_) => true);
+
+  // Integer and float conversions
+  Expect.equals(1, 1.toWasmI32().toIntSigned());
+  Expect.equals(-2, (-2).toWasmI32().toIntSigned());
+  Expect.equals(3, 3.toWasmI32().toIntUnsigned());
+  Expect.notEquals(-4, (-4).toWasmI32().toIntUnsigned());
+  Expect.equals(5, 5.toWasmI64().toInt());
+  Expect.equals(6.0, 6.0.toWasmF32().toDouble());
+  Expect.notEquals(7.1, 7.1.toWasmF32().toDouble());
+  Expect.equals(8.0, 8.0.toWasmF64().toDouble());
+
+  // Create a typed function reference for a Dart function and call it, both
+  // directly and from JS.
+  var dartObjectRef = WasmEqRef.fromObject("Dart object");
+  var ff = WasmFunction.fromFunction(fun);
+  ff.call(dartObjectRef);
+  apply(ff, createObject(null), singularArray(dartObjectRef));
+  Expect.isFalse(ff.isObject);
+
+  // Cast a typed function reference to a `funcref` and back.
+  WasmFuncRef funcref = WasmFuncRef.fromWasmFunction(ff);
+  var ff2 = WasmFunction<void Function(WasmEqRef)>.fromRef(funcref);
+  ff2.call(dartObjectRef);
+  Expect.isFalse(ff2.isObject);
+
+  // Casting a non-function JS object to a typed function reference throws.
+  Expect.throws(() {
+    WasmFunction<double Function(double)>.fromRef(jsObject1);
+  }, (_) => true);
+
+  // Create a typed function reference from an import and call it.
+  var createObjectFun = WasmFunction.fromFunction(createObject);
+  WasmAnyRef jsObject3 = createObjectFun.call(null);
+  Expect.isFalse(jsObject3.isObject);
+
+  Expect.equals(3, funCount);
+}
+
+main() {
+  try {
+    test();
+  } catch (e, s) {
+    print(e);
+    print(s);
+    rethrow;
+  }
+}