Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1385)

Unified Diff: sdk/lib/js/dartium/js_dartium.dart

Issue 15782009: RFC: introduce dart:js (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: remove Element handling + fix js_dartium for passing tests Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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..b8a64aff5ba2f6f5dd1d81615fc016cb36e1bb39
--- /dev/null
+++ b/sdk/lib/js/dartium/js_dartium.dart
@@ -0,0 +1,1124 @@
+// 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:async';
+import 'dart:html';
+import 'dart:isolate';
+import 'dart:mirrors';
+
+// JavaScript bootstrapping code.
+// TODO(vsm): Migrate this to use a builtin resource mechanism once we have
+// one.
+
+// NOTE: Please re-run tools/create_bootstrap.dart on any modification of
+// this bootstrap string.
vsm 2013/06/11 16:17:44 I just landed my CL with interop.js. Shall we mov
alexandre.ardhuin 2013/06/12 21:29:19 Done.
+final _JS_BOOTSTRAP = r"""
+(function() {
+ // Proxy support for js.dart.
+
+ var globalContext = window;
+
+ // Support for binding the receiver (this) in proxied functions.
+ function bindIfFunction(f, _this) {
+ if (typeof(f) != "function") {
+ return f;
+ } else {
+ return new BoundFunction(_this, f);
+ }
+ }
+
+ function unbind(obj) {
+ if (obj instanceof BoundFunction) {
+ return obj.object;
+ } else {
+ return obj;
+ }
+ }
+
+ function BoundFunction(_this, object) {
+ this._this = _this;
+ this.object = object;
+ }
+
+ // Table for local objects and functions that are proxied.
+ function ProxiedObjectTable() {
+ // Name for debugging.
+ this.name = 'js-ref';
+
+ // Table from IDs to JS objects.
+ this.map = {};
+
+ // Generator for new IDs.
+ this._nextId = 0;
+
+ // Counter for deleted proxies.
+ this._deletedCount = 0;
+
+ // Flag for one-time initialization.
+ this._initialized = false;
+
+ // Ports for managing communication to proxies.
+ this.port = new ReceivePortSync();
+ this.sendPort = this.port.toSendPort();
+
+ // Set of IDs that are global.
+ // These will not be freed on an exitScope().
+ this.globalIds = {};
+
+ // Stack of scoped handles.
+ this.handleStack = [];
+
+ // Stack of active scopes where each value is represented by the size of
+ // the handleStack at the beginning of the scope. When an active scope
+ // is popped, the handleStack is restored to where it was when the
+ // scope was entered.
+ this.scopeIndices = [];
+ }
+
+ // Number of valid IDs. This is the number of objects (global and local)
+ // kept alive by this table.
+ ProxiedObjectTable.prototype.count = function () {
+ return Object.keys(this.map).length;
+ }
+
+ // Number of total IDs ever allocated.
+ ProxiedObjectTable.prototype.total = function () {
+ return this.count() + this._deletedCount;
+ }
+
+ // Adds an object to the table and return an ID for serialization.
+ ProxiedObjectTable.prototype.add = function (obj) {
+ if (this.scopeIndices.length == 0) {
+ throw "Cannot allocate a proxy outside of a scope.";
+ }
+ // TODO(vsm): Cache refs for each obj?
+ var ref = this.name + '-' + this._nextId++;
+ this.handleStack.push(ref);
+ this.map[ref] = obj;
+ return ref;
+ }
+
+ ProxiedObjectTable.prototype._initializeOnce = function () {
+ if (!this._initialized) {
+ this._initialize();
+ this._initialized = true;
+ }
+ }
+
+ // Enters a new scope for this table.
+ ProxiedObjectTable.prototype.enterScope = function() {
+ this._initializeOnce();
+ this.scopeIndices.push(this.handleStack.length);
+ }
+
+ // Invalidates all non-global IDs in the current scope and
+ // exit the current scope.
+ ProxiedObjectTable.prototype.exitScope = function() {
+ var start = this.scopeIndices.pop();
+ for (var i = start; i < this.handleStack.length; ++i) {
+ var key = this.handleStack[i];
+ if (!this.globalIds.hasOwnProperty(key)) {
+ delete this.map[this.handleStack[i]];
+ this._deletedCount++;
+ }
+ }
+ this.handleStack = this.handleStack.splice(0, start);
+ }
+
+ // Makes this ID globally scope. It must be explicitly invalidated.
+ ProxiedObjectTable.prototype.globalize = function(id) {
+ this.globalIds[id] = true;
+ }
+
+ // Invalidates this ID, potentially freeing its corresponding object.
+ ProxiedObjectTable.prototype.invalidate = function(id) {
+ var old = this.get(id);
+ delete this.globalIds[id];
+ delete this.map[id];
+ this._deletedCount++;
+ }
+
+ // Gets the object or function corresponding to this ID.
+ ProxiedObjectTable.prototype.get = function (id) {
+ if (!this.map.hasOwnProperty(id)) {
+ throw 'Proxy ' + id + ' has been invalidated.'
+ }
+ return this.map[id];
+ }
+
+ ProxiedObjectTable.prototype._initialize = function () {
+ // Configure this table's port to forward methods, getters, and setters
+ // from the remote proxy to the local object.
+ var table = this;
+
+ this.port.receive(function (message) {
+ // TODO(vsm): Support a mechanism to register a handler here.
+ try {
+ var object = table.get(message[0]);
+ var receiver = unbind(object);
+ var member = message[1];
+ var kind = message[2];
+ var args = message[3].map(deserialize);
+ if (kind == 'get') {
+ // Getter.
+ var field = member;
+ if (field in receiver && args.length == 0) {
+ var result = bindIfFunction(receiver[field], receiver);
+ return [ 'return', serialize(result) ];
+ }
+ } else if (kind == 'set') {
+ // Setter.
+ var field = member;
+ if (args.length == 1) {
+ return [ 'return', serialize(receiver[field] = args[0]) ];
+ }
+ } else if (kind == 'hasProperty') {
+ var field = member;
+ return [ 'return', field in receiver ];
+ } else if (kind == 'apply') {
+ // Direct function invocation.
+ return [ 'return', serialize(receiver.apply(args[0], args.slice(1))) ];
+ } else if (member == '[]' && args.length == 1) {
+ // Index getter.
+ var result = bindIfFunction(receiver[args[0]], receiver);
+ return [ 'return', serialize(result) ];
+ } else if (member == '[]=' && args.length == 2) {
+ // Index setter.
+ return [ 'return', serialize(receiver[args[0]] = args[1]) ];
+ } else {
+ // Member function invocation.
+ var f = receiver[member];
+ if (f) {
+ var result = f.apply(receiver, args);
+ return [ 'return', serialize(result) ];
+ }
+ }
+ return [ 'none' ];
+ } catch (e) {
+ return [ 'throws', e.toString() ];
+ }
+ });
+ }
+
+ // Singleton for local proxied objects.
+ var proxiedObjectTable = new ProxiedObjectTable();
+
+ // Type for remote proxies to Dart objects.
+ function DartProxy(id, sendPort) {
+ this.id = id;
+ this.port = sendPort;
+ }
+
+ // Serializes JS types to SendPortSync format:
+ // - primitives -> primitives
+ // - sendport -> sendport
+ // - Function -> [ 'funcref', function-id, sendport ]
+ // - Object -> [ 'objref', object-id, sendport ]
+ function serialize(message) {
+ if (message == null) {
+ return null; // Convert undefined to null.
+ } else if (typeof(message) == 'string' ||
+ typeof(message) == 'number' ||
+ typeof(message) == 'boolean') {
+ // Primitives are passed directly through.
+ return message;
+ } else if (message instanceof SendPortSync) {
+ // Non-proxied objects are serialized.
+ return message;
+ } else if (message instanceof BoundFunction &&
+ typeof(message.object) == 'function') {
+ // Local function proxy.
+ return [ 'funcref',
+ proxiedObjectTable.add(message),
+ proxiedObjectTable.sendPort ];
+ } else if (typeof(message) == 'function') {
+ if ('_dart_id' in message) {
+ // Remote function proxy.
+ var remoteId = message._dart_id;
+ var remoteSendPort = message._dart_port;
+ return [ 'funcref', remoteId, remoteSendPort ];
+ } else {
+ // Local function proxy.
+ return [ 'funcref',
+ proxiedObjectTable.add(message),
+ proxiedObjectTable.sendPort ];
+ }
+ } else if (message instanceof DartProxy) {
+ // Remote object proxy.
+ return [ 'objref', message.id, message.port ];
+ } else {
+ // Local object proxy.
+ return [ 'objref',
+ proxiedObjectTable.add(message),
+ proxiedObjectTable.sendPort ];
+ }
+ }
+
+ function deserialize(message) {
+ if (message == null) {
+ return null; // Convert undefined to null.
+ } else if (typeof(message) == 'string' ||
+ typeof(message) == 'number' ||
+ typeof(message) == 'boolean') {
+ // Primitives are passed directly through.
+ return message;
+ } else if (message instanceof 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;
+ }
+
+ // Create a local function that forwards to the remote function.
+ function deserializeFunction(message) {
+ var id = message[1];
+ var port = message[2];
+ // TODO(vsm): Add a more robust check for a local SendPortSync.
+ if ("receivePort" in port) {
+ // Local function.
+ return unbind(proxiedObjectTable.get(id));
+ } else {
+ // Remote function. Forward to its port.
+ var f = function () {
+ var depth = enterScope();
+ try {
+ var args = Array.prototype.slice.apply(arguments);
+ args.splice(0, 0, this);
+ args = args.map(serialize);
+ var result = port.callSync([id, '#call', args]);
+ if (result[0] == 'throws') throw deserialize(result[1]);
+ return deserialize(result[1]);
+ } finally {
+ exitScope(depth);
+ }
+ };
+ // Cache the remote id and port.
+ f._dart_id = id;
+ f._dart_port = port;
+ return f;
+ }
+ }
+
+ // Creates a DartProxy to forwards to the remote object.
+ function deserializeObject(message) {
+ var id = message[1];
+ var port = message[2];
+ // TODO(vsm): Add a more robust check for a local SendPortSync.
+ if ("receivePort" in port) {
+ // Local object.
+ return proxiedObjectTable.get(id);
+ } else {
+ // Remote object.
+ return new DartProxy(id, port);
+ }
+ }
+
+ // Remote handler to construct a new JavaScript object given its
+ // serialized constructor and arguments.
+ function construct(args) {
+ args = args.map(deserialize);
+ var constructor = unbind(args[0]);
+ args = Array.prototype.slice.call(args, 1);
+
+ // Until 10 args, the 'new' operator is used. With more arguments we use a
+ // generic way that may not work, particularly when the constructor does not
+ // have an "apply" method.
+ var ret = null;
+ if (args.length === 0) {
+ ret = new constructor();
+ } else if (args.length === 1) {
+ ret = new constructor(args[0]);
+ } else if (args.length === 2) {
+ ret = new constructor(args[0], args[1]);
+ } else if (args.length === 3) {
+ ret = new constructor(args[0], args[1], args[2]);
+ } else if (args.length === 4) {
+ ret = new constructor(args[0], args[1], args[2], args[3]);
+ } else if (args.length === 5) {
+ ret = new constructor(args[0], args[1], args[2], args[3], args[4]);
+ } else if (args.length === 6) {
+ ret = new constructor(args[0], args[1], args[2], args[3], args[4],
+ args[5]);
+ } else if (args.length === 7) {
+ ret = new constructor(args[0], args[1], args[2], args[3], args[4],
+ args[5], args[6]);
+ } else if (args.length === 8) {
+ ret = new constructor(args[0], args[1], args[2], args[3], args[4],
+ args[5], args[6], args[7]);
+ } else if (args.length === 9) {
+ ret = new constructor(args[0], args[1], args[2], args[3], args[4],
+ args[5], args[6], args[7], args[8]);
+ } else if (args.length === 10) {
+ ret = new constructor(args[0], args[1], args[2], args[3], args[4],
+ args[5], args[6], args[7], args[8], args[9]);
+ } else {
+ // Dummy Type with correct constructor.
+ var Type = function(){};
+ Type.prototype = constructor.prototype;
+
+ // Create a new instance
+ var instance = new Type();
+
+ // Call the original constructor.
+ ret = constructor.apply(instance, args);
+ ret = Object(ret) === ret ? ret : instance;
+ }
+ return serialize(ret);
+ }
+
+ // Remote handler to return the top-level JavaScript context.
+ function context(data) {
+ return serialize(globalContext);
+ }
+
+ // Remote handler to track number of live / allocated proxies.
+ function proxyCount() {
+ var live = proxiedObjectTable.count();
+ var total = proxiedObjectTable.total();
+ return [live, total];
+ }
+
+ // Return true if two JavaScript proxies are equal (==).
+ function proxyEquals(args) {
+ return deserialize(args[0]) == deserialize(args[1]);
+ }
+
+ // Return true if a JavaScript proxy is instance of a given type (instanceof).
+ function proxyInstanceof(args) {
+ var obj = unbind(deserialize(args[0]));
+ var type = unbind(deserialize(args[1]));
+ return obj instanceof type;
+ }
+
+ // Return true if a JavaScript proxy is instance of a given type (instanceof).
+ function proxyDeleteProperty(args) {
+ var obj = unbind(deserialize(args[0]));
+ var member = unbind(deserialize(args[1]));
+ delete obj[member];
+ }
+
+ function proxyConvert(args) {
+ return serialize(deserializeDataTree(args));
+ }
+
+ function deserializeDataTree(data) {
+ var type = data[0];
+ var value = data[1];
+ if (type === 'map') {
+ var obj = {};
+ for (var i = 0; i < value.length; i++) {
+ obj[value[i][0]] = deserializeDataTree(value[i][1]);
+ }
+ return obj;
+ } else if (type === 'list') {
+ var list = [];
+ for (var i = 0; i < value.length; i++) {
+ list.push(deserializeDataTree(value[i]));
+ }
+ return list;
+ } else /* 'simple' */ {
+ return deserialize(value);
+ }
+ }
+
+ function makeGlobalPort(name, f) {
+ var port = new ReceivePortSync();
+ port.receive(f);
+ window.registerPort(name, port.toSendPort());
+ }
+
+ // Enters a new scope in the JavaScript context.
+ function enterJavaScriptScope() {
+ proxiedObjectTable.enterScope();
+ }
+
+ // Enters a new scope in both the JavaScript and Dart context.
+ var _dartEnterScopePort = null;
+ function enterScope() {
+ enterJavaScriptScope();
+ if (!_dartEnterScopePort) {
+ _dartEnterScopePort = window.lookupPort('js-dart-enter-scope');
+ }
+ return _dartEnterScopePort.callSync([]);
+ }
+
+ // Exits the current scope (and invalidate local IDs) in the JavaScript
+ // context.
+ function exitJavaScriptScope() {
+ proxiedObjectTable.exitScope();
+ }
+
+ // Exits the current scope in both the JavaScript and Dart context.
+ var _dartExitScopePort = null;
+ function exitScope(depth) {
+ exitJavaScriptScope();
+ if (!_dartExitScopePort) {
+ _dartExitScopePort = window.lookupPort('js-dart-exit-scope');
+ }
+ return _dartExitScopePort.callSync([ depth ]);
+ }
+
+ makeGlobalPort('dart-js-context', context);
+ makeGlobalPort('dart-js-create', construct);
+ makeGlobalPort('dart-js-proxy-count', proxyCount);
+ makeGlobalPort('dart-js-equals', proxyEquals);
+ makeGlobalPort('dart-js-instanceof', proxyInstanceof);
+ makeGlobalPort('dart-js-delete-property', proxyDeleteProperty);
+ makeGlobalPort('dart-js-convert', proxyConvert);
+ makeGlobalPort('dart-js-enter-scope', enterJavaScriptScope);
+ makeGlobalPort('dart-js-exit-scope', exitJavaScriptScope);
+ makeGlobalPort('dart-js-globalize', function(data) {
+ if (data[0] == "objref" || data[0] == "funcref") {
+ return proxiedObjectTable.globalize(data[1]);
+ }
+ throw 'Illegal type: ' + data[0];
+ });
+ makeGlobalPort('dart-js-invalidate', function(data) {
+ if (data[0] == "objref" || data[0] == "funcref") {
+ return proxiedObjectTable.invalidate(data[1]);
+ }
+ throw 'Illegal type: ' + data[0];
+ });
+})();
+""";
+
+// Injects JavaScript source code onto the page.
+// This is only used to load the bootstrapping code above.
+void _inject(code) {
vsm 2013/06/11 16:17:44 We can also eliminate this.
alexandre.ardhuin 2013/06/12 21:29:19 Done.
+ final script = new ScriptElement();
+ script.type = 'text/javascript';
+ script.innerHtml = code;
+ document.body.nodes.add(script);
+}
+
+// Global ports to manage communication from Dart to JS.
+SendPortSync _jsPortSync = null;
+SendPortSync _jsPortCreate = null;
+SendPortSync _jsPortProxyCount = null;
+SendPortSync _jsPortEquals = null;
+SendPortSync _jsPortInstanceof = null;
+SendPortSync _jsPortDeleteProperty = null;
+SendPortSync _jsPortConvert = null;
+SendPortSync _jsEnterJavaScriptScope = null;
+SendPortSync _jsExitJavaScriptScope = null;
+SendPortSync _jsGlobalize = null;
+SendPortSync _jsInvalidate = null;
+
+// Global ports to manage communication from JS to Dart.
+ReceivePortSync _dartEnterDartScope = null;
+ReceivePortSync _dartExitDartScope = null;
+
+// Initializes bootstrap code and ports.
+void _initialize() {
+ if (_jsPortSync != null) return;
+
+ // Test if the port is already defined.
+ try {
+ _jsPortSync = window.lookupPort('dart-js-context');
+ } catch (e) {
+ // TODO(vsm): Suppress the exception until dartbug.com/5854 is fixed.
+ }
+
+ // If not, try injecting the script.
+ if (_jsPortSync == null) {
+ _inject(_JS_BOOTSTRAP);
+ _jsPortSync = window.lookupPort('dart-js-context');
+ }
+
+ _jsPortCreate = window.lookupPort('dart-js-create');
+ _jsPortProxyCount = window.lookupPort('dart-js-proxy-count');
+ _jsPortEquals = window.lookupPort('dart-js-equals');
+ _jsPortInstanceof = window.lookupPort('dart-js-instanceof');
+ _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property');
+ _jsPortConvert = window.lookupPort('dart-js-convert');
+ _jsEnterJavaScriptScope = window.lookupPort('dart-js-enter-scope');
+ _jsExitJavaScriptScope = window.lookupPort('dart-js-exit-scope');
+ _jsGlobalize = window.lookupPort('dart-js-globalize');
+ _jsInvalidate = window.lookupPort('dart-js-invalidate');
+
+ _dartEnterDartScope = new ReceivePortSync()
+ ..receive((_) => _enterScope());
+ _dartExitDartScope = new ReceivePortSync()
+ ..receive((args) => _exitScope(args[0]));
+ window.registerPort('js-dart-enter-scope', _dartEnterDartScope.toSendPort());
+ window.registerPort('js-dart-exit-scope', _dartExitDartScope.toSendPort());
+}
+
+/**
+ * Returns a proxy to the global JavaScript context for this page.
+ */
+JsObject get context {
+ _enterScopeIfNeeded();
+ return _deserialize(_jsPortSync.callSync([]));
+}
+
+// Depth of current scope. Return 0 if no scope.
+get _depth => _proxiedObjectTable._scopeIndices.length;
+
+// If we are not already in a scope, enter one and register a
+// corresponding exit once we return to the event loop.
+void _enterScopeIfNeeded() {
+ if (_depth == 0) {
+ var depth = _enterScope();
+ runAsync(() => _exitScope(depth));
+ }
+}
+
+/**
+ * Executes the closure [f] within a scope. Any proxies created within this
+ * scope are invalidated afterward unless they are converted to a global proxy.
+ */
+scoped(f) {
+ var depth = _enterScope();
+ try {
+ return f();
+ } finally {
+ _exitScope(depth);
+ }
+}
+
+int _enterScope() {
+ _initialize();
+ _proxiedObjectTable.enterScope();
+ _jsEnterJavaScriptScope.callSync([]);
+ return _proxiedObjectTable._scopeIndices.length;
+}
+
+void _exitScope(int depth) {
+ assert(_proxiedObjectTable._scopeIndices.length == depth);
+ _jsExitJavaScriptScope.callSync([]);
+ _proxiedObjectTable.exitScope();
+}
+
+/**
+ * Retains the given [object] beyond the current scope.
+ * Instead, it will need to be explicitly released.
+ * The given [object] is returned for convenience.
+ */
+// TODO(aa) : change dynamic to Serializable<Proxy> if http://dartbug.com/9023
+// is fixed.
+// TODO(aa) : change to "<T extends Serializable<Proxy>> T retain(T object)"
+// once generic methods have landed.
+dynamic retain(Serializable<JsObject> object) {
+ _jsGlobalize.callSync(_serialize(object.toJs()));
+ return object;
+}
+
+/**
+ * Releases a retained [object].
+ */
+void release(Serializable<JsObject> object) {
+ _jsInvalidate.callSync(_serialize(object.toJs()));
+}
+
+/**
+ * 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.
+ *
+ * A callback can either be:
+ *
+ * - single-fire, in which case it is automatically invalidated after the first
+ * invocation, or
+ * - multi-fire, in which case it must be explicitly disposed.
+ */
+class Callback implements Serializable<JsFunction> {
+ final bool _manualDispose;
+ JsFunction _f;
+
+ Callback._internal(this._manualDispose, Function f, bool withThis) {
+ final id = _proxiedObjectTable.add((List args) {
+ final arguments = new List.from(args);
+ if (!withThis) arguments.removeAt(0);
+ if (_manualDispose) {
+ return Function.apply(f, arguments);
+ } else {
+ try {
+ return Function.apply(f, arguments);
+ } finally {
+ _dispose();
+ }
+ }
+ });
+ _proxiedObjectTable.globalize(id);
+ _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id);
+ }
+
+ _dispose() {
+ _proxiedObjectTable.invalidate(_f._id);
+ }
+
+ JsFunction toJs() => _f;
+
+ /**
+ * Disposes this [Callback] so that it may be collected.
+ * Once a [Callback] is disposed, it is an error to invoke it from JavaScript.
+ */
+ dispose() {
+ assert(_manualDispose);
+ _dispose();
+ }
+
+ /**
+ * Creates a single-fire [Callback] that invokes [f]. The callback is
+ * automatically disposed after the first invocation.
+ */
+ factory Callback.once(Function f, {bool withThis: false}) =>
+ new Callback._internal(false, f, withThis);
+
+ /**
+ * Creates a multi-fire [Callback] that invokes [f]. The callback must be
+ * explicitly disposed to avoid memory leaks.
+ */
+ factory Callback.many(Function f, {bool withThis: false}) =>
+ new Callback._internal(true, f, withThis);
+}
+
+/**
+ * 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]) {
+ _enterScopeIfNeeded();
+ 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) {
+ _enterScopeIfNeeded();
+ return _convert(data);
+ }
+
+ static _convert(data) {
+ return _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 ]);
+
+ // Test if this is equivalent to another Proxy. This essentially
+ // maps to JavaScript's == operator.
+ // TODO(vsm): Can we avoid forwarding to JS?
+ operator==(other) => identical(this, other)
+ ? true
+ : (other is JsObject &&
+ _jsPortEquals.callSync([_serialize(this), _serialize(other)]));
+
+ /**
+ * 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) {
+ _enterScopeIfNeeded();
+ 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;
+
+ // Counter for invalidated IDs for debugging.
+ int _deletedCount;
+
+ // 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;
+
+ // The set of IDs that are global. These must be explicitly invalidated.
+ final Set<String> _globalIds;
+
+ // The stack of valid IDs.
+ final List<String> _handleStack;
+
+ // The stack of scopes, where each scope is represented by an index into the
+ // handleStack.
+ final List<int> _scopeIndices;
+
+ // Enters a new scope.
+ enterScope() {
+ _scopeIndices.add(_handleStack.length);
+ }
+
+ // Invalidates non-global IDs created in the current scope and
+ // restore to the previous scope.
+ exitScope() {
+ int start = _scopeIndices.removeLast();
+ for (int i = start; i < _handleStack.length; ++i) {
+ String key = _handleStack[i];
+ if (!_globalIds.contains(key)) {
+ _registry.remove(_handleStack[i]);
+ _deletedCount++;
+ }
+ }
+ if (start != _handleStack.length) {
+ _handleStack.removeRange(start, _handleStack.length - start);
+ }
+ }
+
+ // Converts an ID to a global.
+ globalize(id) => _globalIds.add(id);
+
+ // Invalidates an ID.
+ invalidate(id) {
+ var old = _registry[id];
+ _globalIds.remove(id);
+ _registry.remove(id);
+ _deletedCount++;
+ return old;
+ }
+
+ // Replaces the object referenced by an ID.
+ _replace(id, x) {
+ _registry[id] = x;
+ }
+
+ _ProxiedObjectTable() :
+ _name = 'dart-ref',
+ _nextId = 0,
+ _deletedCount = 0,
+ _registry = {},
+ _port = new ReceivePortSync(),
+ _handleStack = new List<String>(),
+ _scopeIndices = new List<int>(),
+ _globalIds = new Set<String>() {
+ _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) {
+ _enterScopeIfNeeded();
+ // TODO(vsm): Cache x and reuse id.
+ final id = '$_name-${_nextId++}';
+ _registry[id] = x;
+ _handleStack.add(id);
+ 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 the total number of IDs ever allocated.
+ get total => count + _deletedCount;
+
+ // 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 {
+ // 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';
+}
+
+// Fetch the number of proxies to JavaScript objects.
+// This returns a 2 element list. The first is the number of currently
+// live proxies. The second is the total number of proxies ever
+// allocated.
+List _proxyCountJavaScript() => _jsPortProxyCount.callSync([]);
+
+/**
+ * Returns the number of allocated proxy objects matching the given
+ * conditions. By default, the total number of live proxy objects are
+ * return. In a well behaved program, this should stay below a small
+ * bound.
+ *
+ * Set [all] to true to return the total number of proxies ever allocated.
+ * Set [dartOnly] to only count proxies to Dart objects (live or all).
+ * Set [jsOnly] to only count proxies to JavaScript objects (live or all).
+ */
+int proxyCount({all: false, dartOnly: false, jsOnly: false}) {
+ final js = !dartOnly;
+ final dart = !jsOnly;
+ final jsCounts = js ? _proxyCountJavaScript() : null;
+ var sum = 0;
+ if (!all) {
+ if (js)
+ sum += jsCounts[0];
+ if (dart)
+ sum += _proxiedObjectTable.count;
+ } else {
+ if (js)
+ sum += jsCounts[1];
+ if (dart)
+ sum += _proxiedObjectTable.total;
+ }
+ return sum;
+}
+
+// Prints the number of live handles in Dart and JavaScript. This is for
+// debugging / profiling purposes.
+void _proxyDebug([String message = '']) {
+ print('Proxy status $message:');
+ var dartLive = proxyCount(dartOnly: true);
+ var dartTotal = proxyCount(dartOnly: true, all: true);
+ var jsLive = proxyCount(jsOnly: true);
+ var jsTotal = proxyCount(jsOnly: true, all: true);
+ print(' Dart objects Live : $dartLive (out of $dartTotal ever allocated).');
+ print(' JS objects Live : $jsLive (out of $jsTotal ever allocated).');
+}

Powered by Google App Engine
This is Rietveld 408576698