Index: sdk/lib/js/dartium/js_dartium.dart |
diff --git a/sdk/lib/js/dartium/js_dartium.dart b/sdk/lib/js/dartium/js_dartium.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..393320f91ffd8f70fe532342e4164d54a8e452d1 |
--- /dev/null |
+++ b/sdk/lib/js/dartium/js_dartium.dart |
@@ -0,0 +1,371 @@ |
+// Copyright (c) 2013, 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. |
+ |
+/** |
+ * The js.dart library provides simple JavaScript invocation from Dart that |
+ * works on both Dartium and on other modern browsers via Dart2JS. |
+ * |
+ * It provides a model based on scoped [JsObject] objects. Proxies give Dart |
+ * code access to JavaScript objects, fields, and functions as well as the |
+ * ability to pass Dart objects and functions to JavaScript functions. Scopes |
+ * enable developers to use proxies without memory leaks - a common challenge |
+ * with cross-runtime interoperation. |
+ * |
+ * The top-level [context] getter provides a [JsObject] to the global JavaScript |
+ * context for the page your Dart code is running on. In the following example: |
+ * |
+ * import 'dart:js'; |
+ * |
+ * void main() { |
+ * context.callMethod('alert', ['Hello from Dart via JavaScript']); |
+ * } |
+ * |
+ * context['alert'] creates a proxy to the top-level alert function in |
+ * JavaScript. It is invoked from Dart as a regular function that forwards to |
+ * the underlying JavaScript one. By default, proxies are released when |
+ * the currently executing event completes, e.g., when main is completes |
+ * in this example. |
+ * |
+ * The library also enables JavaScript proxies to Dart objects and functions. |
+ * For example, the following Dart code: |
+ * |
+ * context['dartCallback'] = new Callback.once((x) => print(x*2)); |
+ * |
+ * defines a top-level JavaScript function 'dartCallback' that is a proxy to |
+ * the corresponding Dart function. The [Callback.once] constructor allows the |
+ * proxy to the Dart function to be retained across multiple events; |
+ * instead it is released after the first invocation. (This is a common |
+ * pattern for asychronous callbacks.) |
+ * |
+ * Note, parameters and return values are intuitively passed by value for |
+ * primitives and by reference for non-primitives. In the latter case, the |
+ * references are automatically wrapped and unwrapped as proxies by the library. |
+ * |
+ * This library also allows construction of JavaScripts objects given a |
+ * [JsObject] to a corresponding JavaScript constructor. For example, if the |
+ * following JavaScript is loaded on the page: |
+ * |
+ * function Foo(x) { |
+ * this.x = x; |
+ * } |
+ * |
+ * Foo.prototype.add = function(other) { |
+ * return new Foo(this.x + other.x); |
+ * } |
+ * |
+ * then, the following Dart: |
+ * |
+ * var foo = new JsObject(context['Foo'], [42]); |
+ * var foo2 = foo.callMethod('add', [foo]); |
+ * print(foo2['x']); |
+ * |
+ * will construct a JavaScript Foo object with the parameter 42, invoke its |
+ * add method, and return a [JsObject] to a new Foo object whose x field is 84. |
+ */ |
+ |
+library dart.js; |
+ |
+import 'dart:html'; |
+import 'dart:isolate'; |
+ |
+// Global ports to manage communication from Dart to JS. |
+SendPortSync _jsPortSync = window.lookupPort('dart-js-context'); |
+SendPortSync _jsPortCreate = window.lookupPort('dart-js-create'); |
+SendPortSync _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); |
+SendPortSync _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property'); |
+SendPortSync _jsPortConvert = window.lookupPort('dart-js-convert'); |
+ |
+/** |
+ * Returns a proxy to the global JavaScript context for this page. |
+ */ |
+JsObject get context => _deserialize(_jsPortSync.callSync([])); |
+ |
+/** |
+ * Converts a json-like [data] to a JavaScript map or array and return a |
+ * [JsObject] to it. |
+ */ |
+JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); |
+ |
+/** |
+ * Converts a local Dart function to a callback that can be passed to |
+ * JavaScript. |
+ */ |
+class Callback implements Serializable<JsFunction> { |
+ JsFunction _f; |
+ |
+ Callback._(Function f, bool withThis) { |
+ final id = _proxiedObjectTable.add((List args) { |
+ final arguments = new List.from(args); |
+ if (!withThis) arguments.removeAt(0); |
+ return Function.apply(f, arguments); |
+ }); |
+ _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id); |
+ } |
+ |
+ factory Callback(Function f) => new Callback._(f, false); |
+ factory Callback.withThis(Function f) => new Callback._(f, true); |
+ |
+ JsFunction toJs() => _f; |
+} |
+ |
+/** |
+ * Proxies to JavaScript objects. |
+ */ |
+class JsObject implements Serializable<JsObject> { |
+ final SendPortSync _port; |
+ final String _id; |
+ |
+ /** |
+ * Constructs a [JsObject] to a new JavaScript object by invoking a (proxy to |
+ * a) JavaScript [constructor]. The [arguments] list should contain either |
+ * primitive values, DOM elements, or Proxies. |
+ */ |
+ factory JsObject(Serializable<JsFunction> constructor, [List arguments]) { |
+ final params = [constructor]; |
+ if (arguments != null) params.addAll(arguments); |
+ final serialized = params.map(_serialize).toList(); |
+ final result = _jsPortCreate.callSync(serialized); |
+ return _deserialize(result); |
+ } |
+ |
+ /** |
+ * Constructs a [JsObject] to a new JavaScript map or list created defined via |
+ * Dart map or list. |
+ */ |
+ factory JsObject._json(data) => _convert(data); |
+ |
+ static _convert(data) => |
+ _deserialize(_jsPortConvert.callSync(_serializeDataTree(data))); |
+ |
+ static _serializeDataTree(data) { |
+ if (data is Map) { |
+ final entries = new List(); |
+ for (var key in data.keys) { |
+ entries.add([key, _serializeDataTree(data[key])]); |
+ } |
+ return ['map', entries]; |
+ } else if (data is Iterable) { |
+ return ['list', data.map(_serializeDataTree).toList()]; |
+ } else { |
+ return ['simple', _serialize(data)]; |
+ } |
+ } |
+ |
+ JsObject._internal(this._port, this._id); |
+ |
+ JsObject toJs() => this; |
+ |
+ // Resolve whether this is needed. |
+ operator[](arg) => _forward(this, '[]', 'method', [ arg ]); |
+ |
+ // Resolve whether this is needed. |
+ operator[]=(key, value) => _forward(this, '[]=', 'method', [ key, value ]); |
+ |
+ int get hashCode => _id.hashCode; |
+ |
+ // Test if this is equivalent to another Proxy. This essentially |
+ // maps to JavaScript's === operator. |
+ operator==(other) => other is JsObject && this._id == other._id; |
+ |
+ /** |
+ * Check if this [JsObject] has a [name] property. |
+ */ |
+ bool hasProperty(String name) => _forward(this, name, 'hasProperty', []); |
+ |
+ /** |
+ * Delete the [name] property. |
+ */ |
+ void deleteProperty(String name) { |
+ _jsPortDeleteProperty.callSync([this, name].map(_serialize).toList()); |
+ } |
+ |
+ /** |
+ * Check if this [JsObject] is instance of [type]. |
+ */ |
+ bool instanceof(Serializable<JsFunction> type) => |
+ _jsPortInstanceof.callSync([this, type].map(_serialize).toList()); |
+ |
+ String toString() { |
+ try { |
+ return _forward(this, 'toString', 'method', []); |
+ } catch(e) { |
+ return super.toString(); |
+ } |
+ } |
+ |
+ callMethod(String name, [List args]) { |
+ return _forward(this, name, 'method', args != null ? args : []); |
+ } |
+ |
+ // Forward member accesses to the backing JavaScript object. |
+ static _forward(JsObject receiver, String member, String kind, List args) { |
+ var result = receiver._port.callSync([receiver._id, member, kind, |
+ args.map(_serialize).toList()]); |
+ switch (result[0]) { |
+ case 'return': return _deserialize(result[1]); |
+ case 'throws': throw _deserialize(result[1]); |
+ case 'none': throw new NoSuchMethodError(receiver, member, args, {}); |
+ default: throw 'Invalid return value'; |
+ } |
+ } |
+} |
+ |
+/// A [JsObject] subtype to JavaScript functions. |
+class JsFunction extends JsObject implements Serializable<JsFunction> { |
+ JsFunction._internal(SendPortSync port, String id) |
+ : super._internal(port, id); |
+ |
+ apply(thisArg, [List args]) { |
+ return JsObject._forward(this, '', 'apply', |
+ [thisArg]..addAll(args == null ? [] : args)); |
+ } |
+} |
+ |
+/// Marker class used to indicate it is serializable to js. If a class is a |
+/// [Serializable] the "toJs" method will be called and the result will be used |
+/// as value. |
+abstract class Serializable<T> { |
+ T toJs(); |
+} |
+ |
+// A table to managed local Dart objects that are proxied in JavaScript. |
+class _ProxiedObjectTable { |
+ // Debugging name. |
+ final String _name; |
+ |
+ // Generator for unique IDs. |
+ int _nextId; |
+ |
+ // Table of IDs to Dart objects. |
+ final Map<String, Object> _registry; |
+ |
+ // Port to handle and forward requests to the underlying Dart objects. |
+ // A remote proxy is uniquely identified by an ID and SendPortSync. |
+ final ReceivePortSync _port; |
+ |
+ _ProxiedObjectTable() : |
+ _name = 'dart-ref', |
+ _nextId = 0, |
+ _registry = {}, |
+ _port = new ReceivePortSync() { |
+ _port.receive((msg) { |
+ try { |
+ final receiver = _registry[msg[0]]; |
+ final method = msg[1]; |
+ final args = msg[2].map(_deserialize).toList(); |
+ if (method == '#call') { |
+ final func = receiver as Function; |
+ var result = _serialize(func(args)); |
+ return ['return', result]; |
+ } else { |
+ // TODO(vsm): Support a mechanism to register a handler here. |
+ throw 'Invocation unsupported on non-function Dart proxies'; |
+ } |
+ } catch (e) { |
+ // TODO(vsm): callSync should just handle exceptions itself. |
+ return ['throws', '$e']; |
+ } |
+ }); |
+ } |
+ |
+ // Adds a new object to the table and return a new ID for it. |
+ String add(x) { |
+ // TODO(vsm): Cache x and reuse id. |
+ final id = '$_name-${_nextId++}'; |
+ _registry[id] = x; |
+ return id; |
+ } |
+ |
+ // Gets an object by ID. |
+ Object get(String id) { |
+ return _registry[id]; |
+ } |
+ |
+ // Gets the current number of objects kept alive by this table. |
+ get count => _registry.length; |
+ |
+ // Gets a send port for this table. |
+ get sendPort => _port.toSendPort(); |
+} |
+ |
+// The singleton to manage proxied Dart objects. |
+_ProxiedObjectTable _proxiedObjectTable = new _ProxiedObjectTable(); |
+ |
+/// End of proxy implementation. |
+ |
+// Dart serialization support. |
+ |
+_serialize(var message) { |
+ if (message == null) { |
+ return null; // Convert undefined to null. |
+ } else if (message is String || |
+ message is num || |
+ message is bool) { |
+ // Primitives are passed directly through. |
+ return message; |
+ } else if (message is SendPortSync) { |
+ // Non-proxied objects are serialized. |
+ return message; |
+ } else if (message is JsFunction) { |
+ // Remote function proxy. |
+ return [ 'funcref', message._id, message._port ]; |
+ } else if (message is JsObject) { |
+ // Remote object proxy. |
+ return [ 'objref', message._id, message._port ]; |
+ } else if (message is Serializable) { |
+ // use of result of toJs() |
+ return _serialize(message.toJs()); |
+ } else if (message is Function) { |
+ return _serialize(new Callback(message)); |
+ } else { |
+ // Local object proxy. |
+ return [ 'objref', |
+ _proxiedObjectTable.add(message), |
+ _proxiedObjectTable.sendPort ]; |
+ } |
+} |
+ |
+_deserialize(var message) { |
+ deserializeFunction(message) { |
+ var id = message[1]; |
+ var port = message[2]; |
+ if (port == _proxiedObjectTable.sendPort) { |
+ // Local function. |
+ return _proxiedObjectTable.get(id); |
+ } else { |
+ // Remote function. Forward to its port. |
+ return new JsFunction._internal(port, id); |
+ } |
+ } |
+ |
+ deserializeObject(message) { |
+ var id = message[1]; |
+ var port = message[2]; |
+ if (port == _proxiedObjectTable.sendPort) { |
+ // Local object. |
+ return _proxiedObjectTable.get(id); |
+ } else { |
+ // Remote object. |
+ return new JsObject._internal(port, id); |
+ } |
+ } |
+ |
+ if (message == null) { |
+ return null; // Convert undefined to null. |
+ } else if (message is String || |
+ message is num || |
+ message is bool) { |
+ // Primitives are passed directly through. |
+ return message; |
+ } else if (message is SendPortSync) { |
+ // Serialized type. |
+ return message; |
+ } |
+ var tag = message[0]; |
+ switch (tag) { |
+ case 'funcref': return deserializeFunction(message); |
+ case 'objref': return deserializeObject(message); |
+ } |
+ throw 'Unsupported serialized data: $message'; |
+} |