Files
sdk/sdk/lib/_internal/wasm/js_common/js_helper.dart
T
Martin Kustermann 2d78883f27 [dart2wasm] Simplify handling of JS interop callbacks
Right now a JS interop callback works like this:

* Each wasm module that gets instantiated will be given it's module
  instance (JS calls Dart to set it) via `setThisModule`

* When Dart code calls JS and gives it a callback to invoke, it gave it
  this module instance. It will also make the callback wasm function
  weakly exported.

* The JS trampoline code, when invoked, would then call the weakly
  exported wasm function from the module instance.

We simplify this now by making the Dart code simply give the wasm
function reference to JS, then JS can later on invoke it. No need to
weakly export a function and call back via
`module.exports.<weaklyExportedCallback>`

To ensure binaryen is aware that the wasm function may be called from
JS, we annotate it via the `(@binaryen.js.called)` annotation.

Change-Id: I828dd0cf8d3b36db338792c4e277a4bb94c76faf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/511080
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
2026-06-11 12:24:16 -07:00

875 lines
28 KiB
Dart

// 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.
// Helpers for working with JS.
library dart._js_helper;
import 'dart:_error_utils';
import 'dart:_internal';
import 'dart:_js_types' as js_types;
import 'dart:_string';
import 'dart:_wasm';
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop' as interop;
import 'dart:js_interop_unsafe';
import 'dart:typed_data';
part 'regexp_helper.dart';
/// [JSValue] is just a box for a `ref null extern` that is not a JS `null` or
/// `undefined`.
///
/// This is the type that all JS interop types (`JSAny`, `JSNumber` etc.) are an
/// extension of.
class JSValue extends JSExternWrapper {
/// This reference is always non-null and it never points to a JS `undefined`.
/// We currently don't make it non-nullable as that makes it impossible to
/// dummy-initialize locals with `JSValue` type.
JSValue(WasmExternRef? ref) : assert(!isDartNull(ref)), super(ref);
static JSValue? box(WasmExternRef? ref) =>
isDartNull(ref) ? null : JSValue(ref);
static T boxT<T>(WasmExternRef? ref) => unsafeCastOpaque<T>(box(ref));
@pragma('wasm:prefer-inline')
static WasmExternRef? unbox(JSValue? v) =>
v == null ? WasmExternRef.nullRef : v.wrappedExternRef;
@override
bool operator ==(Object that) =>
that is JSValue && areEqualInJS(wrappedExternRef, that.wrappedExternRef);
// Because [JSValue] is a subtype of [Object] it can be used in Dart
// collections. Unfortunately, JS does not expose an efficient hash code
// operation. To avoid surprising behavior, we force all [JSValue]s to fall
// back to differentiation via equality, essentially making [Set] and [Map]
// a regular linked list when the keys are [JSValue]. This behavior is not
// intuitive.
// TODO(joshualitt): There are a lot of different directions we can go, but
// the most straightforward to expose `JSMap` and `JSSet` from JS for users
// who need to efficiently manage JS objects in collections.
@override
int get hashCode => 0;
@override
String toString() => stringify(wrappedExternRef);
bool get isExternalizedDartValue => isWasmGCStruct(wrappedExternRef);
}
// Extension helpers to convert to an externref.
// TODO(srujzs): We should rename these to `getAsExternRef` so they don't
// collide with instance members of box objects.
extension DoubleToExternRef on double? {
WasmExternRef? get toExternRef =>
this == null ? WasmExternRef.nullRef : toJSNumber(this!);
}
extension StringToExternRef on String? {
WasmExternRef? get toExternRef => this == null
? WasmExternRef.nullRef
: jsStringFromDartString(this!).wrappedExternRef;
}
extension JSValueToExternRef on JSValue? {
WasmExternRef? get toExternRef => JSValue.unbox(this);
}
extension JSAnyToExternRef on JSAny? {
WasmExternRef? get toExternRef => JSValue.unbox(this as JSValue?);
}
// For `dartify` and `jsify`, we match the conflation of `JSUndefined`, `JSNull`
// and `null`.
@pragma('wasm:entry-point')
bool isDartNull(WasmExternRef? ref) => ref.isNull || isJSUndefined(ref);
class JSArrayIteratorAdapter<T> implements Iterator<T> {
final JSArray array;
int index = -1;
JSArrayIteratorAdapter(this.array);
@override
bool moveNext() {
index++;
int length = array.length;
if (index > length) {
throw 'Iterator out of bounds';
}
return index < length;
}
@override
T get current => dartifyRaw(array[index].toExternRef) as T;
}
/// [JSArrayIterableAdapter] lazily adapts a [JSArray] to Dart's [Iterable]
/// interface.
class JSArrayIterableAdapter<T> extends EfficientLengthIterable<T>
implements HideEfficientLengthIterable<T> {
final JSArray array;
JSArrayIterableAdapter(this.array);
@override
Iterator<T> get iterator => JSArrayIteratorAdapter<T>(array);
@override
int get length => array.length;
}
Object jsObjectToDartObject(WasmExternRef? ref) =>
unsafeCastOpaque<Object>(ref.internalize());
WasmExternRef jsObjectFromDartObject(Object object) =>
unsafeCastOpaque<WasmAnyRef>(object).externalize();
bool isJSUndefined(WasmExternRef? o) => JS<bool>('o => o === undefined', o);
bool isJSFunction(WasmExternRef? o) =>
JS<bool>("o => typeof o === 'function'", o);
bool isJSWrappedDartFunction(WasmExternRef? o) => JS<bool>(
"o => typeof o === 'function' && o[jsWrappedDartFunctionSymbol] === true",
o,
);
bool isJSObject(WasmExternRef? o) => JS<bool>("o => o instanceof Object", o);
bool isJSSimpleObject(WasmExternRef? o) => JS<bool>("""o => {
const proto = Object.getPrototypeOf(o);
return proto === Object.prototype || proto === null;
}""", o);
bool isJSRegExp(WasmExternRef? o) => JS<bool>("o => o instanceof RegExp", o);
bool areEqualInJS(WasmExternRef? l, WasmExternRef? r) =>
JS<bool>("(l, r) => l === r", l, r);
@pragma('wasm:entry-point')
double toDartDouble(WasmExternRef? ref) {
final numberType = _checkNumberType(ref);
if (numberType != 1) {
throw ArgumentError('JS value is not a number');
}
return _toDartDoubleUnchecked(ref);
}
@pragma('wasm:entry-point')
double? toDartNullableDouble(WasmExternRef? ref) {
final refType = _checkNumberType(ref);
if (refType == 0) return null;
if (refType == 1) return _toDartDoubleUnchecked(ref);
throw ArgumentError('JS value is not a number');
}
double _toDartDoubleUnchecked(WasmExternRef? ref) => JS<double>("o => o", ref);
int _checkNumberType(WasmExternRef? ref) {
return JS<WasmI32>("""o => {
if (o === undefined || o === null) return 0;
if (typeof o === 'number') return 1;
return 2;
}""", ref).toIntUnsigned();
}
int _jsNonNullToInt(WasmExternRef? ref, bool typeIsRight) {
if (typeIsRight) {
final dartDouble = _toDartDoubleUnchecked(ref);
if (dartDouble.isFinite) {
final dartInt = dartDouble.toInt();
if (dartInt.toDouble() == dartDouble) {
return dartInt;
}
}
}
throw ArgumentError('JS value is not integer');
}
@pragma('wasm:entry-point')
int toDartInt(WasmExternRef? ref) {
final numberType = _checkNumberType(ref);
return _jsNonNullToInt(ref, numberType == 1);
}
@pragma('wasm:entry-point')
int? toDartNullableInt(WasmExternRef? ref) {
final numberType = _checkNumberType(ref);
if (numberType == 0) return null;
return _jsNonNullToInt(ref, numberType == 1);
}
@pragma('wasm:entry-point')
WasmExternRef? toJSNumber(double ref) => JS<WasmExternRef?>("o => o", ref);
int _checkBoolType(WasmExternRef? ref) {
return JS<WasmI32>("""o => {
if (o === undefined || o === null) return 0;
if (typeof o === 'boolean') return 1;
return 2;
}""", ref).toIntUnsigned();
}
@pragma('wasm:entry-point')
bool toDartBool(WasmExternRef? ref) {
final refType = _checkBoolType(ref);
if (refType != 1) {
throw ArgumentError('JS value is not a boolean');
}
return _toDartBoolUnchecked(ref);
}
@pragma('wasm:entry-point')
bool? toDartNullableBool(WasmExternRef? ref) {
final refType = _checkBoolType(ref);
if (refType == 0) return null;
if (refType == 1) return _toDartBoolUnchecked(ref);
throw ArgumentError('JS value is not a boolean');
}
bool _toDartBoolUnchecked(WasmExternRef? ref) => JS<bool>("o => o", ref);
WasmExternRef? toJSBoolean(bool b) => JS<WasmExternRef?>("b => !!b", b);
int objectLength(WasmExternRef? o) =>
JS<WasmI32>("o => o.length", o).toIntSigned();
int byteLength(WasmExternRef? o) =>
JS<WasmI32>("o => o.byteLength", o).toIntSigned();
WasmExternRef? objectReadIndex(WasmExternRef? o, int index) =>
JS<WasmExternRef?>("(o, i) => o[i]", o, index.toWasmI32());
Function unwrapJSWrappedDartFunction(WasmExternRef? f) =>
JS<Function>("f => f.dartFunction", f);
external WasmExternRef jsInt8ArrayFromDartInt8List(Int8List l);
external WasmExternRef jsUint8ArrayFromDartUint8List(Uint8List l);
external WasmExternRef jsUint8ClampedArrayFromDartUint8ClampedList(
Uint8ClampedList l,
);
external WasmExternRef jsInt16ArrayFromDartInt16List(Int16List l);
external WasmExternRef jsUint16ArrayFromDartUint16List(Uint16List l);
external WasmExternRef jsInt32ArrayFromDartInt32List(Int32List l);
external WasmExternRef jsUint32ArrayFromDartUint32List(Uint32List l);
external WasmExternRef jsFloat32ArrayFromDartFloat32List(Float32List l);
external WasmExternRef jsFloat64ArrayFromDartFloat64List(Float64List l);
external WasmExternRef jsDataViewFromDartByteData(ByteData data, int length);
WasmExternRef? _jsifyRawList(List<Object?> list) {
final length = list.length;
final result = JSArray<JSAny?>.withLength(length);
for (int i = 0; i < length; i++) {
result[i] = JSValue.box(jsifyRaw(list[i])) as JSAny?;
}
return (result as JSValue).toExternRef;
}
external JSStringImpl jsStringFromDartString(String s);
WasmExternRef? newObjectRaw() => JS<WasmExternRef?>('() => ({})');
WasmExternRef? newArrayRaw() => JS<WasmExternRef?>('() => []');
WasmExternRef? newArrayFromLengthRaw(int length) =>
JS<WasmExternRef?>('l => new Array(l)', length.toWasmI32());
WasmExternRef? globalThisRaw() => JS<WasmExternRef?>('() => globalThis');
WasmExternRef? callConstructorVarArgsRaw(
WasmExternRef? o,
WasmExternRef? args,
) =>
// Apply bind to the constructor. We pass `null` as the first argument
// to `bind.apply` because this is `bind`'s unused context
// argument(`new` will explicitly create a new context).
JS<WasmExternRef?>(
"""(constructor, args) => {
const factoryFunction = constructor.bind.apply(
constructor, [null, ...args]);
return new factoryFunction();
}""",
o,
args,
);
bool hasPropertyRaw(WasmExternRef? o, WasmExternRef? p) =>
JS<bool>("(o, p) => p in o", o, p);
WasmExternRef? getPropertyRaw(WasmExternRef? o, WasmExternRef? p) =>
JS<WasmExternRef?>("(o, p) => o[p]", o, p);
WasmExternRef? setPropertyRaw(
WasmExternRef? o,
WasmExternRef? p,
WasmExternRef? v,
) => JS<WasmExternRef?>("(o, p, v) => o[p] = v", o, p, v);
WasmExternRef? callMethodVarArgsRaw(
WasmExternRef? o,
WasmExternRef? method,
WasmExternRef? args,
) => JS<WasmExternRef?>("(o, m, a) => o[m].apply(o, a)", o, method, args);
String typeof(WasmExternRef? object) =>
JSStringImpl.fromRefUnchecked(JS<WasmExternRef?>("o => typeof o", object));
String stringify(WasmExternRef? object) =>
JSStringImpl.fromRefUnchecked(JS<WasmExternRef?>("o => String(o)", object));
/// `Promise.then` call where [failureFunc] can be a JS function that expects
/// two arguments, the first being the error, and the second being whether the
/// error was undefined.
///
/// The second argument is needed as dart2wasm implicitly converts all JS
/// `undefined`s to Dart `null` when boxing JS values.
void promiseThenWithIsUndefined(
WasmExternRef? promise,
WasmExternRef? successFunc,
WasmExternRef? failureFunc,
) => JS<void>(
"(p, s, f) => p.then(s, (e) => f(e, e === undefined))",
promise,
successFunc,
failureFunc,
);
Future<T> externPromiseToFuture<T>(WasmExternRef? jsPromise) {
final completer = Completer<T>();
final success = (JSAny? r) {
// Note that we explicitly type the parameter as `JSAny?` instead of `T`.
// This is because if there's a `TypeError` with the cast, we want to
// bubble that up through the completer, so we end up doing a try-catch
// here to do so.
try {
final value = r as T;
completer.complete(value);
} catch (e) {
completer.completeError(e);
}
}.toJS;
final error = (JSAny? e, bool isUndefined) {
// `e` is null when the original error is either JS `null` or JS
// `undefined`.
if (e == null) {
completer.completeError(NullRejectionException(isUndefined));
return;
}
completer.completeError(e);
}.toJS;
promiseThenWithIsUndefined(jsPromise, success.toExternRef, error.toExternRef);
return completer.future;
}
// Currently, `allowInterop` returns a Function type. This is unfortunate for
// Dart2wasm because it means arbitrary Dart functions can flow to JS util
// calls. Our only solutions is to cache every function called with
// `allowInterop` and to replace them with the wrapped variant when they flow
// to JS.
// NOTE: We are not currently replacing functions returned from JS.
final Map<Function, JSValue> functionToJSWrapper = Map.identity();
WasmExternRef? jsArrayBufferFromDartByteBuffer(ByteBuffer buffer) {
ByteData byteData = ByteData.view(buffer);
WasmExternRef? dataView = jsDataViewFromDartByteData(
byteData,
byteData.lengthInBytes,
);
return getPropertyRaw(dataView, 'buffer'.toExternRef);
}
WasmExternRef? jsifyRaw(Object? o) {
if (o == null) return WasmExternRef.nullRef;
if (o is bool) return toJSBoolean(o);
if (o is num) return jsifyNum(o);
if (o is JSValue) return jsifyJSValue(o);
if (o is String) return jsifyString(o);
if (o is js_types.JSArrayBase) {
if (o is js_types.JSInt8ArrayImpl) return jsifyJSInt8ArrayImpl(o);
if (o is js_types.JSUint8ArrayImpl) return jsifyJSUint8ArrayImpl(o);
if (o is js_types.JSUint8ClampedArrayImpl) {
return jsifyJSUint8ClampedArrayImpl(o);
}
if (o is js_types.JSInt16ArrayImpl) return jsifyJSInt16ArrayImpl(o);
if (o is js_types.JSUint16ArrayImpl) return jsifyJSUint16ArrayImpl(o);
if (o is js_types.JSInt32ArrayImpl) return jsifyJSInt32ArrayImpl(o);
if (o is js_types.JSUint32ArrayImpl) return jsifyJSUint32ArrayImpl(o);
if (o is js_types.JSFloat32ArrayImpl) return jsifyJSFloat32ArrayImpl(o);
if (o is js_types.JSFloat64ArrayImpl) return jsifyJSFloat64ArrayImpl(o);
} else if (o is TypedData) {
if (o is Int8List) return jsInt8ArrayFromDartInt8List(o);
if (o is Uint8List) return jsUint8ArrayFromDartUint8List(o);
if (o is Uint8ClampedList) {
return jsUint8ClampedArrayFromDartUint8ClampedList(o);
}
if (o is Int16List) return jsInt16ArrayFromDartInt16List(o);
if (o is Uint16List) return jsUint16ArrayFromDartUint16List(o);
if (o is Int32List) return jsInt32ArrayFromDartInt32List(o);
if (o is Uint32List) return jsUint32ArrayFromDartUint32List(o);
if (o is Float32List) return jsFloat32ArrayFromDartFloat32List(o);
if (o is Float64List) return jsFloat64ArrayFromDartFloat64List(o);
if (o is js_types.JSDataViewImpl) return jsifyJSDataViewImpl(o);
if (o is ByteData) return jsifyByteData(o);
} else if (o is List<Object?>) {
// TODO(srujzs): Once `package:js` support is fully removed, we should
// remove this as it'll be dead code. `jsify` will convert iterables
// differently, and `dart:js_interop` `external` conversions shouldn't come
// across this code.
return _jsifyRawList(o);
} else if (o is ByteBuffer) {
if (o is js_types.JSArrayBufferImpl) return jsifyJSArrayBufferImpl(o);
return jsArrayBufferFromDartByteBuffer(o);
} else if (o is Function) {
// TODO(srujzs): Once `package:js` support is fully removed, we should
// remove this to unify with the JS backends, which don't do this
// conversion.
return jsifyFunction(o);
} else {
return jsObjectFromDartObject(o);
}
}
WasmExternRef? jsifyInt(int i) {
const int minI31 = -(1 << 30);
const int maxI31 = (1 << 30) - 1;
// Pass small ints as `i31ref` to avoid allocation.
if (i >= minI31 && i <= maxI31) {
return WasmI31Ref.fromI32(WasmI32.fromInt(i)).externalize();
}
return toJSNumber(i.toDouble());
}
WasmExternRef? jsifyNum(num o) =>
o is int ? jsifyInt(o) : toJSNumber(unsafeCast<double>(o));
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSValue(JSValue o) => o.toExternRef;
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyString(String o) => jsStringFromDartString(o).toExternRef;
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSInt8ArrayImpl(js_types.JSInt8ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint8ArrayImpl(js_types.JSUint8ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint8ClampedArrayImpl(
js_types.JSUint8ClampedArrayImpl o,
) => o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSInt16ArrayImpl(js_types.JSInt16ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint16ArrayImpl(js_types.JSUint16ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSInt32ArrayImpl(js_types.JSInt32ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint32ArrayImpl(js_types.JSUint32ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSFloat32ArrayImpl(js_types.JSFloat32ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSFloat64ArrayImpl(js_types.JSFloat64ArrayImpl o) =>
o.toJSArrayExternRef();
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSDataViewImpl(js_types.JSDataViewImpl o) =>
o.wrappedExternRef;
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSArrayBufferImpl(js_types.JSArrayBufferImpl o) =>
o.wrappedExternRef;
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyByteData(ByteData o) =>
jsDataViewFromDartByteData(o, o.lengthInBytes);
@pragma('wasm:prefer-inline')
WasmExternRef? jsifyFunction(Function o) {
assert(
functionToJSWrapper.containsKey(o),
'Must call `allowInterop` on functions before they flow to JS',
);
return functionToJSWrapper[o]!.toExternRef;
}
bool isWasmGCStruct(WasmExternRef? ref) => ref.internalize()?.isObject ?? false;
/// Container class for constants that represent the possible types of a
/// [WasmExternRef] that can then be used in a [dartifyRaw] call.
///
/// The values within this class should correspond to the values returned by
/// [externRefType] and should be updated if that function is updated. Constants
/// are preferred over enums for performance.
abstract final class ExternRefType {
static const int null_ = 0;
static const int undefined = 1;
static const int boolean = 2;
static const int number = 3;
static const int string = 4;
static const int array = 5;
static const int int8Array = 6;
static const int uint8Array = 7;
static const int uint8ClampedArray = 8;
static const int int16Array = 9;
static const int uint16Array = 10;
static const int int32Array = 11;
static const int uint32Array = 12;
static const int float32Array = 13;
static const int float64Array = 14;
static const int dataView = 15;
static const int arrayBuffer = 16;
static const int sharedArrayBuffer = 17;
static const int promise = 18;
static const int unknown = 19;
}
/// Returns an integer representing the type of [ref] that corresponds to one of
/// the constant values in [ExternRefType].
///
/// If this function is updated to return different values, [ExternRefType]
/// should be updated as well.
int externRefType(WasmExternRef? ref) {
if (ref.isNull) return ExternRefType.null_;
final val = JS<WasmI32>('''
o => {
if (o === undefined) return 1;
var type = typeof o;
if (type === 'boolean') return 2;
if (type === 'number') return 3;
if (type === 'string') return 4;
if (o instanceof Array) return 5;
if (ArrayBuffer.isView(o)) {
if (o instanceof Int8Array) return 6;
if (o instanceof Uint8Array) return 7;
if (o instanceof Uint8ClampedArray) return 8;
if (o instanceof Int16Array) return 9;
if (o instanceof Uint16Array) return 10;
if (o instanceof Int32Array) return 11;
if (o instanceof Uint32Array) return 12;
if (o instanceof Float32Array) return 13;
if (o instanceof Float64Array) return 14;
if (o instanceof DataView) return 15;
}
if (o instanceof ArrayBuffer) return 16;
// Feature check for `SharedArrayBuffer` before doing a type-check.
if (globalThis.SharedArrayBuffer !== undefined &&
o instanceof SharedArrayBuffer) {
return 17;
}
if (o instanceof Promise) return 18;
return 19;
}
''', ref).toIntUnsigned();
return val;
}
/// Non-recursively converts [ref] from a JS value to a Dart value for some JS
/// types.
///
/// If [refType] is not null, it is treated as one of the values from
/// [ExternRefType]. Otherwise, this method calls [externRefType] to determine
/// the right [ExternRefType].
Object? dartifyRaw(WasmExternRef? ref, [int? refType]) {
refType ??= externRefType(ref);
return switch (refType) {
ExternRefType.null_ || ExternRefType.undefined => null,
ExternRefType.boolean => _toDartBoolUnchecked(ref),
ExternRefType.number => _toDartDoubleUnchecked(ref),
ExternRefType.string => JSStringImpl.fromRefUnchecked(ref),
ExternRefType.array => toDartList(ref),
ExternRefType.int8Array => js_types.JSInt8ArrayImpl.fromRefUnchecked(ref),
ExternRefType.uint8Array => js_types.JSUint8ArrayImpl.fromRefUnchecked(ref),
ExternRefType.uint8ClampedArray =>
js_types.JSUint8ClampedArrayImpl.fromRefUnchecked(ref),
ExternRefType.int16Array => js_types.JSInt16ArrayImpl.fromRefUnchecked(ref),
ExternRefType.uint16Array => js_types.JSUint16ArrayImpl.fromRefUnchecked(
ref,
),
ExternRefType.int32Array => js_types.JSInt32ArrayImpl.fromRefUnchecked(ref),
ExternRefType.uint32Array => js_types.JSUint32ArrayImpl.fromRefUnchecked(
ref,
),
ExternRefType.float32Array => js_types.JSFloat32ArrayImpl.fromRefUnchecked(
ref,
),
ExternRefType.float64Array => js_types.JSFloat64ArrayImpl.fromRefUnchecked(
ref,
),
ExternRefType.arrayBuffer || ExternRefType.sharedArrayBuffer =>
js_types.JSArrayBufferImpl.fromRefUnchecked(ref),
ExternRefType.dataView => js_types.JSDataViewImpl.fromRefUnchecked(ref),
ExternRefType.promise => externPromiseToFuture<JSValue?>(ref),
ExternRefType.unknown =>
isJSWrappedDartFunction(ref)
? unwrapJSWrappedDartFunction(ref)
: isWasmGCStruct(ref)
? jsObjectToDartObject(ref)
: JSValue(ref),
_ => () {
// Assert that we've handled everything in the range.
assert(refType! >= 0 && refType >= ExternRefType.unknown);
throw 'Unhandled dartifyRaw type case: $refType';
}(),
};
}
List<double> jsFloatTypedArrayToDartFloatTypedData(
WasmExternRef? ref,
List<double> makeTypedData(int size),
) {
int length = objectLength(ref);
List<double> list = makeTypedData(length);
for (int i = 0; i < length; i++) {
list[i] = toDartDouble(objectReadIndex(ref, i));
}
return list;
}
List<int> jsIntTypedArrayToDartIntTypedData(
WasmExternRef? ref,
List<int> makeTypedData(int size),
) {
int length = objectLength(ref);
List<int> list = makeTypedData(length);
for (int i = 0; i < length; i++) {
list[i] = toDartDouble(objectReadIndex(ref, i)).toInt();
}
return list;
}
JSArray<T> toJSArray<T extends JSAny?>(List<T> list) {
final length = list.length;
if (length <= 4) {
if (length == 0) {
return JSArray<T>.withLength(0);
}
final list0 = list[0].toExternRef;
if (length == 1) {
return JSValue(JS<WasmExternRef>("o => [o]", list0)) as JSArray<T>;
}
final list1 = list[1].toExternRef;
if (length == 2) {
return JSValue(JS<WasmExternRef>("(o0, o1) => [o0, o1]", list0, list1))
as JSArray<T>;
}
final list2 = list[2].toExternRef;
if (length == 3) {
return JSValue(
JS<WasmExternRef>("(o0, o1, o2) => [o0, o1, o2]", list0, list1, list2),
) as JSArray<T>;
}
final list3 = list[3].toExternRef;
if (length == 4) {
return JSValue(
JS<WasmExternRef>(
"(o0, o1, o2, o3) => [o0, o1, o2, o3]",
list0,
list1,
list2,
list3,
),
) as JSArray<T>;
}
}
JSArray<T> result = JSArray<T>.withLength(length);
for (int i = 0; i < length; i++) {
result[i] = list[i];
}
return result;
}
@pragma('wasm:entry-point')
List<Object?> toDartList(WasmExternRef? ref) => List<Object?>.generate(
objectLength(ref),
(int n) => dartifyRaw(objectReadIndex(ref, n)),
);
@pragma('wasm:entry-point')
List<Object?>? toDartNullableList(WasmExternRef? ref) {
if (ref.isNull || isJSUndefined(ref)) return null;
return toDartList(ref);
}
// These two trivial helpers are needed to work around an issue with tearing off
// functions that take / return [WasmExternRef].
bool _isDartFunctionWrapped<F extends Function>(F f) =>
functionToJSWrapper.containsKey(f);
F _wrapDartFunction<F extends Function>(F f, WasmExternRef ref) {
functionToJSWrapper[f] = JSValue(ref);
return f;
}
/// Takes a [codeTemplate] string which must represent a valid JS function, and
/// a list of optional arguments. The [codeTemplate] will be inserted into the
/// JS runtime, and the call to [JS] will be replaced by a call to an external
/// static method stub that imports the JS function.
///
/// We will replace the enclosing procedure itself if:
/// 1) The enclosing procedure is static.
/// 2) The enclosing procedure has a body with a single statement, and that
/// statement is just the `StaticInvocation` of [JS] itself.
/// 3) All of the arguments to [JS] are `VariableGet`s.
external T JS<T>(
String codeTemplate, [
arg0,
arg1,
arg2,
arg3,
arg4,
arg5,
arg6,
arg7,
arg8,
arg9,
arg10,
arg11,
arg12,
arg13,
arg14,
arg51,
arg16,
arg17,
arg18,
arg19,
]);
/// Represents a JS `null` or `undefined` thrown from JS and caught in Wasm.
///
/// The class name is copied from the dart2js class for the same thing, for
/// compatibility.
///
/// This class is allocated by the generated code.
class NullThrownFromJavaScriptException implements Exception {
/// Whether the reference was `null`. If not, then it must be pointing to a JS
/// `undefined`.
final bool _isNull;
const NullThrownFromJavaScriptException.fromNull() : _isNull = true;
const NullThrownFromJavaScriptException.fromUndefined() : _isNull = false;
/// `toString` copied from dart2js's `NullThrownFromJavaScriptException` for
/// compatibility.
@override
String toString() =>
"Throw of null ('${_isNull ? 'null' : 'undefined'}' from JavaScript)";
}
/// Box an exception caught from JS, the same way as dart2js.
///
/// When the exception value is `null` or `undefined`, this returns a
/// [NullThrownFromJavaScriptException].
///
/// Otherwise it returns a `JSValue`.
///
/// This is called by the generated code and passed a JS exception as
/// `externref`, caught using the `WebAssembly.JSTag` exception tag. The return
/// value will be used to assign the exception variable in `catch` blocks, so it
/// needs to have type `Object`.
@pragma('wasm:entry-point')
Object boxJsException(WasmExternRef? ref) {
if (ref.isNull) return NullThrownFromJavaScriptException.fromNull();
if (isJSUndefined(ref))
return NullThrownFromJavaScriptException.fromUndefined();
return JSValue(ref);
}
/// Get the stack trace of an exception value thrown in JS and caught in Wasm.
///
/// This is called by the generated code, with the same argument as
/// [boxJsException].
///
/// The return value will be assigned to the stack trace variables in `catch`
/// blocks, so it needs to have type [StackTrace].
@pragma('wasm:entry-point')
StackTrace jsExceptionStackTrace(WasmExternRef? ref) => JavaScriptStack(
JS<WasmExternRef?>("""
(exn) => {
if (exn instanceof Error) {
return exn.stack;
} else {
return null;
}
}
""", ref),
);
class JavaScriptStack extends JSExternWrapper implements StackTrace {
final bool _fromCurrent;
JavaScriptStack(WasmExternRef? ref) : _fromCurrent = false, super(ref);
// Note: We remove the first four frames to prevent including
// `StackTrace.current`, other current helpers and the JS interop function.
// On Chrome, the first line is not a frame but a line with just "Error",
// sometimes with details:
// "Error: ...". Also remove that line.
late final String _stringified = wrappedExternRef.isNull
? ""
: JSStringImpl.fromRefUnchecked(
_fromCurrent
? JS<WasmExternRef?>(r"""(exn) => {
let stackString = exn.toString();
let frames = stackString.split('\n');
let drop = 4;
if (frames[0].startsWith('Error')) {
drop += 1;
}
return frames.slice(drop).join('\n');
}""", wrappedExternRef)
: wrappedExternRef,
);
@pragma("wasm:never-inline")
JavaScriptStack.current()
: _fromCurrent = true,
super(JS<WasmExternRef?>("() => new Error().stack"));
@override
String toString() => _stringified;
}
base class JSExternWrapper {
final WasmExternRef? _externRef;
JSExternWrapper(this._externRef);
}
extension JSExternWrapperExt on JSExternWrapper {
@pragma("wasm:prefer-inline")
WasmExternRef? get wrappedExternRef => _externRef;
}