From b8402f5c27200b29330f18d24bbd0545ad90712c Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Wed, 26 Jun 2024 21:53:53 +0000 Subject: [PATCH] [dart:js_interop] Make ExternalDartReference generic Closes https://github.com/dart-lang/sdk/issues/55342 Closes https://github.com/dart-lang/sdk/issues/55536 Closes https://github.com/dart-lang/sdk/issues/56015 - Adds a type parameter T that extends Object? to ExternalDartReference to capture the type of the value that was externalized. -- In the JS compilers, the representation type of ExternalDartReference is now T. -- In dart2wasm, the representation type is now JSValue?. - ExternalDartReference no longer implements Object. - Return type of toDartObject is now T. - ObjectToExternalDartReference and ExternalDartReferenceToObject both now are on a T that is bound to Object?. - Internal patches for WeakReference and Finalizer are updated. Change-Id: Ic2dc834b17ec6a4eb2122cba3c495a6e0a1eae6e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/370663 Commit-Queue: Srujan Gaddam Reviewed-by: Martin Kustermann Reviewed-by: Sigmund Cherem Reviewed-by: Lasse Nielsen --- CHANGELOG.md | 9 + .../lib/js/callback_specializer.dart | 2 +- pkg/dart2wasm/lib/js/util.dart | 4 +- .../js_shared/lib/js_interop_patch.dart | 84 +++++---- sdk/lib/_internal/js_shared/lib/js_types.dart | 2 +- .../_internal/wasm/lib/js_interop_patch.dart | 107 ++++++++---- sdk/lib/_internal/wasm/lib/js_types.dart | 2 +- sdk/lib/_internal/wasm/lib/weak_patch.dart | 28 +-- sdk/lib/js_interop/js_interop.dart | 34 ++-- ...on_interop_extension_type_static_test.dart | 6 +- .../external_dart_reference_test.dart | 162 ++++++++++++++---- 11 files changed, 303 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03286c3d718..6fed68e8a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,9 +57,18 @@ of a `String` to support other JS values as well, like `TrustedScriptURL`s. - **Breaking Change** [#55267][]: `isTruthy` and `not` now return `JSBoolean` instead of `bool` to be consistent with the other operators. +- **Breaking Change** `ExternalDartReference` no longer implements `Object`. + `ExternalDartReference` now accepts a type parameter `T` with a bound of + `Object?` to capture the type of the Dart object that is externalized. + `ExternalDartReferenceToObject.toDartObject` now returns a `T`. + `ExternalDartReferenceToObject` and `ObjectToExternalDartReference` are now + extensions on `T` and `ExternalDartReference`, respectively, where `T + extends Object?`. See [#55342][] and [#55536][] for more details. [#55508]: https://github.com/dart-lang/sdk/issues/55508 [#55267]: https://github.com/dart-lang/sdk/issues/55267 +[#55342]: https://github.com/dart-lang/sdk/issues/55342 +[#55536]: https://github.com/dart-lang/sdk/issues/55536 ### Tools diff --git a/pkg/dart2wasm/lib/js/callback_specializer.dart b/pkg/dart2wasm/lib/js/callback_specializer.dart index 7df14b69d3b..d4d1c5244d5 100644 --- a/pkg/dart2wasm/lib/js/callback_specializer.dart +++ b/pkg/dart2wasm/lib/js/callback_specializer.dart @@ -33,7 +33,7 @@ class CallbackSpecializer { VariableGet v = VariableGet(positionalParameters[i]); if (_util.isJSValueType(callbackParameterType) && boxExternRef) { expression = _createJSValue(v); - if (!callbackParameterType.isPotentiallyNullable) { + if (!callbackParameterType.extensionTypeErasure.isPotentiallyNullable) { expression = NullCheck(expression); } } else { diff --git a/pkg/dart2wasm/lib/js/util.dart b/pkg/dart2wasm/lib/js/util.dart index addaf90642c..bbe8759054a 100644 --- a/pkg/dart2wasm/lib/js/util.dart +++ b/pkg/dart2wasm/lib/js/util.dart @@ -70,7 +70,7 @@ class CoreTypesUtil { Procedure jsifyTarget(DartType type) => isJSValueType(type) ? jsValueUnboxTarget : jsifyRawTarget; - /// Return whether [type] erases to a `JSValue`. + /// Whether [type] erases to a `JSValue` or `JSValue?`. bool isJSValueType(DartType type) => _extensionIndex.isStaticInteropType(type) || _extensionIndex.isExternalDartReferenceType(type); @@ -112,7 +112,7 @@ class CoreTypesUtil { // there are static interop types that are not boxed as JSValue, we // might need a proper cast then. expression = invokeOneArg(jsValueBoxTarget, invocation); - if (returnType.isPotentiallyNonNullable) { + if (returnType.extensionTypeErasure.isPotentiallyNonNullable) { expression = NullCheck(expression); } } else { diff --git a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart index deee3dfaf25..9513deebf41 100644 --- a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart +++ b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart @@ -19,7 +19,7 @@ JSObjectRepType _createObjectLiteral() => @pragma('dart2js:prefer-inline') JSObject get globalContext => staticInteropGlobalContext as JSObject; -/// Helper for working with the [JSAny?] top type in a backend agnostic way. +// Helper for working with the JSAny? top type in a backend agnostic way. @patch extension NullableUndefineableJSAnyExtension on JSAny? { @patch @@ -53,7 +53,7 @@ extension JSAnyUtilityExtension on JSAny? { Object? dartify() => js_util.dartify(this); } -/// Utility extensions for [Object?]. +// Utility extensions for Object?. @patch extension NullableObjectUtilExtension on Object? { @patch @@ -61,7 +61,8 @@ extension NullableObjectUtilExtension on Object? { JSAny? jsify() => js_util.jsify(this); } -/// [JSExportedDartFunction] <-> [Function] +// ----------------------------------------------------------------------------- +// JSExportedDartFunction <-> Function @patch extension JSExportedDartFunctionToFunction on JSExportedDartFunction { // TODO(srujzs): We should unwrap rather than allow arbitrary JS functions @@ -79,15 +80,16 @@ extension FunctionToJSExportedDartFunction on Function { js_util.allowInterop(this) as JSExportedDartFunction; } -/// Embedded global property for wrapped Dart objects passed via JS interop. -/// -/// This is a Symbol so that different Dart applications don't share Dart -/// objects from different Dart runtimes. We expect all [JSBoxedDartObject]s to -/// have this Symbol. +// Embedded global property for wrapped Dart objects passed via JS interop. +// +// This is a Symbol so that different Dart applications don't share Dart +// objects from different Dart runtimes. We expect all JSBoxedDartObjects to +// have this Symbol. final Object _jsBoxedDartObjectProperty = foreign_helper.JS('', 'Symbol("jsBoxedDartObjectProperty")'); -/// [JSBoxedDartObject] <-> [Object] +// ----------------------------------------------------------------------------- +// JSBoxedDartObject <-> Object @patch extension JSBoxedDartObjectToObject on JSBoxedDartObject { @patch @@ -118,23 +120,25 @@ extension ObjectToJSBoxedDartObject on Object { } } -/// [ExternalDartReference] <-> [Object] +// ----------------------------------------------------------------------------- +// ExternalDartReference <-> T @patch -extension ExternalDartReferenceToObject on ExternalDartReference { +extension ExternalDartReferenceToObject + on ExternalDartReference { @patch @pragma('dart2js:prefer-inline') - Object get toDartObject => this; + T get toDartObject => _externalDartReference; } @patch -extension ObjectToExternalDartReference on Object { +extension ObjectToExternalDartReference on T { @patch @pragma('dart2js:prefer-inline') - ExternalDartReference get toExternalReference => - ExternalDartReference._(this); + ExternalDartReference get toExternalReference => + ExternalDartReference._(this); } -/// [JSPromise] -> [Future]. +// JSPromise -> Future @patch extension JSPromiseToFuture on JSPromise { @patch @@ -142,7 +146,8 @@ extension JSPromiseToFuture on JSPromise { Future get toDart => js_util.promiseToFuture(this); } -/// [JSArrayBuffer] <-> [ByteBuffer] +// ----------------------------------------------------------------------------- +// JSArrayBuffer <-> ByteBuffer @patch extension JSArrayBufferToByteBuffer on JSArrayBuffer { @patch @@ -157,7 +162,8 @@ extension ByteBufferToJSArrayBuffer on ByteBuffer { JSArrayBuffer get toJS => this as JSArrayBuffer; } -/// [JSDataView] <-> [ByteData] +// ----------------------------------------------------------------------------- +// JSDataView <-> ByteData @patch extension JSDataViewToByteData on JSDataView { @patch @@ -172,7 +178,8 @@ extension ByteDataToJSDataView on ByteData { JSDataView get toJS => this as JSDataView; } -/// [JSInt8Array] <-> [Int8List] +// ----------------------------------------------------------------------------- +// JSInt8Array <-> Int8List @patch extension JSInt8ArrayToInt8List on JSInt8Array { @patch @@ -187,7 +194,8 @@ extension Int8ListToJSInt8Array on Int8List { JSInt8Array get toJS => this as JSInt8Array; } -/// [JSUint8Array] <-> [Uint8List] +// ----------------------------------------------------------------------------- +// JSUint8Array <-> Uint8List @patch extension JSUint8ArrayToUint8List on JSUint8Array { @patch @@ -202,7 +210,8 @@ extension Uint8ListToJSUint8Array on Uint8List { JSUint8Array get toJS => this as JSUint8Array; } -/// [JSUint8ClampedArray] <-> [Uint8ClampedList] +// ----------------------------------------------------------------------------- +// JSUint8ClampedArray <-> Uint8ClampedList @patch extension JSUint8ClampedArrayToUint8ClampedList on JSUint8ClampedArray { @patch @@ -217,7 +226,8 @@ extension Uint8ClampedListToJSUint8ClampedArray on Uint8ClampedList { JSUint8ClampedArray get toJS => this as JSUint8ClampedArray; } -/// [JSInt16Array] <-> [Int16List] +// ----------------------------------------------------------------------------- +// JSInt16Array <-> Int16List @patch extension JSInt16ArrayToInt16List on JSInt16Array { @patch @@ -232,7 +242,8 @@ extension Int16ListToJSInt16Array on Int16List { JSInt16Array get toJS => this as JSInt16Array; } -/// [JSUint16Array] <-> [Uint16List] +// ----------------------------------------------------------------------------- +// JSUint16Array <-> Uint16List @patch extension JSUint16ArrayToInt16List on JSUint16Array { @patch @@ -247,7 +258,8 @@ extension Uint16ListToJSInt16Array on Uint16List { JSUint16Array get toJS => this as JSUint16Array; } -/// [JSInt32Array] <-> [Int32List] +// ----------------------------------------------------------------------------- +// JSInt32Array <-> Int32List @patch extension JSInt32ArrayToInt32List on JSInt32Array { @patch @@ -262,7 +274,8 @@ extension Int32ListToJSInt32Array on Int32List { JSInt32Array get toJS => this as JSInt32Array; } -/// [JSUint32Array] <-> [Uint32List] +// ----------------------------------------------------------------------------- +// JSUint32Array <-> Uint32List @patch extension JSUint32ArrayToUint32List on JSUint32Array { @patch @@ -277,7 +290,8 @@ extension Uint32ListToJSUint32Array on Uint32List { JSUint32Array get toJS => this as JSUint32Array; } -/// [JSFloat32Array] <-> [Float32List] +// ----------------------------------------------------------------------------- +// JSFloat32Array <-> Float32List @patch extension JSFloat32ArrayToFloat32List on JSFloat32Array { @patch @@ -292,7 +306,8 @@ extension Float32ListToJSFloat32Array on Float32List { JSFloat32Array get toJS => this as JSFloat32Array; } -/// [JSFloat64Array] <-> [Float64List] +// ----------------------------------------------------------------------------- +// JSFloat64Array <-> Float64List @patch extension JSFloat64ArrayToFloat64List on JSFloat64Array { @patch @@ -307,7 +322,8 @@ extension Float64ListToJSFloat64Array on Float64List { JSFloat64Array get toJS => this as JSFloat64Array; } -/// [JSArray] <-> [List] +// ----------------------------------------------------------------------------- +// JSArray <-> List @patch extension JSArrayToList on JSArray { @patch @@ -339,7 +355,8 @@ extension ListToJSArray on List { JSArray get toJSProxyOrRef => this as JSArray; } -/// [JSNumber] -> [double] or [int]. +// ----------------------------------------------------------------------------- +// JSNumber -> double or int @patch extension JSNumberToNumber on JSNumber { @patch @@ -351,7 +368,8 @@ extension JSNumberToNumber on JSNumber { int get toDartInt => this as int; } -/// [double] -> [JSNumber]. +// ----------------------------------------------------------------------------- +// double -> JSNumber @patch extension DoubleToJSNumber on double { @patch @@ -359,7 +377,8 @@ extension DoubleToJSNumber on double { JSNumber get toJS => JSNumber._(this); } -/// [JSBoolean] <-> [bool] +// ----------------------------------------------------------------------------- +// JSBoolean <-> bool @patch extension JSBooleanToBool on JSBoolean { @patch @@ -374,7 +393,8 @@ extension BoolToJSBoolean on bool { JSBoolean get toJS => JSBoolean._(this); } -/// [JSString] <-> [String] +// ----------------------------------------------------------------------------- +// JSString <-> String @patch extension JSStringToString on JSString { @patch diff --git a/sdk/lib/_internal/js_shared/lib/js_types.dart b/sdk/lib/_internal/js_shared/lib/js_types.dart index cbc9acffcb0..2ae4df02beb 100644 --- a/sdk/lib/_internal/js_shared/lib/js_types.dart +++ b/sdk/lib/_internal/js_shared/lib/js_types.dart @@ -66,7 +66,7 @@ typedef JSBigIntRepType = interceptors.JavaScriptBigInt; // While this type is not a JS type, it is here for convenience so we don't need // to create a new shared library. -typedef ExternalDartReferenceRepType = Object; +typedef ExternalDartReferenceRepType = T; // JSVoid is just a typedef for void. typedef JSVoidRepType = void; diff --git a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart index 3a15faa3102..a5f04e259ed 100644 --- a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart +++ b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart @@ -22,7 +22,7 @@ js_types.JSObjectRepType _createObjectLiteral() => @patch JSObject get globalContext => js_util.globalThis as JSObject; -/// Helper for working with the [JSAny?] top type in a backend agnostic way. +// Helper for working with the JSAny? top type in a backend agnostic way. @patch extension NullableUndefineableJSAnyExtension on JSAny? { // TODO(joshualitt): To support incremental migration of existing users to @@ -64,14 +64,15 @@ extension JSAnyUtilityExtension on JSAny? { Object? dartify() => js_util.dartify(this); } -/// Utility extensions for [Object?]. +// Utility extensions for Object?. @patch extension NullableObjectUtilExtension on Object? { @patch JSAny? jsify() => js_util.jsify(this); } -/// [JSExportedDartFunction] <-> [Function] +// ----------------------------------------------------------------------------- +// JSExportedDartFunction <-> Function @patch extension JSExportedDartFunctionToFunction on JSExportedDartFunction { @patch @@ -93,15 +94,16 @@ extension FunctionToJSExportedDartFunction on Function { 'transformed by the interop transformer.'); } -/// Embedded global property for wrapped Dart objects passed via JS interop. -/// -/// This is a Symbol so that different Dart applications don't share Dart -/// objects from different Dart runtimes. We expect all [JSBoxedDartObject]s to -/// have this Symbol. +// Embedded global property for wrapped Dart objects passed via JS interop. +// +// This is a Symbol so that different Dart applications don't share Dart +// objects from different Dart runtimes. We expect all JSBoxedDartObjects to +// have this Symbol. final JSSymbol _jsBoxedDartObjectProperty = JSSymbol._(JSValue( js_helper.JS('() => Symbol("jsBoxedDartObjectProperty")'))); -/// [JSBoxedDartObject] <-> [Object] +// ----------------------------------------------------------------------------- +// JSBoxedDartObject <-> Object @patch extension JSBoxedDartObjectToObject on JSBoxedDartObject { @patch @@ -130,22 +132,36 @@ extension ObjectToJSBoxedDartObject on Object { } } -/// [ExternalDartReference] <-> [Object] +// ----------------------------------------------------------------------------- +// ExternalDartReference <-> Object @patch -extension ExternalDartReferenceToObject on ExternalDartReference { +extension ExternalDartReferenceToObject + on ExternalDartReference { @patch - Object get toDartObject => - jsObjectToDartObject(this._externalDartReference.toExternRef); + T get toDartObject { + // TODO(srujzs): We could do an `unsafeCast` here for performance, but + // that can result in unsoundness for users. Alternatively, we can + // introduce a generic version of `JSValue` which would allow us to safely + // `unsafeCast`. However, this has its own issues where a user can't + // do casts like `ExternalDartReference as + // ExternalDartReference` since `JSValue` is not a subtype of + // `JSValue`, even though it may be valid to do such a cast. + final t = this._externalDartReference; + return (t == null ? null : jsObjectToDartObject(t.toExternRef)) as T; + } } @patch -extension ObjectToExternalDartReference on Object { +extension ObjectToExternalDartReference on T { @patch - ExternalDartReference get toExternalReference => - ExternalDartReference._(JSValue(jsObjectFromDartObject(this))); + ExternalDartReference get toExternalReference { + final t = this; + return ExternalDartReference._( + t == null ? null : JSValue(jsObjectFromDartObject(t))); + } } -/// [JSPromise] -> [Future]. +// JSPromise -> Future @patch extension JSPromiseToFuture on JSPromise { @patch @@ -182,7 +198,8 @@ extension JSPromiseToFuture on JSPromise { } } -/// [JSArrayBuffer] <-> [ByteBuffer] +// ----------------------------------------------------------------------------- +// JSArrayBuffer <-> ByteBuffer @patch extension JSArrayBufferToByteBuffer on JSArrayBuffer { @patch @@ -202,7 +219,8 @@ extension ByteBufferToJSArrayBuffer on ByteBuffer { } } -/// [JSDataView] <-> [ByteData] +// ----------------------------------------------------------------------------- +// JSDataView <-> ByteData @patch extension JSDataViewToByteData on JSDataView { @patch @@ -220,7 +238,8 @@ extension ByteDataToJSDataView on ByteData { } } -/// [JSInt8Array] <-> [Int8List] +// ----------------------------------------------------------------------------- +// JSInt8Array <-> Int8List @patch extension JSInt8ArrayToInt8List on JSInt8Array { @patch @@ -238,7 +257,8 @@ extension Int8ListToJSInt8Array on Int8List { } } -/// [JSUint8Array] <-> [Uint8List] +// ----------------------------------------------------------------------------- +// JSUint8Array <-> Uint8List @patch extension JSUint8ArrayToUint8List on JSUint8Array { @patch @@ -256,7 +276,8 @@ extension Uint8ListToJSUint8Array on Uint8List { } } -/// [JSUint8ClampedArray] <-> [Uint8ClampedList] +// ----------------------------------------------------------------------------- +// JSUint8ClampedArray <-> Uint8ClampedList @patch extension JSUint8ClampedArrayToUint8ClampedList on JSUint8ClampedArray { @patch @@ -275,7 +296,8 @@ extension Uint8ClampedListToJSUint8ClampedArray on Uint8ClampedList { } } -/// [JSInt16Array] <-> [Int16List] +// ----------------------------------------------------------------------------- +// JSInt16Array <-> Int16List @patch extension JSInt16ArrayToInt16List on JSInt16Array { @patch @@ -293,7 +315,8 @@ extension Int16ListToJSInt16Array on Int16List { } } -/// [JSUint16Array] <-> [Uint16List] +// ----------------------------------------------------------------------------- +// JSUint16Array <-> Uint16List @patch extension JSUint16ArrayToInt16List on JSUint16Array { @patch @@ -311,7 +334,8 @@ extension Uint16ListToJSInt16Array on Uint16List { } } -/// [JSInt32Array] <-> [Int32List] +// ----------------------------------------------------------------------------- +// JSInt32Array <-> Int32List @patch extension JSInt32ArrayToInt32List on JSInt32Array { @patch @@ -329,7 +353,8 @@ extension Int32ListToJSInt32Array on Int32List { } } -/// [JSUint32Array] <-> [Uint32List] +// ----------------------------------------------------------------------------- +// JSUint32Array <-> Uint32List @patch extension JSUint32ArrayToUint32List on JSUint32Array { @patch @@ -347,7 +372,8 @@ extension Uint32ListToJSUint32Array on Uint32List { } } -/// [JSFloat32Array] <-> [Float32List] +// ----------------------------------------------------------------------------- +// JSFloat32Array <-> Float32List @patch extension JSFloat32ArrayToFloat32List on JSFloat32Array { @patch @@ -366,7 +392,8 @@ extension Float32ListToJSFloat32Array on Float32List { } } -/// [JSFloat64Array] <-> [Float64List] +// ----------------------------------------------------------------------------- +// JSFloat64Array <-> Float64List @patch extension JSFloat64ArrayToFloat64List on JSFloat64Array { @patch @@ -385,7 +412,8 @@ extension Float64ListToJSFloat64Array on Float64List { } } -/// [JSArray] <-> [List] +// ----------------------------------------------------------------------------- +// JSArray <-> List @patch extension JSArrayToList on JSArray { @patch @@ -410,7 +438,8 @@ extension ListToJSArray on List { _underlyingArray ?? _createJSProxyOfList(this); } -/// [JSNumber] -> [double] or [int]. +// ----------------------------------------------------------------------------- +// JSNumber -> double or int @patch extension JSNumberToNumber on JSNumber { @patch @@ -434,7 +463,8 @@ extension DoubleToJSNumber on double { JSNumber get toJS => JSNumber._(JSValue(toJSNumber(this))); } -/// [JSBoolean] <-> [bool] +// ----------------------------------------------------------------------------- +// JSBoolean <-> bool @patch extension JSBooleanToBool on JSBoolean { @patch @@ -447,7 +477,8 @@ extension BoolToJSBoolean on bool { JSBoolean get toJS => JSBoolean._(JSValue(toJSBoolean(this))); } -/// [JSString] <-> [String] +// ----------------------------------------------------------------------------- +// JSString <-> String @patch extension JSStringToString on JSString { @patch @@ -575,12 +606,12 @@ class _Symbol { @staticInterop class __ListBackedJSArray {} -/// Implementation of indexing, `length`, and core handler methods. -/// -/// JavaScript's `Array` methods are similar to Dart's `ListMixin`, because they -/// only rely on the implementation of `length` and indexing methods (and -/// support for any JS operators like `in` or `delete`). -/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#generic_array_methods +// Implementation of indexing, `length`, and core handler methods. +// +// JavaScript's `Array` methods are similar to Dart's `ListMixin`, because they +// only rely on the implementation of `length` and indexing methods (and +// support for any JS operators like `in` or `delete`). +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#generic_array_methods class _ListBackedJSArray { final List _list; // The proxy that wraps this list. diff --git a/sdk/lib/_internal/wasm/lib/js_types.dart b/sdk/lib/_internal/wasm/lib/js_types.dart index a22ba459d3d..223306c6d62 100644 --- a/sdk/lib/_internal/wasm/lib/js_types.dart +++ b/sdk/lib/_internal/wasm/lib/js_types.dart @@ -83,7 +83,7 @@ typedef JSBigIntRepType = js.JSValue; // While this type is not a JS type, it is here for convenience so we don't need // to create a new shared library. -typedef ExternalDartReferenceRepType = js.JSValue; +typedef ExternalDartReferenceRepType = js.JSValue?; // JSVoid is just a typedef for void. While we could just use JSUndefined, in // the future we may be able to use this to elide `return`s in JS trampolines. diff --git a/sdk/lib/_internal/wasm/lib/weak_patch.dart b/sdk/lib/_internal/wasm/lib/weak_patch.dart index 2a65b072548..a26200ade6e 100644 --- a/sdk/lib/_internal/wasm/lib/weak_patch.dart +++ b/sdk/lib/_internal/wasm/lib/weak_patch.dart @@ -56,9 +56,9 @@ external JSFunction? _jsWeakRefFunction; final bool _supportsWeakRef = _jsWeakRefFunction != null; @js_interop.JS('WeakRef') -extension type _JSWeakRef._(JSObject _) implements JSObject { - external _JSWeakRef(ExternalDartReference target); - external ExternalDartReference? deref(); +extension type _JSWeakRef._(JSObject _) implements JSObject { + external _JSWeakRef(ExternalDartReference target); + external ExternalDartReference? deref(); } @patch @@ -77,10 +77,10 @@ class WeakReference { } class _WeakReferenceWrapper implements WeakReference { - final _JSWeakRef _jsWeakRef; + final _JSWeakRef _jsWeakRef; _WeakReferenceWrapper(T target) : _jsWeakRef = _JSWeakRef(target.toExternalReference); - T? get target => _jsWeakRef.deref()?.toDartObject as T?; + T? get target => _jsWeakRef.deref()?.toDartObject; } class _WeakReferencePolyfill implements WeakReference { @@ -93,14 +93,14 @@ external JSFunction? _jsFinalizationRegistry; final bool _supportsFinalizationRegistry = _jsFinalizationRegistry != null; @js_interop.JS('FinalizationRegistry') -extension type _JSFinalizationRegistry._(JSObject _) implements JSObject { +extension type _JSFinalizationRegistry._(JSObject _) implements JSObject { external _JSFinalizationRegistry(JSFunction callback); @js_interop.JS('register') - external void registerWithDetach(ExternalDartReference value, - ExternalDartReference? peer, ExternalDartReference detach); + external void registerWithDetach(ExternalDartReference value, + ExternalDartReference peer, ExternalDartReference detach); external void register( - ExternalDartReference value, ExternalDartReference? peer); - external void unregister(ExternalDartReference detach); + ExternalDartReference value, ExternalDartReference peer); + external void unregister(ExternalDartReference detach); } @patch @@ -134,8 +134,8 @@ class _FinalizationRegistryWrapper implements Finalizer { _FinalizationRegistryWrapper(void Function(T) callback) : _jsFinalizationRegistry = - _JSFinalizationRegistry(((ExternalDartReference? peer) { - callback(unsafeCast(peer?.toDartObject)); + _JSFinalizationRegistry(((ExternalDartReference peer) { + callback(peer.toDartObject); }).toJS); void attach(Object value, T peer, {Object? detach}) { @@ -143,10 +143,10 @@ class _FinalizationRegistryWrapper implements Finalizer { if (detach != null) { _checkValidWeakTarget(detach); _jsFinalizationRegistry.registerWithDetach(value.toExternalReference, - peer?.toExternalReference, detach.toExternalReference); + peer.toExternalReference, detach.toExternalReference); } else { _jsFinalizationRegistry.register( - value.toExternalReference, peer?.toExternalReference); + value.toExternalReference, peer.toExternalReference); } } diff --git a/sdk/lib/js_interop/js_interop.dart b/sdk/lib/js_interop/js_interop.dart index cf6142716de..eb1b51b2822 100644 --- a/sdk/lib/js_interop/js_interop.dart +++ b/sdk/lib/js_interop/js_interop.dart @@ -274,6 +274,8 @@ extension type JSBigInt._(JSBigIntRepType _jsBigInt) implements JSAny {} /// used directly without any conversions. When compiling to Wasm, an internal /// Wasm function is used to convert the Dart object to an opaque JavaScript /// value, which can later be converted back using another internal function. +/// The underlying representation type is nullable, meaning a non-nullable +/// [ExternalDartReference] may be `null`. /// /// This interface is a faster alternative to [JSBoxedDartObject] by not /// wrapping the Dart object with a JavaScript object. However, unlike @@ -290,9 +292,9 @@ extension type JSBigInt._(JSBigIntRepType _jsBigInt) implements JSAny {} /// [ExternalDartReference]. /// /// See [ObjectToExternalDartReference.toExternalReference] to allow an -/// arbitrary [Object] to be passed to JavaScript. -extension type ExternalDartReference._( - ExternalDartReferenceRepType _externalDartReference) implements Object {} +/// arbitrary value of type [T] to be passed to JavaScript. +extension type ExternalDartReference._( + ExternalDartReferenceRepType _externalDartReference) {} /// JS type equivalent for `undefined` for interop member return types. /// @@ -380,8 +382,8 @@ extension JSAnyUtilityExtension on JSAny? { /// Whether this [JSAny]? is an instance of the JavaScript type /// that is declared by [T]. /// - /// This method uses a combination of null, `typeof`, and `instanceof` checks - /// in order to do this check. Use this instead of `is` checks. + /// This method uses a combination of `null`, `typeof`, and `instanceof` + /// checks in order to do this check. Use this instead of `is` checks. /// /// If [T] is a primitive JS type like [JSString], this uses a `typeof` check /// that corresponds to that primitive type like `typeofEquals('string')`. @@ -405,7 +407,7 @@ extension JSAnyUtilityExtension on JSAny? { /// `JSTypedArray`. As `TypedArray` does not exist as a property in /// JavaScript, this does some prototype checking to make `isA` /// do the right thing. The other exception is `JSAny`. If you do a - /// `isA` check, it will only do a null-check. + /// `isA` check, it will only do a `null` check. /// /// Using this method with a [T] that has an object literal constructor will /// result in an error as you likely want to use [JSObject] instead. @@ -517,25 +519,27 @@ extension ObjectToJSBoxedDartObject on Object { external JSBoxedDartObject get toJSBox; } -/// Conversions from [ExternalDartReference] to [Object]. -extension ExternalDartReferenceToObject on ExternalDartReference { - /// The Dart [Object] that this [ExternalDartReference] refers to. +/// Conversions from [ExternalDartReference] to the value of type [T]. +extension ExternalDartReferenceToObject + on ExternalDartReference { + /// The Dart value of type [T] that this [ExternalDartReference] refers to. /// /// When compiling to JavaScript, a Dart object is a JavaScript object, and /// therefore this directly returns the Dart object. When compiling to Wasm, /// an internal Wasm function is used to convert the opaque JavaScript value /// to the original Dart object. - external Object get toDartObject; + external T get toDartObject; } -/// Conversions from [Object] to [ExternalDartReference]. -extension ObjectToExternalDartReference on Object { - /// An opaque reference to this [Object] which can be passed to JavaScript. +/// Conversions from a value of type [T] to [ExternalDartReference]. +extension ObjectToExternalDartReference on T { + /// An opaque reference to this value of type [T] which can be passed to + /// JavaScript. /// /// When compiling to JavaScript, a Dart object is a JavaScript object, and /// therefore this directly returns the Dart object. When compiling to Wasm, /// an internal Wasm function is used to convert the Dart object to an opaque - /// JavaScript value. + /// JavaScript value. If this value is `null`, returns `null`. /// /// A value of type [ExternalDartReference] should be treated as completely /// opaque. It can only be passed around as-is or converted back using @@ -546,7 +550,7 @@ extension ObjectToExternalDartReference on Object { /// guaranteed to be equal. Therefore, `==` will always return true between /// such [ExternalDartReference]s. However, like JS types, `identical` between /// such values may return different results depending on the compiler. - external ExternalDartReference get toExternalReference; + external ExternalDartReference get toExternalReference; } /// Conversions from [JSPromise] to [Future]. diff --git a/tests/lib/js/static_interop_test/extension_type/non_interop_extension_type_static_test.dart b/tests/lib/js/static_interop_test/extension_type/non_interop_extension_type_static_test.dart index dba1e32efd8..89b344a6f0c 100644 --- a/tests/lib/js/static_interop_test/extension_type/non_interop_extension_type_static_test.dart +++ b/tests/lib/js/static_interop_test/extension_type/non_interop_extension_type_static_test.dart @@ -151,16 +151,16 @@ extension on ENonInterop { // [web] JS interop type or @Native type from an SDK web library required for 'external' extension members. } -extension type EExternalDartReference._(ExternalDartReference _) { +extension type EExternalDartReference._(ExternalDartReference _) { external EExternalDartReference(); // ^ // [web] Extension type member is marked 'external', but the representation type of its extension type is not a valid JS interop type. } @JS() -extension type EExternalDartReference2._(ExternalDartReference _) {} +extension type EExternalDartReference2._(ExternalDartReference _) {} // ^ -// [web] Extension type 'EExternalDartReference2' is marked with a '@JS' annotation, but its representation type is not a valid JS interop type: 'ExternalDartReference'. +// [web] Extension type 'EExternalDartReference2' is marked with a '@JS' annotation, but its representation type is not a valid JS interop type: 'ExternalDartReference'. extension on EExternalDartReference { external int field; diff --git a/tests/lib/js/static_interop_test/external_dart_reference_test.dart b/tests/lib/js/static_interop_test/external_dart_reference_test.dart index ad375ceaba0..33108e4179d 100644 --- a/tests/lib/js/static_interop_test/external_dart_reference_test.dart +++ b/tests/lib/js/static_interop_test/external_dart_reference_test.dart @@ -7,30 +7,34 @@ import 'dart:js_interop'; import 'package:expect/expect.dart'; +import 'package:expect/variations.dart'; -const isJSBackend = const bool.fromEnvironment('dart.library.html'); +final bool isJSBackend = 1 is ExternalDartReference; -extension type EExternalDartReference(ExternalDartReference _) - implements ExternalDartReference {} +extension type EExternalDartReference(ExternalDartReference _) + implements ExternalDartReference {} @JS() -external ExternalDartReference externalDartReference; +external ExternalDartReference externalDartReference; @JS() -external EExternalDartReference eExternalDartReference; +external EExternalDartReference eExternalDartReference; @JS('externalDartReference') -external ExternalDartReference? nullableExternalDartReference; +external ExternalDartReference externalDartNullableReference; + +@JS('externalDartReference') +external ExternalDartReference? nullableExternalDartReference; // Use a function so that we can use a type parameter that extends an // `ExternalDartReference` type. @JS('identity') external set _identity(JSFunction _); @JS() -external T identity(T t); +external T identity(T t); extension type ObjectLiteral(JSObject _) { - external void operator []=(String key, ExternalDartReference value); + external void operator []=(String key, ExternalDartReference value); } class DartClass { @@ -39,23 +43,37 @@ class DartClass { DartClass(this.field); } -void main() { - final dartObject = DartClass(42); +class DartSubclass extends DartClass { + DartSubclass(super.field); +} +void generalTest() { + var dartObject = DartClass(42); + + // `Object` test. externalDartReference = dartObject.toExternalReference; Expect.equals(dartObject, externalDartReference.toDartObject as DartClass); - Expect.isTrue( - identical(dartObject, externalDartReference.toDartObject as DartClass)); - eExternalDartReference = EExternalDartReference(externalDartReference); - Expect.equals(dartObject, eExternalDartReference.toDartObject as DartClass); - Expect.isTrue( - identical(dartObject, eExternalDartReference.toDartObject as DartClass)); - _identity = ((ExternalDartReference e) => e).toJS; + Expect.identical(dartObject, externalDartReference.toDartObject as DartClass); + + // Generic test. + var externalDartClassReference = dartObject.toExternalReference; + Expect.equals(dartObject, externalDartClassReference.toDartObject); + Expect.identical(dartObject, externalDartClassReference.toDartObject); + // Ensure we get assignability. + dartObject = externalDartClassReference.toDartObject; + + // Check that we do the right thing with extension types on + // `ExternalDartReference`. + eExternalDartReference = EExternalDartReference(externalDartClassReference); + Expect.equals(dartObject, eExternalDartReference.toDartObject); + Expect.identical(dartObject, eExternalDartReference.toDartObject); + + // Check that `ExternalDartReference` can be used as a parameter and return + // type for `Function.toJS`'d functions. + _identity = ((ExternalDartReference e) => e).toJS; final externalDartReferenceTypeParam = identity(eExternalDartReference); - Expect.equals( - dartObject, externalDartReferenceTypeParam.toDartObject as DartClass); - Expect.isTrue(identical( - dartObject, externalDartReferenceTypeParam.toDartObject as DartClass)); + Expect.equals(dartObject, externalDartReferenceTypeParam.toDartObject); + Expect.identical(dartObject, externalDartReferenceTypeParam.toDartObject); // Multiple invocations should return the same underlying value, which is // tested by `==`. @@ -63,27 +81,111 @@ void main() { // However, they may or may not be identical depending on the compiler due to // dart2wasm wrapping values with new JSValue instances. if (isJSBackend) { - Expect.isTrue( - identical(externalDartReference, dartObject.toExternalReference)); + Expect.identical(externalDartReference, dartObject.toExternalReference); } else { Expect.isFalse( identical(externalDartReference, dartObject.toExternalReference)); } - final jsString = ''.toJS; // We don't validate that the input is a Dart object or a JS value as that may // be expensive to validate. We end up externalizing the JSValue wrapper in // this case. + final jsString = ''.toJS; Expect.equals(jsString.toExternalReference.toDartObject, jsString); - Expect.isTrue(identical(jsString.toExternalReference.toDartObject, jsString)); + Expect.identical(jsString.toExternalReference.toDartObject, jsString); - // Check that we do the right thing with nullability still. - nullableExternalDartReference = null; - if (hasSoundNullSafety) Expect.throws(() => externalDartReference); + // Check that the type is checked when internalized for soundness. + externalDartReference = dartObject.toExternalReference; + Expect.throws( + () => (externalDartReference as ExternalDartReference) + // The cast is deferred until `toDartObject` for dart2wasm, so call it + // explicitly. + .toDartObject); + + _identity = ((ExternalDartReference et) => + et.toDartObject.toExternalReference).toJS; + Expect.throws(() => identity(externalDartReference)); + + // Check that we do the right thing with nullability still, both in the type + // parameter and outside it. + _identity = ((ExternalDartReference et) => + et.toDartObject.toExternalReference).toJS; + nullableExternalDartReference = null?.toExternalReference; + Expect.isTrue(nullableExternalDartReference == null); + Expect.throwsWhen( + !unsoundNullSafety, () => externalDartReference.toDartObject); + Expect.throwsWhen( + !unsoundNullSafety, () => identity(nullableExternalDartReference)); + externalDartNullableReference = null.toExternalReference; + Expect.isTrue(externalDartNullableReference == null); + Expect.throwsWhen( + !unsoundNullSafety, () => externalDartReference.toDartObject); + Expect.throwsWhen( + !unsoundNullSafety, () => identity(externalDartNullableReference)); + // Check that they're both Dart `null`. + Expect.identical( + nullableExternalDartReference, externalDartNullableReference); // Functions should not trigger `assertInterop`. externalDartReference = () {}.toExternalReference; identity(EExternalDartReference(() {}.toExternalReference)); - final literal = ObjectLiteral(JSObject()); - literal['ref'] = externalDartReference; + ObjectLiteral(JSObject())['ref'] = externalDartReference; +} + +// An example interface for a generic `WritableSignal` from +// https://angular.dev/guide/signals that avoids unnecessary casts and wrapper +// functions in the JS compilers. The functions are stubbed to just test casts +// and assignability. +extension type WritableSignal(JSFunction _) { + void _set(ExternalDartReference value) {} + + void set(T value) => _set(value.toExternalReference); + + void _update(JSExportedDartFunction update) {} + + void update(T Function(T) function) { + // Because `ExternalDartReference`s are `T` on the JS backends, we can + // avoid the wrapper function that is needed for dart2wasm. If we want code + // that is guaranteed to work on all backends, the wrapper function will + // work, but will be slower on the JS backends. See + // https://github.com/dart-lang/sdk/issues/55342 for more details. + if (isJSBackend) { + _update((function as ExternalDartReference Function( + ExternalDartReference)) + .toJS); + } + // Should work on all backends. + _update(((ExternalDartReference e) => + function(e.toDartObject).toExternalReference).toJS); + } +} + +WritableSignal signal(T initialValue) => + WritableSignal((() => initialValue.toExternalReference).toJS); + +void signalsTest() { + final writableSignal = signal(0); + writableSignal.set(null); + writableSignal.set(true); + writableSignal.set(DartClass(42)); + writableSignal.update((Object? x) => x); + writableSignal.update((_) => false); + writableSignal.update((Object? _) => null); + final writableIntSignal = signal(null as int?); + writableIntSignal.set(null); + writableIntSignal.set(0); + writableIntSignal.update((int? x) => x); + writableIntSignal.update((int? _) => null); + writableIntSignal.update((num? _) => 0); + final writableDartSignal = signal(DartClass(42)); + writableDartSignal.set(null); + writableDartSignal.set(DartSubclass(42)); + writableDartSignal.update((DartClass? x) => x); + writableDartSignal.update((DartClass? x) => x as DartSubclass); + writableDartSignal.update((Object? _) => DartClass(42)); +} + +void main() { + generalTest(); + signalsTest(); }