blob: 40e1700ad76d53fe7d69ec4819c10387678d1789 [file] [log] [blame]
// 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);
})();