Chromium Code Reviews| 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).'); |
| +} |