| // 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. |
| |
| // We don't use 'window' because we might be in a web worker, but we don't |
| // use 'self' because not all browsers support it |
| var globalContext = function() { return this; }(); |
| |
| // 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); |
| })(); |