From 856fcb9c8c37351475e544bc4bf0aa537455b78f Mon Sep 17 00:00:00 2001 From: Joshua Litt Date: Wed, 30 Mar 2022 17:00:20 +0000 Subject: [PATCH] [dart2wasm] Add experimental JS interop API for dart2wasm. This is a fork of `js_util` to support the needs of Wasm. It is very much a WIP. We're landing this now to facilitate prototyping, and so we can get a sense of what the right JS interop API might look like for Wasm. Change-Id: I8b2ddda07e906f1938d4cd5fe0e63203e9cdd6d5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/230120 Reviewed-by: Srujan Gaddam Reviewed-by: Aske Simon Christensen Reviewed-by: Jonas Termansen Commit-Queue: Joshua Litt --- pkg/dart2wasm/bin/run_wasm.js | 68 ++++++++ pkg/dart2wasm/lib/code_generator.dart | 2 +- pkg/dart2wasm/lib/target.dart | 1 + .../wasm/lib/js_util_wasm_patch.dart | 31 ++++ sdk/lib/js_util/js_util_sources.gni | 5 +- sdk/lib/js_util/js_util_wasm.dart | 164 ++++++++++++++++++ sdk/lib/libraries.json | 4 + sdk/lib/libraries.yaml | 3 + tests/web/wasm/js_util_test.dart | 85 +++++++++ tests/web/web.status | 3 + tools/bots/test_matrix.json | 3 +- 11 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 sdk/lib/_internal/wasm/lib/js_util_wasm_patch.dart create mode 100644 sdk/lib/js_util/js_util_wasm.dart create mode 100644 tests/web/wasm/js_util_test.dart diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js index b5034eac40d..593f159f9d7 100644 --- a/pkg/dart2wasm/bin/run_wasm.js +++ b/pkg/dart2wasm/bin/run_wasm.js @@ -42,6 +42,39 @@ function stringToDartString(string) { } } +// Converts a JS array to a Dart List, and also recursively converts the items +// in the array. +function arrayToDartList(array, allocator, adder) { + var length = array.length; + var dartList = dartInstance.exports.$listAllocate(); + for (var i = 0; i < length; i++) { + dartInstance.exports.$listAdd(dartList, array[i]); + } + return dartList; +} + +// Converts a Dart List to a JS array. Any Dart objects will be converted, but +// this will be cheap for JSValues. +function arrayFromDartList(list, reader) { + var length = dartInstance.exports.$listLength(list); + var array = new Array(length); + for (var i = 0; i < length; i++) { + array[i] = dartInstance.exports.$listRead(list, i); + } + return array; +} + +// Recursively converts a JS object into a Dart object. +function dartify(object) { + if (typeof object === "string") { + return stringToDartString(object); + } else if (object instanceof Array) { + return arrayToDartList(object); + } else { + return object; + } +} + // Imports for printing and event loop var dart2wasm = { printToConsole: function(string) { @@ -64,6 +97,41 @@ var dart2wasm = { // stack trace. let userStackString = stackString.split('\n').slice(3).join('\n'); return stringToDartString(userStackString); + }, + arrayFromDartList: arrayFromDartList, + arrayToDartList: arrayToDartList, + stringFromDartString: stringFromDartString, + stringToDartString: stringToDartString, + dartify: dartify, + newObject: function() { + return {}; + }, + globalThis: function() { + return globalThis; + }, + getProperty: function(object, name) { + return object[name]; + }, + hasProperty: function(object, name) { + return name in object; + }, + setProperty: function(object, name, value) { + return object[name] = value; + }, + callMethodVarArgs: function(object, name, args) { + return object[name].apply(object, args); + }, + callConstructorVarArgs: function(object, name, args) { + // Gets a constructor property at object[name], and apply bind to the + // constructor. We pass `null` as the first argument to `bind.apply` + // because this is `bind`'s unused context argument(`new` will + // explicitly create a new context). + var constructor = object[name]; + var factoryFunction = constructor.bind.apply(constructor, [null, ...args]); + return new factoryFunction(); + }, + eval: function(string) { + eval(string); } }; diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart index 658a6185259..6631460be32 100644 --- a/pkg/dart2wasm/lib/code_generator.dart +++ b/pkg/dart2wasm/lib/code_generator.dart @@ -1181,7 +1181,7 @@ class CodeGenerator extends ExpressionVisitor1 @override w.ValueType visitEqualsNull(EqualsNull node, w.ValueType expectedType) { - wrap(node.expression, translator.topInfo.nullableType); + wrap(node.expression, const w.RefType.any()); b.ref_is_null(); return w.NumType.i32; } diff --git a/pkg/dart2wasm/lib/target.dart b/pkg/dart2wasm/lib/target.dart index 15e6651ab4c..cb5310be62b 100644 --- a/pkg/dart2wasm/lib/target.dart +++ b/pkg/dart2wasm/lib/target.dart @@ -46,6 +46,7 @@ class WasmTarget extends Target { 'dart:_internal', 'dart:typed_data', 'dart:nativewrappers', + 'dart:js_util_wasm', ]; @override diff --git a/sdk/lib/_internal/wasm/lib/js_util_wasm_patch.dart b/sdk/lib/_internal/wasm/lib/js_util_wasm_patch.dart new file mode 100644 index 00000000000..f4e75123c5c --- /dev/null +++ b/sdk/lib/_internal/wasm/lib/js_util_wasm_patch.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library dart.js_util_wasm; + +import "dart:_internal"; +import "dart:js_util_wasm"; +import "dart:wasm"; + +/// js_util_wasm methods used by the wasm runtime. +@pragma("wasm:export", "\$listLength") +double _listLength(List list) => list.length.toDouble(); + +@pragma("wasm:export", "\$listRead") +WasmAnyRef? _listRead(List list, double index) => + jsifyRaw(list[index.toInt()]); + +@pragma("wasm:export", "\$listAllocate") +List _listAllocate() => []; + +@pragma("wasm:export", "\$listAdd") +void _listAdd(List list, WasmAnyRef? item) => + list.add(dartifyRaw(item)); + +@patch +Object _jsObjectToDartObject(WasmAnyRef ref) => unsafeCastOpaque(ref); + +@patch +WasmAnyRef _jsObjectFromDartObject(Object object) => + unsafeCastOpaque(object); diff --git a/sdk/lib/js_util/js_util_sources.gni b/sdk/lib/js_util/js_util_sources.gni index d3ac6d24d23..011df2e9746 100644 --- a/sdk/lib/js_util/js_util_sources.gni +++ b/sdk/lib/js_util/js_util_sources.gni @@ -2,4 +2,7 @@ # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. -js_util_sdk_sources = [ "js_util.dart" ] +js_util_sdk_sources = [ + "js_util.dart", + "js_util_wasm.dart", +] diff --git a/sdk/lib/js_util/js_util_wasm.dart b/sdk/lib/js_util/js_util_wasm.dart new file mode 100644 index 00000000000..49b4a900086 --- /dev/null +++ b/sdk/lib/js_util/js_util_wasm.dart @@ -0,0 +1,164 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Prototype js util library for wasm. +library dart.js_util_wasm; + +import 'dart:wasm'; + +/// [JSValue] is the root of the JS interop object hierarchy. +class JSValue { + final WasmAnyRef _ref; + + JSValue(this._ref); + + static JSValue? box(WasmAnyRef? ref) => ref == null ? null : JSValue(ref); + + WasmAnyRef toAnyRef() => _ref; + String toString() => _jsStringToDartString(_ref); + List toObjectList() => _jsArrayToDartList(_ref); + Object toObject() => _jsObjectToDartObject(_ref); +} + +/// Raw private JS functions. +external WasmAnyRef _jsObjectFromDartObject(Object object); + +external Object _jsObjectToDartObject(WasmAnyRef ref); + +@pragma("wasm:import", "dart2wasm.arrayFromDartList") +external WasmAnyRef _jsArrayFromDartList(List list); + +@pragma("wasm:import", "dart2wasm.arrayToDartList") +external List _jsArrayToDartList(WasmAnyRef list); + +@pragma("wasm:import", "dart2wasm.stringFromDartString") +external WasmAnyRef _jsStringFromDartString(String string); + +@pragma("wasm:import", "dart2wasm.stringToDartString") +external String _jsStringToDartString(WasmAnyRef string); + +/// Raw public JS functions. +/// These are public temporarily to give performance conscious users an escape +/// hatch while we decide what this API will actually look like. They may +/// become private in the future, or disappear entirely. For descriptions of the +/// API, please see the corresponding non-raw functions. +@pragma("wasm:import", "dart2wasm.eval") +external void evalRaw(WasmAnyRef code); + +@pragma("wasm:import", "dart2wasm.dartify") +external WasmAnyRef? dartifyRaw(WasmAnyRef? object); + +@pragma("wasm:import", "dart2wasm.newObject") +external WasmAnyRef newObjectRaw(); + +@pragma("wasm:import", "dart2wasm.globalThis") +external WasmAnyRef globalThisRaw(); + +@pragma("wasm:import", "dart2wasm.callConstructorVarArgs") +external WasmAnyRef callConstructorVarArgsRaw( + WasmAnyRef o, WasmAnyRef name, WasmAnyRef args); + +@pragma("wasm:import", "dart2wasm.hasProperty") +external bool hasPropertyRaw(WasmAnyRef o, WasmAnyRef name); + +@pragma("wasm:import", "dart2wasm.getProperty") +external WasmAnyRef? getPropertyRaw(WasmAnyRef o, WasmAnyRef name); + +@pragma("wasm:import", "dart2wasm.setProperty") +external WasmAnyRef? setPropertyRaw( + WasmAnyRef o, WasmAnyRef name, WasmAnyRef? value); + +@pragma("wasm:import", "dart2wasm.callMethodVarArgs") +external WasmAnyRef? callMethodVarArgsRaw( + WasmAnyRef o, WasmAnyRef method, WasmAnyRef? args); + +WasmAnyRef? jsifyRaw(Object? object) { + if (object == null) { + return null; + } else if (object is JSValue) { + return object.toAnyRef(); + } else if (object is String) { + return _jsStringFromDartString(object); + } else if (object is List) { + return _jsArrayFromDartList(object); + } else { + return _jsObjectFromDartObject(object); + } +} + +/// Conversion functions. +/// TODO(joshualitt): Only a small set of types currently work: +/// JS -> Dart: +/// null +/// strings +/// arrays +/// opaque Dart objects passed to JS +/// Dart -> JS: +/// null +/// boolean +/// doubles +/// strings +/// lists +/// opaque JS objects passed to Dart +/// In the future we would like to support more types, at least maps, +/// and to fix some of the issues returning some types from JS. + +/// Extension methods for conversions. +extension StringToJS on String { + JSValue toJS() => JSValue(_jsStringFromDartString(this)); +} + +extension ListOfObjectToJS on List { + JSValue toJS() => JSValue(_jsArrayFromDartList(this)); +} + +extension ObjectToJS on Object { + JSValue toJS() => JSValue(_jsObjectFromDartObject(this)); +} + +/// Recursively converts objects from Dart to JS. +JSValue? jsify(Object? object) => JSValue.box(jsifyRaw(object)); + +/// Recursively converts objects from JS to Dart. +Object? dartify(JSValue? object) => object == null + ? null + : _jsObjectToDartObject(dartifyRaw(object.toAnyRef())!); + +/// js util methods. +/// These are low level calls into JS, and require care to use correctly. + +/// Evals a snippet of JS code in a Dart string. +void eval(String code) => evalRaw(code.toJS().toAnyRef()); + +/// Creates a new JS object literal and returns it. +JSValue newObject() => JSValue(newObjectRaw()); + +/// Returns a reference to `globalThis`. +JSValue globalThis() => JSValue(globalThisRaw()); + +/// Gets a [String] name property off of a JS object [o], invokes it as +/// a constructor with a JS array of arguments [args], and returns the +/// constructed JS object. +JSValue callConstructorVarArgs(JSValue o, String name, List args) => + JSValue(callConstructorVarArgsRaw( + o.toAnyRef(), name.toJS().toAnyRef(), args.toJS().toAnyRef())); + +/// Checks for a [String] name on a JS object [o]. +bool hasProperty(JSValue o, String name) => + hasPropertyRaw(o.toAnyRef(), name.toJS().toAnyRef()); + +/// Gets a JS property with [String] name off of a JS object [o]. +JSValue? getProperty(JSValue o, String name) => + JSValue.box(getPropertyRaw(o.toAnyRef(), name.toJS().toAnyRef())); + +/// Sets a JS property with [String] name on JS object [o] to the JS value +/// [value], then returns [value]. +JSValue? setProperty(JSValue o, String name, JSValue? value) => JSValue.box( + setPropertyRaw(o.toAnyRef(), name.toJS().toAnyRef(), value?.toAnyRef())); + +/// Calls a JS method with a [String] name on JS object [o] with a JS array +/// of arguments [args] and returns the resulting JS value. +JSValue? callMethodVarArgs(JSValue o, String method, List args) => + JSValue.box(callMethodVarArgsRaw( + o.toAnyRef(), method.toJS().toAnyRef(), args.toJS().toAnyRef())); diff --git a/sdk/lib/libraries.json b/sdk/lib/libraries.json index dae53f77a8f..212955ea4ff 100644 --- a/sdk/lib/libraries.json +++ b/sdk/lib/libraries.json @@ -232,6 +232,10 @@ "isolate": { "uri": "isolate/isolate.dart" }, + "js_util_wasm": { + "uri": "js_util/js_util_wasm.dart", + "patches": "_internal/wasm/lib/js_util_wasm_patch.dart" + }, "math": { "uri": "math/math.dart", "patches": "_internal/wasm/lib/math_patch.dart" diff --git a/sdk/lib/libraries.yaml b/sdk/lib/libraries.yaml index ca10300448d..e0a8e4261f6 100644 --- a/sdk/lib/libraries.yaml +++ b/sdk/lib/libraries.yaml @@ -216,6 +216,9 @@ wasm: uri: "html/dartium/nativewrappers.dart" isolate: uri: isolate/isolate.dart + js_util_wasm: + uri: js_util/js_util_wasm.dart + patches: _internal/wasm/lib/js_util_wasm_patch.dart math: uri: math/math.dart patches: _internal/wasm/lib/math_patch.dart diff --git a/tests/web/wasm/js_util_test.dart b/tests/web/wasm/js_util_test.dart new file mode 100644 index 00000000000..a5b31c1c3f1 --- /dev/null +++ b/tests/web/wasm/js_util_test.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:js_util_wasm'; + +import 'package:expect/expect.dart'; + +void createObjectTest() { + JSValue o = newObject(); + Expect.isFalse(hasProperty(o, 'foo')); + Expect.equals('bar', setProperty(o, 'foo', 'bar'.toJS()).toString()); + Expect.isTrue(hasProperty(o, 'foo')); + Expect.equals('bar', getProperty(o, 'foo').toString()); +} + +// Unfortunately, lists do not currently compare identically. +void _expectListEquals(List l, List r) { + Expect.equals(l.length, r.length); + for (int i = 0; i < l.length; i++) { + Expect.equals(l[i], r[i]); + } +} + +void evalAndConstructTest() { + eval(r''' + function JSClass(c) { + this.c = c; + this.sum = (a, b) => { + return a + b + this.c; + } + this.list = ['a', 'b', 'c']; + } + globalThis.JSClass = JSClass; + '''); + JSValue gt = globalThis(); + JSValue jsClass = callConstructorVarArgs(gt, 'JSClass', ['world!'.toJS()]); + Expect.equals( + 'hello world!', + callMethodVarArgs(jsClass, 'sum', ['hello'.toJS(), ' '.toJS()]) + .toString()); + _expectListEquals( + ['a', 'b', 'c'], getProperty(jsClass, 'list')!.toObjectList()); +} + +class Foo { + final int i; + Foo(this.i); +} + +void dartObjectRoundTripTest() { + JSValue o = newObject(); + setProperty(o, 'foo', Foo(4).toJS()); + Object foo = getProperty(o, 'foo')!.toObject(); + Expect.equals(4, (foo as Foo).i); +} + +void deepConversionsTest() { + // Dart to JS. + Expect.isNull(dartify(jsify(null))); + Expect.equals(true, dartify(jsify(true))); + Expect.equals(2.0, dartify(jsify(2.0))); + Expect.equals('foo', dartify(jsify('foo'))); + _expectListEquals( + ['a', 'b', 'c'], dartify(jsify(['a', 'b', 'c'])) as List); + + // JS to Dart. + eval(r''' + globalThis.a = null; + globalThis.b = 'foo'; + globalThis.c = ['a', 'b', 'c']; + '''); + JSValue gt = globalThis(); + Expect.isNull(dartify(getProperty(gt, 'a'))); + Expect.equals('foo', dartify(getProperty(gt, 'b'))); + _expectListEquals( + ['a', 'b', 'c'], dartify(getProperty(gt, 'c')) as List); +} + +void main() { + createObjectTest(); + evalAndConstructTest(); + dartObjectRoundTripTest(); + deepConversionsTest(); +} diff --git a/tests/web/web.status b/tests/web/web.status index c54cd80d70a..d20e13611fc 100644 --- a/tests/web/web.status +++ b/tests/web/web.status @@ -5,6 +5,9 @@ [ $compiler != dart2js ] dummy_compiler_test: SkipByDesign # Issue 30773. Test should be migrated as a unit test of dart2js, is only intended to test self-hosting. +[ $compiler != dart2wasm ] +wasm/*: SkipByDesign + [ $runtime == jsshell ] deferred/load_in_correct_order_test: SkipByDesign # jsshell preamble does not support this test. diff --git a/tools/bots/test_matrix.json b/tools/bots/test_matrix.json index cf5a954e718..4f67da1242d 100644 --- a/tools/bots/test_matrix.json +++ b/tools/bots/test_matrix.json @@ -2969,7 +2969,8 @@ "arguments": [ "-ndart2wasm-hostasserts-linux-x64-d8", "language", - "corelib" + "corelib", + "web/wasm" ], "shards": 30, "fileset": "dart2wasm_hostasserts"