Open sigmundch opened 2 years ago
/cc @srujzs @rileyporter @joshualitt - feel free to edit the list above
There have been a lot of changes here since the initial post, namely adding support for dart2wasm JS interop, JS types, and interop with extension types. JS types refer to the set of types we expose through dart:js_interop
that are prefixed with JS
e.g. JSArray
. They are zero-cost wrappers of JS values. This allows us to have a stronger type system for JS values while maintaining compatibility with dart2wasm. external
members should only use these types with the exception of primitives (where they're auto-converted for ergonomics). Note that these types have a @staticInterop
annotation but we do a custom erasure, unlike user @staticInterop
types.
There will be docs added to dart.dev with all these changes in the near future, but for those working with next-gen interop/interested in migration, there are a few key discrepancies that we're aware of that we want to highlight with dart:js_interop
and dart:js_interop_unsafe
:
- Runtime type differences of JS types
On the JS compilers, JS types generally have a more specific Dart type that they are intercepted to. JSNumber
is num
, JSArray
is a List
, etc. On dart2wasm, all JS types are erased to JSValue
, essentially an external reference to some JS value. Therefore casts may succeed on dart2wasm but fail on the JS compilers. For example, casting a JSString
to a JSNumber
will only fail in the JS compilers. Of course, attempting to then use methods that only apply to JS numbers will fail in dart2wasm, as the underlying value is still a JS string.
- Conversions between List
and JSArray
The default List
implementation in the JS compilers is backed by a JS Array
. This is not the case in dart2wasm. Therefore, conversions to an Array
are a copy currently on dart2wasm, while the JS compilers pass by reference. This means modifying the list/array would not modify the array/list, respectively.
There is a toJSProxyOrRef
method that makes it easier for modifications to persist. In the JS compilers, this is a cast like before. On dart2wasm, we create a JS Proxy
object to wrap the given list.
Conversions from an Array
to a List
are fine, however. In the JS compilers, this is again pass by reference, and in the dart2wasm case, we wrap the array with a list implementation that forwards to the array.
Therefore, if you know you’re going to need to eventually pass the list to JS, and you care about modifications persisting, instantiate a JSArray
. Use toDart
if you need it to be a list-like interface, we just remove the wrapper when you later call toJS
.
- Conversions for typed_data
dart:typed_data
types have the same issues with regards to modification as List
and Array
. toJS
will pass the typed array by reference in the JS compilers, while we need to do a copy in dart2wasm.
Unlike List
, however, we can’t proxy Dart types with a JS type, as JS expects ArrayBuffer
to point to the JS heap, but dart2wasm dart:typed_data
types point to the Wasm heap. There have been proposals to make dart2wasm use JS typed arrays/shared memory, but they all come with significant downsides/hurdles.
Like List
, conversions to dart:typed_data
from the respective JS type e.g. JSArrayBuffer
-> ByteBuffer
will introduce a wrapper.
Therefore, if you care about modifications persisting, instantiate a JS typed array. Use toDart
if you need it to be a dart:typed_data
interface as all we do is add a wrapper around the JS typed array (and later remove it when calling toJS
).
- int
/double
and number semantics
Native Dart code and Web Dart code differ in number semantics today: https://dart.dev/guides/language/numbers#differences-in-behavior. dart2wasm will conform to the Native semantics (even though it says "Web" 😄): int
and double
are subtypes of num
, but int
is not a subtype of double
. In practice, this should rarely make a difference with interop. If you write int
as a return value of an external
function or as a parameter value in a JSFunction
, we will attempt to convert the double
value to an int
.
null
/undefined
.In the JS compilers, JS null
and JS undefined
are treated as null
in Dart, but don’t go through a conversion. So if you get undefined
from JS, == null
returns true, but the underlying object is still undefined
.
This is not the case in dart2wasm. dart2wasm’s null
is not a JS value, and therefore we’re forced to make a conversion when JS’ null
or undefined
flow to Dart. Therefore, dart2wasm can’t differentiate between these two values, while dart2js and DDC technically can.
Avoid code where this matters for now. In the future, we might provide an annotation that you can use on external
members/callbacks to internalize JS’ undefined
as a separate JS type.
In general, stick with dart:js_interop
and dart:js_interop_unsafe
to make sure you’re compatible with dart2wasm. Avoid package:js
(@staticInterop
is available through dart:js_interop
) and dart:js_util
, as some of the functionality may be buggy/expensive.
Also, avoid casts in favor of conversion functions. As mentioned above, casts may succeed on one backend but fail on another. Similarly is
checks don't work as you might expect them to. If you need to differentiate between two JS objects, use instanceof
.
There may be missing members that we’ll add to the JS types, but you can add missing members either through your own interop interface or through an extension. You can define an interop interface using an extension type (with a JS type representation type) or through @staticInterop
. Prefer the former going forwards.
Will dart:js_interop
also provide functions like add
, sub
, isTrue
, ... ? Or it's safe to use functions from dart:js_util
with dart:js_interop
types.
That's the plan! The goal is to lock down js_util
and package:js
so they can't be used in dart2wasm. As part of this, we should reach parity before we can do that. Operators should be allowed through external operators instead of methods, and the rest will be reexported.
This is a meta bug to track all of our efforts in implementing and releasing the next generation of JSInterop based on extension types.
We are also using https://github.com/orgs/dart-lang/projects/65/views/1 to track some items with more agility. We'll try to periodically update this issue as well.
JS interop must have:
dart:js_interop
library@staticInterop
andextension type
js_util
style methods indart:js_interop
anddart:js_interop_unsafe
@JS
annotations and@staticInterop
indart:js_interop
dart:js_interop
Package web must have:
extension types
Ecosystem:
dart:js_interop
andpackage:web
Desirables that can be shipped later:
@JS
annotation where needed, addimplements JSObject
)is JsFoo
)@staticInterop
to the new language syntaxdart:js
todart:js_util
package:js
JSInterop to the new styleOther issues to add above: