c6556dc1a3
Change-Id: I77a01469113cdfe6c9c3533cfb398ab97d5cd3b0 Reviewed-on: https://dart-review.googlesource.com/c/89404 Reviewed-by: Vijay Menon <vsm@google.com> Commit-Queue: Jenny Messerly <jmesserly@google.com>
1637 lines
54 KiB
Markdown
1637 lines
54 KiB
Markdown
# Better JavaScript and Dart interoperability
|
|
|
|
Table of contents:
|
|
|
|
<!-- toc -->
|
|
|
|
- [Motivation](#motivation)
|
|
- [Typed APIs](#typed-apis)
|
|
* [Automatic Typed API Generation](#automatic-typed-api-generation)
|
|
* [Static Dispatch](#static-dispatch)
|
|
+ [Extension Fields](#extension-fields)
|
|
* [JS Interop Checked Mode](#js-interop-checked-mode)
|
|
* [Data Type Interoperability](#data-type-interoperability)
|
|
* [Implicit and Explicit Conversions](#implicit-and-explicit-conversions)
|
|
- [Functions](#functions)
|
|
* [Optional Arguments](#optional-arguments)
|
|
* [Named Arguments](#named-arguments)
|
|
* [Overloads](#overloads)
|
|
* [This Argument](#this-argument)
|
|
* [Method Tearoffs](#method-tearoffs)
|
|
* [Generic Type Parameters](#generic-type-parameters)
|
|
- [Data Types Conversions](#data-types-conversions)
|
|
* [Generic Types, List and JS Array](#generic-types-list-and-js-array)
|
|
* [Null and Undefined](#null-and-undefined)
|
|
* [Future and JS Promise](#future-and-js-promise)
|
|
* [Iterable and JS Iterable](#iterable-and-js-iterable)
|
|
* [Stream and JS Async Iterable](#stream-and-js-async-iterable)
|
|
* [Stream and Callbacks](#stream-and-callbacks)
|
|
* [DateTime and JS Date](#datetime-and-js-date)
|
|
* [Map/Set and JS Map/Set](#mapset-and-js-mapset)
|
|
* [Maps and JS Objects](#maps-and-js-objects)
|
|
+ [Wrapper-based JSObjectMap](#wrapper-based-jsobjectmap)
|
|
+ [Autoboxing JSObjectMap](#autoboxing-jsobjectmap)
|
|
- [Less Static Interop](#less-static-interop)
|
|
* [Virtual Dispatch](#virtual-dispatch)
|
|
* [Interface and Dynamic dispatch](#interface-and-dynamic-dispatch)
|
|
* [JS Proxy](#js-proxy)
|
|
* [JS Reflection](#js-reflection)
|
|
- [Exporting Dart to JS](#exporting-dart-to-js)
|
|
* [Exporting classes and libraries](#exporting-classes-and-libraries)
|
|
* [Inheritance between Dart and JS](#inheritance-between-dart-and-js)
|
|
* [Interop with JS Modules](#interop-with-js-modules)
|
|
* [Exposing JS Methods as Getters](#exposing-js-methods-as-getters)
|
|
- [JS Types in the Dart Type System](#js-types-in-the-dart-type-system)
|
|
- [Implementation Roadmap](#implementation-roadmap)
|
|
- [Compatibility and Evolution](#compatibility-and-evolution)
|
|
- [FAQ](#faq)
|
|
* [Q: How does JSON work?](#q-how-does-json-work)
|
|
* [Q: Would new Dart language features help?](#q-would-new-dart-language-features-help)
|
|
* [Q: Can dart2js and dartdevc share implementation for JS interop?](#q-can-dart2js-and-dartdevc-share-implementation-for-js-interop)
|
|
* [Q: If a JS API returns "Object" does this break dart2js tree shaking?](#q-if-a-js-api-returns-object-does-this-break-dart2js-tree-shaking)
|
|
* [Q: Could we use dynamic interop instead?](#q-could-we-use-dynamic-interop-instead)
|
|
|
|
<!-- tocstop -->
|
|
|
|
## Motivation
|
|
|
|
Better interoperability with JavaScript is one of the most highly requested
|
|
features for Dart. Previous work has made JS interop possible, but gaps remain.
|
|
This proposal outlines a series of usability improvements that, taken together,
|
|
make it much easier to use JS from Dart and vice versa.
|
|
|
|
Here's an example of JS interop is like today, from the [Firebase package](https://pub.dartlang.org/packages/firebase):
|
|
```dart
|
|
import 'interop/app_interop.dart';
|
|
|
|
/// A Firebase App holds the initialization information for a collection
|
|
/// of services.
|
|
///
|
|
/// See: <https://firebase.google.com/docs/reference/js/firebase.app>.
|
|
class App extends JsObjectWrapper<AppJsImpl> {
|
|
// [ed: doc comments removed to condense code]
|
|
static final _expando = Expando<App>();
|
|
|
|
String get name => jsObject.name;
|
|
FirebaseOptions get options => jsObject.options;
|
|
|
|
static App getInstance(AppJsImpl jsObject) {
|
|
if (jsObject == null) {
|
|
return null;
|
|
}
|
|
return _expando[jsObject] ??= App._fromJsObject(jsObject);
|
|
}
|
|
|
|
App._fromJsObject(AppJsImpl jsObject) : super.fromJsObject(jsObject);
|
|
|
|
Auth auth() => Auth.getInstance(jsObject.auth());
|
|
Database database() => Database.getInstance(jsObject.database());
|
|
Future delete() => handleThenable(jsObject.delete());
|
|
Firestore firestore() => Firestore.getInstance(jsObject.firestore());
|
|
|
|
Storage storage([String url]) {
|
|
var jsObjectStorage =
|
|
(url != null) ? jsObject.storage(url) : jsObject.storage();
|
|
return Storage.getInstance(jsObjectStorage);
|
|
}
|
|
}
|
|
|
|
// [ed: in interop/app_interop.dart]
|
|
|
|
@JS('App')
|
|
abstract class AppJsImpl {
|
|
external String get name;
|
|
external FirebaseOptions get options;
|
|
external AuthJsImpl auth();
|
|
external DatabaseJsImpl database();
|
|
external PromiseJsImpl delete();
|
|
external StorageJsImpl storage([String url]);
|
|
external FirestoreJsImpl firestore();
|
|
}
|
|
```
|
|
|
|
Nearly all of the Firebase classes are wrapped like that. It's a lot of work and
|
|
boilerplate to implement wrappers like this, and it adds overhead to every
|
|
operation.
|
|
|
|
What we'd like to write is more like this:
|
|
```dart
|
|
@JS()
|
|
class App {
|
|
external String get name;
|
|
external FirebaseOptions get options;
|
|
|
|
// Wrappers no longer necessary for these classes.
|
|
external Auth auth();
|
|
external Database database();
|
|
external Firestore firestore();
|
|
|
|
// Promise -> Future is handled automatically
|
|
external Future delete();
|
|
|
|
// Optional arguments are handled correctly by the compiler.
|
|
// (In advanced cases, there are ways to declare overloads.)
|
|
external Storage storage([String url]);
|
|
}
|
|
```
|
|
|
|
The [Google Maps API](https://github.com/a14n/dart-google-maps) package is
|
|
similar, but generated using the [js_wrapping](https://github.com/a14n/dart-js-wrapping)
|
|
code generator:
|
|
|
|
```dart
|
|
@GeneratedFrom(_LatLngBounds)
|
|
@JsName('google.maps.LatLngBounds')
|
|
class LatLngBounds extends JsInterface {
|
|
LatLngBounds([LatLng sw, LatLng ne])
|
|
: this.created(JsObject(context['google']['maps']['LatLngBounds'],
|
|
[__codec0.encode(sw), __codec0.encode(ne)]));
|
|
LatLngBounds.created(JsObject o) : super.created(o);
|
|
|
|
bool contains(LatLng latLng) =>
|
|
asJsObject(this).callMethod('contains', [__codec0.encode(latLng)]);
|
|
bool equals(LatLngBounds other) =>
|
|
asJsObject(this).callMethod('equals', [__codec1.encode(other)]);
|
|
LatLngBounds extend(LatLng point) => __codec1
|
|
.decode(asJsObject(this).callMethod('extend', [__codec0.encode(point)]));
|
|
LatLng get center => _getCenter();
|
|
LatLng _getCenter() =>
|
|
__codec0.decode(asJsObject(this).callMethod('getCenter'));
|
|
LatLng get northEast => _getNorthEast();
|
|
LatLng _getNorthEast() =>
|
|
__codec0.decode(asJsObject(this).callMethod('getNorthEast'));
|
|
LatLng get southWest => _getSouthWest();
|
|
LatLng _getSouthWest() =>
|
|
__codec0.decode(asJsObject(this).callMethod('getSouthWest'));
|
|
bool intersects(LatLngBounds other) =>
|
|
asJsObject(this).callMethod('intersects', [__codec1.encode(other)]);
|
|
bool get isEmpty => _isEmpty();
|
|
bool _isEmpty() => asJsObject(this).callMethod('isEmpty');
|
|
LatLng toSpan() => __codec0.decode(asJsObject(this).callMethod('toSpan'));
|
|
String toString() => asJsObject(this).callMethod('toString');
|
|
String toUrlValue([num precision]) =>
|
|
asJsObject(this).callMethod('toUrlValue', [precision]);
|
|
LatLngBounds union(LatLngBounds other) => __codec1
|
|
.decode(asJsObject(this).callMethod('union', [__codec1.encode(other)]));
|
|
}
|
|
```
|
|
|
|
What we'd like to write instead:
|
|
```dart
|
|
@JS('google.maps.LatLngBounds')
|
|
class LatLngBounds {
|
|
external LatLngBounds([LatLng sw, LatLng ne]);
|
|
|
|
external bool contains(LatLng latLng);
|
|
external bool equals(LatLngBounds other);
|
|
external LatLngBounds extend(LatLng point);
|
|
external bool intersects(LatLngBounds other);
|
|
external LatLng toSpan();
|
|
external String toString();
|
|
external String toUrlValue([num precision]);
|
|
external LatLngBounds union(LatLngBounds other);
|
|
|
|
@sealed
|
|
LatLng get center => _getCenter();
|
|
@sealed
|
|
LatLng get northEast => _getNorthEast();
|
|
@sealed
|
|
LatLng get southWest => _getSouthWest();
|
|
@sealed
|
|
bool get isEmpty => _isEmpty();
|
|
|
|
@JS('getCenter')
|
|
external LatLng _getCenter();
|
|
@JS('getNorthEast')
|
|
external LatLng _getNorthEast();
|
|
@JS('getSouthWest')
|
|
external LatLng _getSouthWest();
|
|
@JS('isEmpty')
|
|
external bool _isEmpty();
|
|
}
|
|
```
|
|
|
|
The rest of the proposal outlines how we get there, as well as many other
|
|
usability and correctness improvements.
|
|
|
|
|
|
## Typed APIs
|
|
|
|
The key principle behind Dart and JS interop is declaring API signatures,
|
|
as illustrated above. The two main pieces of this are `@JS()` annotations and
|
|
the `external` keyword:
|
|
|
|
```dart
|
|
@JS('firebase.app')
|
|
library firebase.app;
|
|
|
|
import 'package:js/js.dart' show JS;
|
|
|
|
@JS('App')
|
|
class App {
|
|
external String get name;
|
|
// [...]
|
|
}
|
|
```
|
|
|
|
The `external` keyword marks a class member or library member as representing a
|
|
JavaScript API. It allows the body of the API to be omitted, and is only valid
|
|
on a declaration that is in a `@JS()` context (either it is marked with `@JS()`
|
|
itself, or is within a library/class that is marked).
|
|
|
|
The `@JS()` annotation marks a declaration (library, class, library member or
|
|
class member) as representing a JavaScript API. An optional string can be
|
|
provided, to specify the name of the declaration in JS. This allows APIs to be
|
|
renamed. For example:
|
|
|
|
```dart
|
|
@JS()
|
|
@anonymous
|
|
class UserInfo {
|
|
// [...]
|
|
|
|
/// Returns a JSON-serializable representation of this object.
|
|
@JS('toJSON')
|
|
@JSConvert(dartify)
|
|
external Map<String, dynamic> toJson();
|
|
}
|
|
```
|
|
|
|
In the example above `toJSON()` was named `toJson()` to match Dart naming
|
|
conventions. (This also implicitly marks it as `@sealed`.)
|
|
|
|
Typed APIs provide many benefits:
|
|
- They assist discovery and use of the API, via autocompletion in the editor.
|
|
- They provide static type checking to help use the API correctly.
|
|
- They're the foundation of automatic data type conversions, and adding Dart
|
|
members to JS interop classes, to provide a more Dart-like API when desired.
|
|
|
|
|
|
### Automatic Typed API Generation
|
|
|
|
Many JavaScript APIs have types now, either from TypeScript or Closure Compiler
|
|
comments. We'd like to have a tool to automatically generate
|
|
Dart interop APIs from those. The resulting file can then be hand edited, if
|
|
desired, to further customize the interop, or used immediately.
|
|
|
|
|
|
### Static Dispatch
|
|
|
|
The example above renames *instance members*, allowing the API to be made
|
|
more Dart-friendly. This feature is crucial for interoperability, as the
|
|
JavaScript API may have a name that is not legal in Dart, or its name may have
|
|
different visibility, such as names starting with underscore.
|
|
|
|
Renames are made possible by taking advantage of the static type of the
|
|
reciever. This is similar to static calls and extension methods.
|
|
|
|
```dart
|
|
UserInfo user = someExpression;
|
|
print(user.toJson());
|
|
```
|
|
|
|
The compiler only needs to generate something like:
|
|
|
|
```js
|
|
let user = someExpression;
|
|
core.print(js.dartify(user.toJSON()));
|
|
```
|
|
|
|
Here a conversion was also inserted, based on the `@JSConvert(dartify)`
|
|
annotation. Because it's static, the compilers are able to inline JS code if
|
|
they choose to.
|
|
|
|
Static dispatch lets us reshape APIs in very powerful ways, without the
|
|
overhead and complexity of wrappers. It also provides a good user experience
|
|
in the IDE.
|
|
|
|
Notably, this interop is extremely similar to the language team's
|
|
[Static Extension Types](https://github.com/dart-lang/language/issues/42) and
|
|
[Static Extension Methods](https://github.com/dart-lang/language/issues/41)
|
|
proposals. All members in a JS interop class that have Dart implementation code
|
|
(i.e. function bodies, rather than `external`) are required to be nonextensible.
|
|
If we get those language features, they'll provide an alternate syntax, and
|
|
offer additional capabilities.
|
|
|
|
|
|
#### Extension Fields
|
|
|
|
In existing web compilers, there is some ambiguity about how fields on JS types
|
|
are interpreted:
|
|
|
|
```dart
|
|
@JS()
|
|
class MyJSClass {
|
|
external int get foo;
|
|
external set foo(int value);
|
|
|
|
int bar;
|
|
|
|
@JS()
|
|
int baz;
|
|
}
|
|
```
|
|
|
|
Both compilers support "foo". DDC interprets "bar" as a getter/setter pair
|
|
similar to "foo" but dart2js does not. Neither compiler recognizes the `@JS` on
|
|
"baz".
|
|
|
|
Existing JS interop code suggests we need to provide two features:
|
|
- make it easy to declare external getter/setter pairs.
|
|
- add Dart state to JS classes.
|
|
|
|
We can accomplish this by using `@JS` to indicate a JS field. With the new
|
|
interpretation:
|
|
|
|
```dart
|
|
@JS()
|
|
class MyJSClass {
|
|
external int get foo; // external getter/setter
|
|
external set foo(int value);
|
|
|
|
@sealed
|
|
int bar; // Dart extension field
|
|
|
|
@JS() // sugar for external getter/setter, can't have an initializer
|
|
int baz;
|
|
}
|
|
```
|
|
|
|
The "extension field" is essentially an expando:
|
|
|
|
```dart
|
|
@JS()
|
|
class MyJSClass {
|
|
// [...]
|
|
@sealed
|
|
int get bar => _bar[this];
|
|
@sealed
|
|
void set bar(value) { _bar[this] = value; }
|
|
}
|
|
|
|
final _bar = Expando<int>();
|
|
```
|
|
|
|
(Web compilers may implement it differently, such as a JS Symbol property stored
|
|
on the instance.)
|
|
|
|
|
|
### JS Interop Checked Mode
|
|
|
|
We may want a "JS interop checked mode" to instrument APIs and check that
|
|
parameters and return types match their declarations. This would provide a lot
|
|
of added safety. It could be turned on for tests, for example.
|
|
If implemented, this would be an opt-in capability
|
|
|
|
|
|
### Data Type Interoperability
|
|
|
|
To use typed APIs, we need to be able to pass data back and forth, and write
|
|
type annotations for the parameters and return value. One of the key questions
|
|
then is: can JS and Dart objects be passed directly, or is some type of
|
|
conversion or wrappers required?
|
|
|
|
As the Firebase example illustrates, using wrappers everywhere has significant
|
|
cost in code size, performance, and usability.
|
|
|
|
However, it is impossible to make every single Dart and JS object "just work" in
|
|
the other environment. Consider this example:
|
|
|
|
```dart
|
|
external factory RecaptchaVerifier(container,
|
|
[@JSConvert(jsify) Map<String, Object> parameters, App app]);
|
|
```
|
|
|
|
While JavaScript has a [Map class](https://tc39.github.io/ecma262/#sec-map-objects)
|
|
now, many APIs still use raw JS Objects, such as `{ "size": "invisible" }`. In
|
|
Dart `Map<String, Object>` is an interface that at runtime, could contain any
|
|
class that implements Map (including user-defined Maps). Dart's equality
|
|
semantics (`operator ==` and `hashCode`) are different from JS too.
|
|
|
|
(Even if we're only interested in Dart's default LinkedHashMap with String
|
|
keys, it would be difficult to make this work. The Map class would need to
|
|
store its data as properties on itself. Some keys would not work correctly such
|
|
as "__proto__", and some operations would have different performance
|
|
characteristics. The map can be modified from JS, so it's difficult to see how
|
|
things like `length` and `keys` could be implemented efficiently.)
|
|
|
|
For these reasons, we can't take an all-or-nothing approach: ideally most types
|
|
are wrapperless, but we'll need good support for conversions too.
|
|
|
|
|
|
### Implicit and Explicit Conversions
|
|
|
|
Wrapperless types are very convenient: you pass them back and forth, and they
|
|
just work. Explicit conversions involve a lot of boilerplate that must be
|
|
repeated everywhere. For example, we could've written the earlier `toJson()`
|
|
example like this:
|
|
|
|
```dart
|
|
@JS()
|
|
@anonymous
|
|
class UserInfo {
|
|
// [...]
|
|
|
|
/// Returns a JSON-serializable representation of this object.
|
|
@sealed
|
|
Map<String, dynamic> toJson() => dartify(_toJson());
|
|
|
|
@JS('toJSON')
|
|
external Map<String, dynamic> _toJson();
|
|
}
|
|
```
|
|
|
|
Or worse, the original version of the Recaptcha example shown earlier:
|
|
```dart
|
|
factory RecaptchaVerifier(container,
|
|
[Map<String, dynamic> parameters, App app]) =>
|
|
(parameters != null)
|
|
? ((app != null)
|
|
? RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl(
|
|
container, jsify(parameters), app.jsObject))
|
|
: RecaptchaVerifier.fromJsObject(
|
|
RecaptchaVerifierJsImpl(container, jsify(parameters))))
|
|
: RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl(container));
|
|
|
|
// [ed: in interop/auth_interop.dart]
|
|
|
|
external factory RecaptchaVerifierJsImpl(container,
|
|
[Object parameters, AppJsImpl app]);
|
|
```
|
|
|
|
Implicit conversions reduce the boilerplate and make the types feel more
|
|
like directly passed (wrapperless) types. They require less developer input to
|
|
get the correct conversion. The downside is that the conversion is less visible,
|
|
and the implications of conversions may surprise the developer. Overall these
|
|
conversion are similar to "autoboxing" in C# and Java, and are probably a net
|
|
usability benefit.
|
|
|
|
There's a language issue for [implicit conversions](https://github.com/dart-lang/language/issues/107),
|
|
so that may provide us better support. The native FFI also has a similar
|
|
mechanism for marshalling.
|
|
|
|
|
|
## Functions
|
|
|
|
Similar to Array, functions from JS do not have reified type information and are
|
|
allowed to be cast to any Dart function. This is already implemented in Dart web
|
|
compilers.
|
|
|
|
Ideally Dart functions could also be directly passed to JS. However this is not
|
|
the case in dart2js. It is not simple to change that, however.
|
|
|
|
In the meantime, automatic converisons will be provided:
|
|
- passing a Dart function type to any JS parameter performs an `allowInterop`
|
|
conversion.
|
|
- passing any Dart value to a JS function type performs an `allowInterop`
|
|
conversion if the runtime value is a Dart function.
|
|
- Function typed parameters/variables can be annotated with `@JS()` to indicate
|
|
that they should be treated as being passed to JS, implying an `allowInterop`
|
|
conversion.
|
|
- Typedefs can be annoated with `@JS()` to indicated they should be treated as
|
|
being passed to JS, implying an `allowInterop` conversion.
|
|
|
|
One particular challenge is that dartdevc (DDC) represents Dart functions as JS
|
|
functions, so it is difficult to catch problems that may arise later in dart2js.
|
|
It should not occur much in practice, thanks to implicit conversions, and the
|
|
previously mentioned "checked mode" provides a means to catch it.
|
|
|
|
|
|
### Optional Arguments
|
|
|
|
To pass functions between JS and Dart, we need to define how calling conventions
|
|
work. The general principle is "when calling JS, work like JS". Consider the
|
|
first example again:
|
|
|
|
```dart
|
|
Storage storage([String url]) {
|
|
var jsObjectStorage =
|
|
(url != null) ? jsObject.storage(url) : jsObject.storage();
|
|
return Storage.getInstance(jsObjectStorage);
|
|
}
|
|
|
|
// What we'd like to write:
|
|
external Storage storage([String url]);
|
|
```
|
|
|
|
Or for a more complex case, consider the Recaptcha example:
|
|
```dart
|
|
factory RecaptchaVerifier(container,
|
|
[Map<String, dynamic> parameters, App app]) =>
|
|
(parameters != null)
|
|
? ((app != null)
|
|
? RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl(
|
|
container, jsify(parameters), app.jsObject))
|
|
: RecaptchaVerifier.fromJsObject(
|
|
RecaptchaVerifierJsImpl(container, jsify(parameters))))
|
|
: RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl(container));
|
|
|
|
// [ed: in interop/auth_interop.dart]
|
|
external factory RecaptchaVerifierJsImpl(container,
|
|
[Object parameters, AppJsImpl app]);
|
|
```
|
|
|
|
These methods are dispatching the call manually, to handle the difference in
|
|
JS and Dart calling conventions: optional arguments are not passed as `null`
|
|
in JS, but are visible via `arguments.length` and their value defaults to
|
|
`undefined` rather than `null`.
|
|
|
|
What we'd like to write for Recaptcha is:
|
|
```dart
|
|
external factory RecaptchaVerifier(container,
|
|
[@JSMapToObject() Map<String, Object> parameters, App app]);
|
|
```
|
|
|
|
Because it's now a JS function, the compilers must call it with the correct
|
|
number of arguments. For example, this Dart code:
|
|
```dart
|
|
RecaptchaVerifier(container);
|
|
RecaptchaVerifier(container, {'foo': 'bar'});
|
|
RecaptchaVerifier(container, {'foo': 'bar'}, app);
|
|
```
|
|
|
|
Should compile to JS code similar to:
|
|
```js
|
|
new auth.RecaptchaVerifier(container);
|
|
new auth.RecaptchaVerifier(container, {'foo': 'bar'});
|
|
new auth.RecaptchaVerifier(container, {'foo': 'bar'}, app);
|
|
```
|
|
|
|
If someone wants to forward multiple parameters, we could provide an annotation
|
|
to help:
|
|
|
|
```dart
|
|
@JS()
|
|
class Example {
|
|
@sealed
|
|
Object wrapsMethodWithOptionalArgs([Object foo, Object bar]) {
|
|
// [...]
|
|
return _underlyingJSMethod(foo, bar);
|
|
}
|
|
|
|
@JS('underlyingJSMethod')
|
|
external Object _underlyingJSMethod(
|
|
[@JSNullToOptional() Object foo, @JSNullToOptional() Object bar]);
|
|
}
|
|
```
|
|
|
|
With `@JSNullToOptional()` the compiler would automatically check for `null` in
|
|
those arguments and dispatch the call to the correct JS signature.
|
|
|
|
|
|
### Named Arguments
|
|
|
|
JS functions don't have a notion of named arguments; rather by convention, JS
|
|
Object literals are passed, and optionally destructured into parameters by the
|
|
callee.
|
|
|
|
Named arguments will be allowed as syntactic sugar for passing a JS object
|
|
literal. For example:
|
|
|
|
```dart
|
|
class Example {
|
|
external takesJSObject({String a, int b})
|
|
}
|
|
f(Example e) {
|
|
e.takesJSObject(a: 'hi', b: 123);
|
|
}
|
|
```
|
|
|
|
Is equivalent to this JS:
|
|
|
|
```js
|
|
function f(e) {
|
|
e.takesJSObject({a: 'hi', b: 123});
|
|
}
|
|
```
|
|
|
|
Similarly if a Dart function that takes named arguments is passed to JS, it
|
|
must desugar them. To make this work, we'll need to restrict JS function types
|
|
to having only optional or named arguments, not both.
|
|
|
|
|
|
### Overloads
|
|
|
|
Method overloads do not exist in JavaScript. They can be helpful for
|
|
expressing type signatures, and calling native APIs that are
|
|
overloaded, such as Canvas's [createImageData](https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation).
|
|
|
|
Overloads can be expressed by declaring multiple Dart methods and giving them
|
|
the same JavaScript name:
|
|
|
|
```dart
|
|
// in CanvasRenderingContext2D
|
|
@JS('createImageData')
|
|
external ImageData createImageDataFromImage(ImageData imagedata);
|
|
@JS()
|
|
external ImageData createImageData(int sw, int sh);
|
|
```
|
|
|
|
If the author wanted to expose those as the same method, they could instead do:
|
|
|
|
```dart
|
|
@JS()
|
|
external ImageData createImageData(imagedata_OR_sw, [int sh]);
|
|
```
|
|
|
|
Dispatching based on types should not generally be required, but if it is, it
|
|
can be written like this:
|
|
|
|
```dart
|
|
@sealed
|
|
ImageData createImageData(imagedata_OR_sw, [int sh]) {
|
|
if (imagedata_OR_sw is int && sh != null) {
|
|
return _createImageData(imagedata_OR_sw, sh);
|
|
} else {
|
|
return _createImageDataFromImage(imagedata_OR_sw);
|
|
}
|
|
}
|
|
JS('createImageData')
|
|
external ImageData _createImageDataFromImage(ImageData imagedata);
|
|
@JS()
|
|
external ImageData _createImageData(int sw, int sh);
|
|
```
|
|
|
|
The language team is also considering the problem of declaring Dart APIs like
|
|
this, see [issue 145](https://github.com/dart-lang/language/issues/145).
|
|
|
|
|
|
### This Argument
|
|
|
|
JS functions have a hidden `this` parameter, and it is sometimes important for
|
|
Dart to be able to pass that to JS, or receive it from JS.
|
|
|
|
That's accomplished with the `@JSThis()` annotation:
|
|
|
|
```dart
|
|
@JS('Object')
|
|
abstract class JSObject {
|
|
external static _JSObjectPrototype prototype;
|
|
}
|
|
|
|
@JS()
|
|
@anonymous
|
|
abstract class _JSObjectPrototype {
|
|
external static bool hasOwnProperty(@JSThis() target, propertyKey);
|
|
}
|
|
|
|
@JS()
|
|
@anonymous
|
|
class PropertyDescriptor {
|
|
@JS() Object value;
|
|
@JS() bool writable;
|
|
@JS() bool configurable;
|
|
@JS() bool enumerable;
|
|
external Object Function(@JSThis() Object) get;
|
|
external void Function(@JSThis() Object, Object) set;
|
|
}
|
|
```
|
|
|
|
This lets you declare which parameter should receive `this`/be passed as `this`.
|
|
|
|
|
|
### Method Tearoffs
|
|
|
|
Tearoffs of JS types should bind `this`, as noted in
|
|
[issue 32370](https://github.com/dart-lang/sdk/issues/32370). We also need to
|
|
decide what runtime type information to attach. Tearoffs could get the
|
|
statically visible type at the call site, or they could be treated like other
|
|
JS functions, and be assignable to any function type. Untyped is advantageous
|
|
for performance/simplicity, so it's probably preferrable, unless we find
|
|
compelling examples.
|
|
|
|
|
|
### Generic Type Parameters
|
|
|
|
JavaScript does not have reified generic types. Generic types can
|
|
be used in interop signatures, but they have no effect at runtime, and no type
|
|
argument is passed.
|
|
|
|
For JS code calling into exported Dart functions (e.g. `@JSExport`), generic
|
|
type arguments will come through as a special, runtime-only type `JSAny` that
|
|
represents the absence of a reified generic type. This is discussed further in
|
|
the next section, about Generic Types, List and JS Array.
|
|
|
|
|
|
## Data Types Conversions
|
|
|
|
This section discusses specific data types, and has recommendations for how
|
|
these should be handled in the Dart web compilers
|
|
|
|
### Generic Types, List and JS Array
|
|
|
|
Both Dart web compilers already use JS Array to implement Dart `List<T>` in
|
|
a wrapperless style. The main challenge for JS interop is how to handle generic
|
|
types.
|
|
|
|
Generic types in Dart are reified, in other words, generic type arguments are
|
|
represented and stored at run time. These are used by `is` and `as` checks, for
|
|
example, `<int>[1, 2, 3] is List<int>` will return true, and the cast
|
|
`<int>[1, 2, 3] as List<int>` will succeed. But `<int>[1, 2, 3] is List<String>`
|
|
will return false.
|
|
|
|
JavaScript cannot create Arrays that have an associated Dart generic type.
|
|
Currently these are interpreted as `List<dynamic>`, which frequently results in
|
|
a cast failure, or requires a workaround. For example:
|
|
```dart
|
|
List<App> get apps => firebase.apps
|
|
// explicitly typing the param as dynamic to work-around
|
|
// https://github.com/dart-lang/sdk/issues/33537
|
|
.map((dynamic e) => App.getInstance(e))
|
|
.toList();
|
|
|
|
// [in another file]
|
|
|
|
@JS()
|
|
external List<AppJsImpl> get apps;
|
|
```
|
|
|
|
The problem is that `List<AppJsImpl> get apps` actually returns `List<dynamic>`,
|
|
so calls like `.map` will fail Dart's reified generic checks (because the list
|
|
could contain anything, the parameter `e` must be typed to accept anything).
|
|
This leads to other strange results, such as:
|
|
|
|
```dart
|
|
@JS()
|
|
external List<String> get stringsFromJS;
|
|
|
|
main() {
|
|
// Either prints `true` or `false`!
|
|
// True if the compiler optimizes it, false if it's evaluated at runtime.
|
|
print(stringsFromJS is List<String>);
|
|
|
|
List list = stringsFromJS;
|
|
List<String> list2 = list; // type error: not a List<String>.
|
|
}
|
|
```
|
|
|
|
The new version of this API is simply:
|
|
```dart
|
|
external List<App> get apps;
|
|
```
|
|
|
|
And it works mostly how you'd expect with `is` and `as` checks:
|
|
|
|
```dart
|
|
main() {
|
|
List apps = firebase.apps;
|
|
print(apps is List<App>); // true
|
|
apps as List<App>; // works
|
|
|
|
// prints list of names names, `(a)` is inferred as `(App a)`
|
|
print(apps.map((a) => a.name).toList());
|
|
|
|
apps is List<int> // true: `int` could be from JS
|
|
apps is List<MyDartClass> // false: can't be a list of Dart classes
|
|
// (unless the class is exported to JS)
|
|
}
|
|
```
|
|
|
|
The last check is an example of one consequence of the looser checking.
|
|
Conceptually we have a `JSAny` type. This type only exists in runtime, and does
|
|
not require a representation, since it results from the absense of type
|
|
information. This is discussed later when we look at the type system.
|
|
|
|
Besides JS Arrays, Dart generic functions and methods can also be called from
|
|
JS. In that case, the reified generic type will usually be omitted. This is
|
|
discussed later when we look at exporting Dart classes to JS.
|
|
|
|
|
|
### Null and Undefined
|
|
|
|
Dart web compilers generally treat these as interchangeable; while they don't
|
|
create `undefined` themselves, it's treated as `== null`. This makes interop
|
|
easier, so it's probably worth keeping. We should also add an `undefined`
|
|
constant to "package:js", for cases where it's necessary to pass it explicitly
|
|
to JS.
|
|
|
|
|
|
### Future and JS Promise
|
|
|
|
JS Promises are very common in web APIs, as many operations are asynchronous.
|
|
These function very similarly to Dart's `Future<T>` interface.
|
|
|
|
There are several possible answers:
|
|
1. provide an implicit conversion
|
|
2. both types implement the other's interface
|
|
3. have _Future be a JS Promise
|
|
|
|
Option 3 is not feasible. Option 2 is ideal, but may cause issues in dart2js,
|
|
because they currently assume no JS types implement `Future` (this avoids
|
|
"getInterceptor" overhead). So we probably need to go with Option 1. The
|
|
dart:html library already has a conversion in one direction, and `dartdevc` may
|
|
soon have both directions (to use JS async/await).
|
|
|
|
If we did want to do the adapter design, here is a rough sketch:
|
|
|
|
```dart
|
|
@JS('Promise')
|
|
@JSInterfaceDispatch()
|
|
@JSDynamicDispatch() // Note: because existing SDK native types support this.
|
|
class JSPromise<T> implements Future<T> {
|
|
@JS('then')
|
|
external JSPromise<R> _then<R>(
|
|
/*R | Thenable<R>*/Object Function(T) onFulfilled,
|
|
[/*R | Thenable<R>*/Object Function(Object) onRejected]);
|
|
|
|
external factory JSPromise.resolve(/*R | Thenable<R>*/Object value);
|
|
|
|
Future<R> then<R>(FutureOr<R> onValue(T value), {Function onError}) {
|
|
return _then(
|
|
Zone.current.bindUnaryCallback(onValue),
|
|
onError != null
|
|
// Note: real impl must also support an `onError` callback that
|
|
// takes a StackTrace as the second argument.
|
|
? Zone.current.bindUnaryCallback(onError as dynamic)
|
|
: null);
|
|
}
|
|
// [...]
|
|
}
|
|
|
|
class _Future<T> implements Future<T> {
|
|
// [...]
|
|
|
|
// Note: in reality this could be injected by the compiler on any class
|
|
// implementing Future<T> if we want them to be Promise-like.
|
|
@JSExport('then')
|
|
JSPromise<R> _jsThen<R>(
|
|
/*R | Thenable<R>*/Object Function(T) onFulfilled,
|
|
[/*R | Thenable<R>*/Object Function(Object) onRejected]) {
|
|
|
|
Future<R> f = then((value) => JSPromise.resolve(onFulfilled(value)),
|
|
onError: onRejected != null
|
|
? (error) => JSPromise.resolve(onRejected(error))
|
|
: null);
|
|
// This conversion is not necessary if we only want to implement Thenable.
|
|
return JSPromise.resolve(f);
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
### Iterable<T> and JS Iterable
|
|
|
|
All Dart `Iterable<T>` classes should implement `[Symbol.iterator]`, which
|
|
allows them to be used in JS `for-of` loops.
|
|
|
|
Converting from a JS iterable to a Dart Iterable requires a conversion
|
|
(either implicit or explicit).
|
|
|
|
Avoiding a conversion is probably not ideal. We'd need `Iterable<T>` methods
|
|
on any JS object that contains `[Symbol.iterator]`. This requires compilers to
|
|
place `Iterable<T>` members on Object.prototype, or handle this at the
|
|
interceptor level. The current theory is that not many JS APIs return
|
|
iterables (Arrays are much more common). A wrapper-based conversion, either
|
|
implicit or explcit, should be enought to handle this.
|
|
|
|
|
|
### Stream<T> and JS Async Iterable
|
|
|
|
The EcmaScript draft contains a new feature [for-await-of loops](https://tc39.github.io/ecma262/#sec-for-in-and-for-of-statements)
|
|
and `Symbol.asyncIterator` (see also [MDN for-await-of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of)).
|
|
Once JS has that, `Stream<T>` could work similarly to `Future<T>`.
|
|
|
|
|
|
### Stream<T> and Callbacks
|
|
|
|
These will require a conversion in either direction. However we can provide
|
|
helpers to make this easy. Consider this example from Firebase:
|
|
|
|
```dart
|
|
class Auth extends JsObjectWrapper<AuthJsImpl> {
|
|
// [...]
|
|
Func0 _onAuthUnsubscribe;
|
|
StreamController<User> _changeController;
|
|
|
|
Stream<User> get onAuthStateChanged {
|
|
if (_changeController == null) {
|
|
var nextWrapper = allowInterop((firebase_interop.UserJsImpl user) {
|
|
_changeController.add(User.getInstance(user));
|
|
});
|
|
|
|
var errorWrapper = allowInterop((e) => _changeController.addError(e));
|
|
|
|
void startListen() {
|
|
assert(_onAuthUnsubscribe == null);
|
|
_onAuthUnsubscribe =
|
|
jsObject.onAuthStateChanged(nextWrapper, errorWrapper);
|
|
}
|
|
|
|
void stopListen() {
|
|
_onAuthUnsubscribe();
|
|
_onAuthUnsubscribe = null;
|
|
}
|
|
|
|
_changeController = StreamController<User>.broadcast(
|
|
onListen: startListen, onCancel: stopListen, sync: true);
|
|
}
|
|
return _changeController.stream;
|
|
}
|
|
}
|
|
|
|
// [in interop/auth_iterop.dart]
|
|
@JS('Auth')
|
|
class AuthJsImpl {
|
|
// [...]
|
|
external Func0 onAuthStateChanged(nextOrObserver,
|
|
[Func1 opt_error, Func0 opt_completed]);
|
|
}
|
|
```
|
|
|
|
With the right helpers and elimination of wrappers, this becomes:
|
|
|
|
```dart
|
|
@JS()
|
|
class Auth {
|
|
@sealed
|
|
Stream<User> _changeStream;
|
|
|
|
@JS('onAuthStateChanged')
|
|
external Func0 _onAuthStateChanged(Func1<User, void> nextOrObserver,
|
|
[Func1 opt_error, Func0 opt_completed]);
|
|
|
|
@sealed
|
|
Stream<User> get onAuthStateChanged =>
|
|
_changeStream ??= CreateStream(_onAuthStateChanged);
|
|
}
|
|
|
|
// Note: package:js will defined some helpers like this.
|
|
// More research is needed to find all of the the common patterns.
|
|
static Stream<T> CreateStream<T>(
|
|
@JS() Func0 Function(Func1<T, void> nextOrObserver, [Func1 opt_error])
|
|
subscribeToEvent) {
|
|
Func0 unsubscribe;
|
|
StreamController<T> controller;
|
|
controller = StreamController.broadcast(
|
|
onListen: () {
|
|
// Because `subscribeToEvent` is annotated with `@JS()`, `allowInterop`
|
|
// will be automatically handled on these callbacks.
|
|
unsubscribe = subscribeToEvent(controller.add, controller.addError);
|
|
},
|
|
onCancel: unsubscribe,
|
|
sync: true);
|
|
return controller.stream;
|
|
}
|
|
```
|
|
|
|
|
|
### DateTime and JS Date
|
|
|
|
Similar to Future/Promise, we can investigate and determine which of these is
|
|
best:
|
|
- implement DateTime on top of JS Date
|
|
- have them implement each other's interfaces
|
|
- provide an implicit conversion
|
|
|
|
|
|
### Map/Set and JS Map/Set
|
|
|
|
At the very least, we'll provide `JSMap<K, V>` and `JSSet<T>` types in
|
|
"package:js" that will implement the corresponding Dart interfaces and also be
|
|
wrapperless JS Maps/Sets.
|
|
|
|
Both web compilers already use (or have prototyped use of) ES6 Map/Set under
|
|
some circumstances, such as identity Maps/Sets. So it may be possible to have
|
|
the objects returned by `HashMap.identity()` and `HashSet.identity()` simply be
|
|
JS Map/Set respectively. That would be a nice feature, but is not necessary
|
|
to provide good interop with these types.
|
|
|
|
Declaring a JS API as returning a Dart Map will be a hint, because it will
|
|
probably not work the way the developer expects (if they are expecting a JS
|
|
object to be interpreted as a Map). Instead they can:
|
|
- use `JSMap<K, V>` if they intended the JS Map class.
|
|
- use `JSObjectMap<K, V>` if the return value is a JS object. It's a normal Dart
|
|
class that implements Map and is backed by the JS Object. This indicates a
|
|
wrapping conversion.
|
|
- use `@JSConvert()` to provide a custom conversion to Map
|
|
- use some other JS class type, which declares the properties that the map
|
|
contains. Useful when a JS Object is returned, and the data is structured.
|
|
|
|
Open question: are the generic type arguments to JSMap/JSSet reified if
|
|
allocated in Dart? I think they should be, for consistency with `List<T>`.
|
|
Similar to `List<T>` however, a `Map` allocated in JS could be cast to
|
|
`JSMap<K, V>` for any types `K` and `V` that subtype `JSAny`.
|
|
|
|
|
|
### Maps and JS Objects
|
|
|
|
Converting arbitrary Dart maps to JS Objects is probably not feasible, for
|
|
reasons discussed in "Data Type Interoperability". However we can provide
|
|
a conversion:
|
|
|
|
```dart
|
|
external factory RecaptchaVerifier(container,
|
|
[@JSConvert(jsify) Map<String, Object> parameters, App app]);
|
|
```
|
|
|
|
The `jsify` function is declared by Firebase, and does a deep conversion based
|
|
on the specific types Firebase APIs use. However most of that will no longer be
|
|
necessary, so we might be able to get away with:
|
|
|
|
```dart
|
|
external factory RecaptchaVerifier(container,
|
|
[@MapToJSObject() Map<String, Object> parameters, App app]);
|
|
```
|
|
|
|
This uses a built-in shallow conversion from a Dart Map to a JS object.
|
|
|
|
|
|
#### Wrapper-based JSObjectMap
|
|
|
|
To make it easier to work with JS objects via indexing, we could provide several
|
|
classes in "package:js" to help:
|
|
|
|
```dart
|
|
@JS()
|
|
@anonymous
|
|
class JSIndexable<K extends Object, V extends Object> {
|
|
external V operator [](K key);
|
|
external void operator []=(K key, Object value);
|
|
|
|
Map<K2, V2> toMap<K2, V2>() => JSObjectMap(this);
|
|
}
|
|
|
|
/// Wraps a JS Object in a Dart Map.
|
|
class JSObjectMap<K, V> with MapMixin<K, V> implements Map<K, V> {
|
|
final JSIndexable object;
|
|
|
|
JSObjectMap([Object object])
|
|
: object = object as JSIndexable ?? JSIndexable();
|
|
|
|
factory JSObjectMap.from(Map other) => JSObjectMap<K, V>()..addAll(other);
|
|
factory JSObjectMap.of(Map<K, V> other) = JSObjectMap.from;
|
|
factory JSObjectMap.fromEntries(Iterable<MapEntry<K, V>> entries) =>
|
|
JSObjectMap<K, V>()..addEntries(entries);
|
|
|
|
bool containsKey(Object key) => Reflect.has(object, key);
|
|
V operator [](Object key) => object[key];
|
|
void operator []=(Object key, value) {
|
|
object[key] = value;
|
|
}
|
|
List<K> get keys => Reflect.ownKeys(object) as List<K>;
|
|
V remove(Object key) {
|
|
if (Reflect.has(object, key)) {
|
|
var result = object[key];
|
|
Reflect.deleteProperty(object, key);
|
|
return result;
|
|
}
|
|
return null;
|
|
}
|
|
void clear() {
|
|
for (var key in Reflect.ownKeys(object)) {
|
|
Reflect.deleteProperty(object, key);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Here `JSObjectMap` works like a normal Dart class, but it's easy to get the
|
|
raw JS object from it. Meanwhile any JS object can be cast to `JSIndexable` to
|
|
provide access to the index operators and the `toMap()` function.
|
|
|
|
|
|
#### Autoboxing JSObjectMap
|
|
|
|
|
|
*NOTE*: we probably won't want/need this, but I wanted to mention it as a
|
|
possible optoon. It provides a means of implementing Dart interfaces from JS
|
|
classes, that is reasonably friendly to optimizations.
|
|
|
|
An alternative design for `JSObjectMap` is to work similarly to `JSIndexable`.
|
|
This provides a Map-like view of an arbitrary JS object. Then the question
|
|
becomes: how do we cast our type to a `Map<K, V>`? Autoboxing would allow us to
|
|
do that.
|
|
|
|
Here's a rough sketch:
|
|
|
|
```dart
|
|
@JS()
|
|
@JSAutobox()
|
|
class JSObjectMap<K, V> extends MapBase<K, V> {
|
|
factory JSObjectMap() => _create(null);
|
|
factory JSObjectMap.from(Map other) => _create<K, V>(null)..addAll(other);
|
|
factory JSObjectMap.of(Map<K, V> other) = JSObjectMap.from;
|
|
factory JSObjectMap.fromEntries(Iterable<MapEntry<K, V>> entries) =>
|
|
_create<K, V>(null)..addEntries(entries);
|
|
|
|
@JS('create')
|
|
external static JSObjectMap<K2, V2> _create<K2, V2>(Object proto);
|
|
|
|
external V operator [](Object key);
|
|
external void operator []=(Object key, Object value);
|
|
|
|
bool containsKey(Object key) => Reflect.has(this, key);
|
|
List<K> get keys => Reflect.ownKeys(this) as List<K>;
|
|
|
|
V remove(Object key) {
|
|
if (Reflect.has(this, key)) {
|
|
var result = this[key];
|
|
Reflect.deleteProperty(this, key);
|
|
return result;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void clear() {
|
|
for (var key in Reflect.ownKeys(this)) {
|
|
Reflect.deleteProperty(this, key);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Because this type uses `@JSAutobox()`, the JS object will be automatically
|
|
boxed when cast to any Dart interface that it implements (except for dart:core
|
|
Object). This reduces the boilerplate that might otherwise be required.
|
|
|
|
The benefit of this approach is that any JS object can be freely cast to
|
|
`JSObjectMap`, providing efficient access using Map-like APIs. For example,
|
|
let's revist our `UserInfo.toJson()` example:
|
|
|
|
```dart
|
|
@JS()
|
|
@anonymous
|
|
class UserInfo {
|
|
// [...]
|
|
/// Returns a JSON-serializable representation of this object.
|
|
@JS('toJSON')
|
|
external JSObjectMap<String, dynamic> toJson();
|
|
}
|
|
|
|
// [code snippet from auth_test.dart]
|
|
|
|
test('toJson()', () async {
|
|
// [...]
|
|
var userMap = userCredential.user.toJson();
|
|
expect(userMap, isNotNull);
|
|
expect(userMap as Map, isNotEmpty); // [note: `as Map` was added]
|
|
expect(userMap["displayName"], "Other User");
|
|
expect(userMap["photoURL"], "http://google.com");
|
|
// [...]
|
|
});
|
|
```
|
|
|
|
Consider the line `expect(userMap as Map, isNotEmpty)` in this example.
|
|
The `as Map` is necessary to trigger boxing, so the `isNotEmpty` matcher will
|
|
work.
|
|
|
|
Autoboxing would be a neat way to provide support for implementing Dart
|
|
interfaces from JS. Autoboxing has proven useful in other languages for native
|
|
interop. It may be useful for advanced JS interop scenarios, such as providing
|
|
a dart:html-like API without JS dynamic/interface dispatch.
|
|
|
|
|
|
## Less Static Interop
|
|
|
|
### Virtual Dispatch
|
|
|
|
In many cases JS interop can take advantage of virtual dispatch, even with
|
|
`@sealed`, because the JS method it calls will dispatch with normal JS rules
|
|
(i.e. lookup the property on the prototype chain). `@sealed` procludes overrides
|
|
of itself with another Dart member, however.
|
|
|
|
We support virtual Dart methods on JS types, with a bit more work. This doesn't
|
|
compromise the model very much, but it does add additional dispatch cost.
|
|
Implementing interfaces and dynamic dispatch would still be restricted.
|
|
|
|
|
|
### Interface and Dynamic dispatch
|
|
|
|
In the future, we may want to provide something like `@JSInterfaceDispatch()` or
|
|
`@JSDynamicDispatch()`, that preserves RTTI and enables interface/dynamic
|
|
dispatch. Our compilers already support this, as they use it themselves for core
|
|
types in the SDK, and for the HTML libraries.
|
|
|
|
There are several issues with exposing this:
|
|
- extensions would now have to be globally unique for a given JS prototype.
|
|
- this requires compile time checking (dart2js) or runtime checking (dartdevc).
|
|
- it's possible to subvert compile time checking accidentally (if the same JS
|
|
prototype is exposed with two different names).
|
|
- we'd need to standardize the annotations between the compilers and ensure
|
|
they have the same semantics.
|
|
|
|
Scoped extension method approaches for dynamic languages may help for the naming
|
|
conflicts (e.g.
|
|
[as discussed in this paper](https://arxiv.org/pdf/1708.01679.pdf)),
|
|
but those have performance/complexity/usability tradeoffs.
|
|
|
|
Many users have requested flags to disable dynamic dispatch, so this may not be
|
|
the direction we need go. Implementing interfaces can be useful however.
|
|
(The "autoboxing" approach discussed previously may be a possible compromise.)
|
|
|
|
|
|
### JS Proxy
|
|
|
|
One question that comes up occassionally is how to use JS Proxy from Dart.
|
|
We can expose JSProxy from "package:js":
|
|
|
|
```dart
|
|
@JS('Proxy')
|
|
class JSProxy {
|
|
external factory JSProxy(Object target, JSProxyHadler handler);
|
|
}
|
|
|
|
@JS()
|
|
@anonymous
|
|
abstract class JSProxyHandler {
|
|
// Note: these are defined as field to allow only a subset of them to be
|
|
// implemented
|
|
@JS()
|
|
Object Function(Object target) getPrototypeOf;
|
|
|
|
@JS()
|
|
Object Function(Object target, Object proto) setPrototypeOf;
|
|
|
|
// [...]
|
|
|
|
@JS()
|
|
bool Function(Object target, Object key) has;
|
|
|
|
@JS()
|
|
Object Function(Object target, Object key) get;
|
|
|
|
@JS()
|
|
Object Function(Object target, Object key, Object value) set;
|
|
|
|
@JS()
|
|
bool Function(Object target, Object key) deleteProperty;
|
|
|
|
external factory JSProxyHandler({
|
|
this.getPrototypeOf,
|
|
this.setPrototypeOf,
|
|
this.get,
|
|
this.set,
|
|
this.has,
|
|
this.deleteProperty /* [...] */});
|
|
}
|
|
```
|
|
|
|
It's possible to implement a proxy handler that forwards to Dart's
|
|
noSuchMethod, or the reverse, if someone wanted to do that.
|
|
|
|
(It's also possible to have a noSuchMethod that uses JS Reflect and Object APIs
|
|
to access the JS object. It's not necessary though, because `@JS()` classes are
|
|
essentially a more optimized form of that.)
|
|
|
|
|
|
### JS Reflection
|
|
|
|
JS Reflection APIs are useful for low level support, and these can be provided
|
|
by "package:js":
|
|
|
|
```dart
|
|
@JS('Reflect')
|
|
class JSReflect {
|
|
external static Object apply(target, thisArgument, argumentsList);
|
|
external static Object construct(target, argumentsList, [newTarget]);
|
|
external static bool defineProperty(
|
|
target, propertyKey, PropertyDescriptor attributes);
|
|
external static bool deleteProperty(target, propertyKey, [receiver]);
|
|
external static Object get(target, propertyKey);
|
|
external static PropertyDescriptor getOwnPropertyDescriptor(
|
|
target, propertyKey);
|
|
external static Object getPrototypeOf(target);
|
|
external static bool has(target, propertyKey);
|
|
external static bool isExtensible(target);
|
|
external static List<Object> ownKeys(target);
|
|
external static void preventExtensions();
|
|
external static set(target, propertyKey, value, [receiver]);
|
|
external static setPrototypeOf(target, prototype);
|
|
|
|
static bool hasOwnProperty(target, propertyKey) {
|
|
// Note: could also be implemented using `getOwnPropertyDescriptor`
|
|
return _JSObject.prototype.hasOwnProperty(target, propertyKey);
|
|
}
|
|
|
|
static Object getOwnProperty(target, propertyKey) {
|
|
var desc = getOwnPropertyDescriptor(target, propertyKey);
|
|
if (desc == null) return null;
|
|
if (desc.get != null) return desc.get(target);
|
|
return desc.value;
|
|
}
|
|
}
|
|
|
|
@JS('Object')
|
|
abstract class _JSObject {
|
|
external static _JSObjectPrototype prototype;
|
|
}
|
|
|
|
@JS()
|
|
@anonymous
|
|
abstract class _JSObjectPrototype {
|
|
external static bool hasOwnProperty(@JSThis() target, propertyKey);
|
|
}
|
|
|
|
@JS()
|
|
@anonymous
|
|
class PropertyDescriptor {
|
|
Object value;
|
|
bool writable;
|
|
bool configurable;
|
|
bool enumerable;
|
|
Object Function(@JSThis() Object) get;
|
|
void Function(@JSThis() Object, Object) set;
|
|
}
|
|
```
|
|
|
|
|
|
## Exporting Dart to JS
|
|
|
|
Dart functions and accessors can be exported to JS with `@JSExport`. A compiler
|
|
must provide a version of the function that is callable via the JS interop
|
|
calling conventions described earlier, and ensure that version of the function
|
|
is used when it is passed to JavaScript. (It may have other versions of the
|
|
function that use optimized, dart-specific calling conventions.)
|
|
|
|
|
|
### Exporting classes and libraries
|
|
|
|
This will use `@JSExport` similar to top-level functions/accessors.
|
|
|
|
TODO: more details here
|
|
|
|
### Inheritance between Dart and JS
|
|
|
|
Conceptually extending a JS class from Dart is similar to adding methods,
|
|
because `super` calls are statically dispatched. The tricky part is what to do
|
|
with constructors. JS constructors are normally written as `external factory`
|
|
which precludes extending them. Also there are some notable differences in
|
|
field initialization+super constructor call order between Dart and JS.
|
|
|
|
(JS requires super before initializing fields, Dart requires super after
|
|
initializing final fields. Dart's approach is nice because it prevents
|
|
virtual methods from observing uninitialized state. But for interop, the problem
|
|
is that the two orders are incompatible.)
|
|
|
|
We may be able to provide a method that creates your class:
|
|
|
|
```dart
|
|
class MyDartClass extends TheirJSClass {
|
|
final int x = 1;
|
|
int y, z;
|
|
factory MyDartClass(int y, int z) {
|
|
// super constructpr parameters passed here?
|
|
var self = createJS<MyDartClass>();
|
|
self.y = y;
|
|
self.z = z;
|
|
return self;
|
|
}
|
|
// ...
|
|
}
|
|
```
|
|
|
|
It's not the most satisfying solution, but it seems like a relatively easy way
|
|
to support this.
|
|
|
|
TODO: extending a Dart class from JS
|
|
|
|
|
|
### Interop with JS Modules
|
|
|
|
We'll need a way to declare a JS interop library corresponds to a JS module.
|
|
This could be done on the library tag:
|
|
|
|
```dart
|
|
@JSImport('goog.editor', moduleFormat: 'closure')
|
|
library goog.editor;
|
|
```
|
|
|
|
The compiler can then generate the appropriate import, instead of a global
|
|
reference. This will need to be coordinated with the overall build and server
|
|
system, however, to ensure the JS module is available to the module loader
|
|
(requirejs, ES6 imports, etc).
|
|
|
|
Typically the module format will be assumed to be passed in globally to the
|
|
compiler, as there is generally one standardized module format, so that argument
|
|
won't be present. (Closure library is illustrated here as it can be used in
|
|
addition to other formats, it may be useful to tell the compiler "this module
|
|
is definitely a closure namespace, import it as such".)
|
|
|
|
|
|
### Exposing JS Methods as Getters
|
|
|
|
One common pattern that comes up is converting "getX" methods into "get X"
|
|
getters. We could add `@JSMethod` syntactic sugar to simplify that.
|
|
Consider this prior example of a Google Maps API:
|
|
|
|
```dart
|
|
LatLng get center => _getCenter();
|
|
LatLng get northEast => _getNorthEast();
|
|
LatLng get southWest => _getSouthWest();
|
|
bool get isEmpty => _isEmpty();
|
|
|
|
@JS('getCenter')
|
|
external LatLng _getCenter();
|
|
@JS('getNorthEast')
|
|
external LatLng _getNorthEast();
|
|
@JS('getSouthWest')
|
|
external LatLng _getSouthWest();
|
|
@JS('isEmpty')
|
|
external bool _isEmpty();
|
|
```
|
|
|
|
It could be written as:
|
|
|
|
```dart
|
|
@JSMethod('getCenter')
|
|
external LatLng get center;
|
|
|
|
@JSMethod('getNorthEast')
|
|
external LatLng get northEast;
|
|
|
|
@JSMethod('getSouthWest')
|
|
external LatLng get southWest;
|
|
|
|
@JSMethod('isEmpty')
|
|
external bool get isEmpty;
|
|
```
|
|
|
|
## JS Types in the Dart Type System
|
|
|
|
As discussed earlier, at runtime the absence of reified type information will be
|
|
tracked with a `JSAny` type. `JSAny` can contain types that may originate from
|
|
JS, and it only exists at run time. Here are some examples:
|
|
|
|
```dart
|
|
import 'package:firebase/firebase.dart' show App, Database;
|
|
import 'package:firebase/firebase.dart' as firebase;
|
|
main() {
|
|
List apps = firebase.apps;
|
|
apps is List<Object> // true: Object can hold JS types
|
|
apps is List<int> // true: `int` could be from JS
|
|
apps is List<List> // true: `List` could be from JS
|
|
apps is List<Database> // true: could be a list of any JS interop types
|
|
apps is List<MyDartClass> // false: can't be a Dart class
|
|
apps is List<Stopwatch> // false: can't be a Dart class like Stopwatch
|
|
}
|
|
```
|
|
|
|
We'll probably want to provide access to JS `typeof` and `instanceof` as library
|
|
functions in "package:js".
|
|
|
|
The proposed typing rules for JSAny allow it assigned to/from these types:
|
|
- primitives: Null | bool | int | num | double | String
|
|
- JS interop classes (`@JS()`)
|
|
- Dart classes exported to JS (`@JSExport()`)
|
|
- `List<JSAny>`
|
|
- other "native" SDK types
|
|
- functions (restrict to JSAny param/return types?)
|
|
|
|
This type will not exist in the static type system. The hope is that these
|
|
restrictions keep the unsoundness restricted to objects allocated from JS, and
|
|
to types that are likely to be used by existing JS APIs. By excluding Dart
|
|
classes, we're able to catch things like `List<JSAny>` assigned to
|
|
`List<MyDartClass>`, which is unlikely to work.
|
|
|
|
Open question: should preserve reified JSAny if that type parameter is used to
|
|
construct other types? For example:
|
|
|
|
```dart
|
|
@JS()
|
|
external List foo();
|
|
bar() {
|
|
var list = foo();
|
|
var set = list.toSet() as Set<int>; // does this work?
|
|
// ...
|
|
}
|
|
```
|
|
|
|
|
|
## Implementation Roadmap
|
|
|
|
Here's a rough breakdown of the features into different categories.
|
|
Details subject to change. The items are not in any particular order.
|
|
|
|
Required features (P0):
|
|
- static dispatch (with rename support)
|
|
- static type checking rules (warnings/errors for incorrect usage)
|
|
- runtime type checking rules
|
|
- conversions for builtin types:
|
|
- Functions
|
|
- Dart Map to/from JS Object
|
|
- Future/Promise
|
|
- calling conventions for optional arguments, "this"
|
|
- exporting top-level functions, accessors to JS
|
|
|
|
Important features (P1):
|
|
- user defined conversions
|
|
- conversions for types (e.g. DateTime, Iterables)
|
|
- named arguments
|
|
- exporting classes/members to JS
|
|
- package:js helper library
|
|
- common conversion functions, e.g. jsify/dartify
|
|
- predefined interop for core JS types like Set/Map
|
|
- extension fields
|
|
|
|
Nice to have features (P2):
|
|
- helpers for common callback-to-stream patterns
|
|
- other helpers like `@JSMethod`
|
|
- autoboxing to implement Dart interfaces(?)
|
|
|
|
Features for building "package:html" (P2?):
|
|
- virtual dispatch for JS classes
|
|
- subtyping Dart classes from JS
|
|
- subtyping JS classes from Dart
|
|
- interface dispatch for JS classes (useful for migrating to package:html)
|
|
- dynamic dispatch for JS classes (useful for migrating to package:html)
|
|
|
|
|
|
## Compatibility and Evolution
|
|
|
|
Most of the features described here are backwards compatible, relative to the
|
|
set of JS interop supported by both web compilers.
|
|
|
|
One question that came up is interfaces: currently JS interop classes can be
|
|
implemented by Dart classes (although this may lead to inconsistent behavior
|
|
between the compilers). If `@sealed` Dart members are added to a JS interop
|
|
class, then it won't be legal to implement that as an interface. But this is a
|
|
new feature, so it's only "breaking" in the sense that adding `@sealed` members
|
|
is a breaking change for the package API. (Technically: adding *any* public
|
|
member to a Dart class is a breaking change, for this reason. In practice many
|
|
types are not intended to be used as interfaces, so they don't treat new members
|
|
as breaking).
|
|
|
|
If we discover something that is too backwards incompatible, we can add new
|
|
annotations and leave the current behavior for the existing ones. Or we can
|
|
bump the major version on "package:js". There's a lot of possibilities.
|
|
|
|
|
|
## FAQ
|
|
|
|
### Q: How does JSON work?
|
|
|
|
A: If the JSON is relatively typed, then it can be
|
|
|
|
|
|
### Q: Would new Dart language features help?
|
|
|
|
A: Yes! All of these features would be helpful:
|
|
- extension methods/members
|
|
- extension types
|
|
- implicit conversions
|
|
- autoboxing
|
|
- non-null
|
|
- generalized typedefs
|
|
- `external` on fields (sugar for external getter/setter)
|
|
|
|
In the meantime, this proposal provides a way to improve interop, and is
|
|
intended to be compatible with those features.
|
|
|
|
|
|
### Q: Can dart2js and dartdevc share implementation for JS interop?
|
|
|
|
A: Probably, especially for the static features.
|
|
|
|
If both compilers are exclusively using Kernel, much of this can
|
|
be implemented as a Kernel transform. The static checking can also be done in
|
|
a shared way.
|
|
|
|
Dynamic dispatch, calling conventions, and runtime type checks, and native SDK
|
|
types are different between the two compilers, so those details would require
|
|
separete work.
|
|
|
|
|
|
### Q: If a JS API returns "Object" does this break dart2js tree shaking?
|
|
|
|
A: It does not. Dart classes can be tree-shaken, unless they're explicitly
|
|
exported to JS. Static dispatch members can be tree shaken. Static JS Interop
|
|
types, by design, do not require any runtime type information (RTTI).
|
|
Opt-in features like interface or dynamic dispatch will cause more RTTI to be
|
|
retained.
|
|
|
|
This question may be referring to "dart:html". It supports dynamic dispatch, and
|
|
this causes RTTI to be retained, unless great care is taken on the part
|
|
of dart:html authors (to carefully annotate return types, and avoid untyped
|
|
results) and by developers (to carefully avoid dynamic calls that use names in
|
|
"dart:html", and to avoid JS interop features that could return HTML elements).
|
|
|
|
We may be able to find a better solution, such a providing new "package:html"
|
|
bindings that are mostly static (and using interface/dynamic dispatch sparingly,
|
|
in places where it's really needed).
|
|
|
|
|
|
### Q: Could we use dynamic interop instead?
|
|
|
|
A: It's certainly possible to imagine interop that works "like JS"
|
|
(example from Closure Library's
|
|
[goog.editor Example](https://github.com/google/closure-library/blob/master/closure/goog/demos/editor/editor.html#L84)):
|
|
|
|
```dart
|
|
// Hypothetical dynamic interop
|
|
dynamic goog = window.goog;
|
|
dynamic myField = goog.editor.Field.new('editMe');
|
|
|
|
// Create and register all of the editing plugins you want to use.
|
|
myField.registerPlugin(goog.editor.plugins.BasicTextFormatter.new());
|
|
```
|
|
|
|
The problem is it'll run into the same issues around data type conversions, but
|
|
it won't be able to address them without wrapper classes. As illustrated by the
|
|
".new" it will never be as simple as copying and pasting JS code, and there
|
|
won't be any tooling to help.
|
|
|
|
What we may want to do is provide a way to write small chunk of JS code (in a
|
|
Dart file), similar to GWT. But that shouldn't be used much, with the
|
|
improvements in this proposal to calling conventions and easy ways to access
|
|
properties on JS objects.
|