a81a6fcce6
This is a port of js-interop with an optimized dart2js compilation. It also gets rid of noSuchMethod. I also made some renamings inspired by https://github.com/dart-lang/js-interop/issues/68 BUG= R=vsm@google.com Review URL: https://codereview.chromium.org//15782009 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@25018 260f80e4-7a28-3924-810f-c04153c831b5
540 lines
17 KiB
JavaScript
540 lines
17 KiB
JavaScript
// 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);
|
|
})();
|