dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.28k stars 1.58k forks source link

[jsinterop] Complete static JSInterop implementation #49353

Open sigmundch opened 2 years ago

sigmundch commented 2 years ago

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:

Package web must have:

Ecosystem:

Desirables that can be shipped later:

Other issues to add above:

sigmundch commented 2 years ago

/cc @srujzs @rileyporter @joshualitt - feel free to edit the list above

srujzs commented 1 year ago

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.

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.

ykmnkmi commented 12 months ago

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.

srujzs commented 12 months ago

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.