// 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. // --------------------------------------------------------------------------- // Support for JS interoperability // --------------------------------------------------------------------------- function SendPortSync() { } function ReceivePortSync() { this.id = ReceivePortSync.id++; ReceivePortSync.map[this.id] = this; } // Type for remote proxies to Dart objects with dart2js. function DartProxy(o) { this.o = o; } (function() { // Serialize the following types as follows: // - primitives / null: unchanged // - lists: [ 'list', internal id, list of recursively serialized elements ] // - maps: [ 'map', internal id, map of keys and recursively serialized values ] // - send ports: [ 'sendport', type, isolate id, port id ] // // Note, internal id's are for cycle detection. function serialize(message) { var visited = []; function checkedSerialization(obj, serializer) { // Implementation detail: for now use linear search. // Another option is expando, but it may prohibit // VM optimizations (like putting object into slow mode // on property deletion.) var id = visited.indexOf(obj); if (id != -1) return [ 'ref', id ]; var id = visited.length; visited.push(obj); return serializer(id); } function doSerialize(message) { if (message == null) { return null; // Convert undefined to null. } else if (typeof(message) == 'string' || typeof(message) == 'number' || typeof(message) == 'boolean') { return message; } else if (message instanceof Array) { return checkedSerialization(message, function(id) { var values = new Array(message.length); for (var i = 0; i < message.length; i++) { values[i] = doSerialize(message[i]); } return [ 'list', id, values ]; }); } else if (message instanceof LocalSendPortSync) { return [ 'sendport', 'nativejs', message.receivePort.id ]; } else if (message instanceof DartSendPortSync) { return [ 'sendport', 'dart', message.isolateId, message.portId ]; } else { return checkedSerialization(message, function(id) { var keys = Object.getOwnPropertyNames(message); var values = new Array(keys.length); for (var i = 0; i < keys.length; i++) { values[i] = doSerialize(message[keys[i]]); } return [ 'map', id, keys, values ]; }); } } return doSerialize(message); } function deserialize(message) { return deserializeHelper(message); } function deserializeHelper(message) { if (message == null || typeof(message) == 'string' || typeof(message) == 'number' || typeof(message) == 'boolean') { return message; } switch (message[0]) { case 'map': return deserializeMap(message); case 'sendport': return deserializeSendPort(message); case 'list': return deserializeList(message); default: throw 'unimplemented'; } } function deserializeMap(message) { var result = { }; var id = message[1]; var keys = message[2]; var values = message[3]; for (var i = 0, length = keys.length; i < length; i++) { var key = deserializeHelper(keys[i]); var value = deserializeHelper(values[i]); result[key] = value; } return result; } function deserializeSendPort(message) { var tag = message[1]; switch (tag) { case 'nativejs': var id = message[2]; return new LocalSendPortSync(ReceivePortSync.map[id]); case 'dart': var isolateId = message[2]; var portId = message[3]; return new DartSendPortSync(isolateId, portId); default: throw 'Illegal SendPortSync type: $tag'; } } function deserializeList(message) { var values = message[2]; var length = values.length; var result = new Array(length); for (var i = 0; i < length; i++) { result[i] = deserializeHelper(values[i]); } return result; } window.registerPort = function(name, port) { var stringified = JSON.stringify(serialize(port)); var attrName = 'dart-port:' + name; document.documentElement.setAttribute(attrName, stringified); }; window.lookupPort = function(name) { var attrName = 'dart-port:' + name; var stringified = document.documentElement.getAttribute(attrName); return deserialize(JSON.parse(stringified)); }; ReceivePortSync.id = 0; ReceivePortSync.map = {}; ReceivePortSync.dispatchCall = function(id, message) { // TODO(vsm): Handle and propagate exceptions. var deserialized = deserialize(message); var result = ReceivePortSync.map[id].callback(deserialized); return serialize(result); }; ReceivePortSync.prototype.receive = function(callback) { this.callback = callback; }; ReceivePortSync.prototype.toSendPort = function() { return new LocalSendPortSync(this); }; ReceivePortSync.prototype.close = function() { delete ReceivePortSync.map[this.id]; }; if (navigator.webkitStartDart) { window.addEventListener('js-sync-message', function(event) { var data = JSON.parse(getPortSyncEventData(event)); var deserialized = deserialize(data.message); var result = ReceivePortSync.map[data.id].callback(deserialized); // TODO(vsm): Handle and propagate exceptions. dispatchEvent('js-result', serialize(result)); }, false); } function LocalSendPortSync(receivePort) { this.receivePort = receivePort; } LocalSendPortSync.prototype = new SendPortSync(); LocalSendPortSync.prototype.callSync = function(message) { // TODO(vsm): Do a direct deepcopy. message = deserialize(serialize(message)); return this.receivePort.callback(message); } function DartSendPortSync(isolateId, portId) { this.isolateId = isolateId; this.portId = portId; } DartSendPortSync.prototype = new SendPortSync(); function dispatchEvent(receiver, message) { var string = JSON.stringify(message); var event = document.createEvent('CustomEvent'); event.initCustomEvent(receiver, false, false, string); window.dispatchEvent(event); } function getPortSyncEventData(event) { return event.detail; } DartSendPortSync.prototype.callSync = function(message) { var serialized = serialize(message); var target = 'dart-port-' + this.isolateId + '-' + this.portId; // TODO(vsm): Make this re-entrant. // TODO(vsm): Set this up set once, on the first call. var source = target + '-result'; var result = null; var listener = function (e) { result = JSON.parse(getPortSyncEventData(e)); }; window.addEventListener(source, listener, false); dispatchEvent(target, [source, serialized]); window.removeEventListener(source, listener, false); return deserialize(result); } })(); (function() { // Proxy support for js.dart. var globalContext = window; // 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; // Ports for managing communication to proxies. this.port = new ReceivePortSync(); this.sendPort = this.port.toSendPort(); } // 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; } // Adds an object to the table and return an ID for serialization. ProxiedObjectTable.prototype.add = function (obj) { for (var ref in this.map) { var o = this.map[ref]; if (o === obj) { return ref; } } var ref = this.name + '-' + this._nextId++; this.map[ref] = obj; return ref; } // 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 receiver = table.get(message[0]); 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) { return [ 'return', serialize(receiver[field]) ]; } } 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. return [ 'return', serialize(receiver[args[0]]) ]; } 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(); proxiedObjectTable._initialize() // 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 (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 proxiedObjectTable.get(id); } else { // Remote function. Forward to its port. var f = function () { 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]); }; // 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 = 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); } // Return true if a JavaScript proxy is instance of a given type (instanceof). function proxyInstanceof(args) { var obj = deserialize(args[0]); var type = 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 = deserialize(args[0]); var member = 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()); } makeGlobalPort('dart-js-context', context); makeGlobalPort('dart-js-create', construct); makeGlobalPort('dart-js-instanceof', proxyInstanceof); makeGlobalPort('dart-js-delete-property', proxyDeleteProperty); makeGlobalPort('dart-js-convert', proxyConvert); })();