blob: 234a4bf3bff3f352481c7667dd5b22612853a160 [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.
/**
* 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:collection' show HashMap;
import 'dart:html';
import 'dart:isolate';
// Global ports to manage communication from Dart to JS.
SendPortSync _jsPortSync = window.lookupPort('dart-js-context');
SendPortSync _jsPortCreate = window.lookupPort('dart-js-create');
SendPortSync _jsPortInstanceof = window.lookupPort('dart-js-instanceof');
SendPortSync _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property');
SendPortSync _jsPortConvert = window.lookupPort('dart-js-convert');
final String _objectIdPrefix = 'dart-obj-ref';
final String _functionIdPrefix = 'dart-fun-ref';
final _objectTable = new _ObjectTable();
final _functionTable = new _ObjectTable.forFunctions();
// Port to handle and forward requests to the underlying Dart objects.
// A remote proxy is uniquely identified by an ID and SendPortSync.
ReceivePortSync _port = new ReceivePortSync()
..receive((msg) {
try {
var id = msg[0];
var method = msg[1];
if (method == '#call') {
var receiver = _getObjectTable(id).get(id);
var result;
if (receiver is Function) {
// remove the first argument, which is 'this', but never
// used for a raw function
var args = msg[2].sublist(1).map(_deserialize).toList();
result = Function.apply(receiver, args);
} else if (receiver is Callback) {
var args = msg[2].map(_deserialize).toList();
result = receiver._call(args);
} else {
throw new StateError('bad function type: $receiver');
}
return ['return', _serialize(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'];
}
});
_ObjectTable _getObjectTable(String id) {
if (id.startsWith(_functionIdPrefix)) return _functionTable;
if (id.startsWith(_objectIdPrefix)) return _objectTable;
throw new ArgumentError('internal error: invalid object id: $id');
}
JsObject _context;
/**
* Returns a proxy to the global JavaScript context for this page.
*/
JsObject get context {
if (_context == null) {
var port = _jsPortSync;
if (port == null) {
return null;
}
_context = _deserialize(_jsPortSync.callSync([]));
}
return _context;
}
/**
* 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.
*/
class Callback implements Serializable<JsFunction> {
final bool _withThis;
final Function _function;
JsFunction _jsFunction;
Callback._(this._function, this._withThis) {
var id = _functionTable.add(this);
_jsFunction = new JsFunction._internal(_port.toSendPort(), id);
}
factory Callback(Function f) => new Callback._(f, false);
factory Callback.withThis(Function f) => new Callback._(f, true);
dynamic _call(List args) {
var arguments = (_withThis) ? args : args.sublist(1);
return Function.apply(_function, arguments);
}
JsFunction toJs() => _jsFunction;
}
/**
* 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]) {
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) => _convert(data);
static _convert(data) =>
_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 ]);
int get hashCode => _id.hashCode;
// Test if this is equivalent to another Proxy. This essentially
// maps to JavaScript's === operator.
operator==(other) => other is JsObject && this._id == other._id;
/**
* 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) {
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, new Symbol(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();
}
class _ObjectTable {
final String name;
final Map<String, Object> objects;
final Map<Object, String> ids;
int nextId = 0;
// Creates a table that uses an identity Map to store IDs
_ObjectTable()
: name = _objectIdPrefix,
objects = new HashMap<String, Object>(),
ids = new HashMap<Object, String>.identity();
// Creates a table that uses an equality-based Map to store IDs, since
// closurized methods may be equal, but not identical
_ObjectTable.forFunctions()
: name = _functionIdPrefix,
objects = new HashMap<String, Object>(),
ids = new HashMap<Object, String>();
// Adds a new object to the table. If [id] is not given, a new unique ID is
// generated. Returns the ID.
String add(Object o, {String id}) {
// TODO(vsm): Cache x and reuse id.
if (id == null) id = ids[o];
if (id == null) id = '$name-${nextId++}';
ids[o] = id;
objects[id] = o;
return id;
}
// Gets an object by ID.
Object get(String id) => objects[id];
bool contains(String id) => objects.containsKey(id);
String getId(Object o) => ids[o];
// Gets the current number of objects kept alive by this table.
get count => objects.length;
}
// 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 if (message is Function) {
var id = _functionTable.getId(message);
if (id != null) {
return ['funcref', id, _port.toSendPort()];
}
id = _functionTable.add(message);
return ['funcref', id, _port.toSendPort()];
} else {
// Local object proxy.
return ['objref', _objectTable.add(message), _port.toSendPort()];
}
}
_deserialize(var message) {
deserializeFunction(message) {
var id = message[1];
var port = message[2];
if (port == _port.toSendPort()) {
// Local function.
return _functionTable.get(id);
} else {
// Remote function.
var jsFunction = _functionTable.get(id);
if (jsFunction == null) {
jsFunction = new JsFunction._internal(port, id);
_functionTable.add(jsFunction, id: id);
}
return jsFunction;
}
}
deserializeObject(message) {
var id = message[1];
var port = message[2];
if (port == _port.toSendPort()) {
// Local object.
return _objectTable.get(id);
} else {
// Remote object.
var jsObject = _objectTable.get(id);
if (jsObject == null) {
jsObject = new JsObject._internal(port, id);
_objectTable.add(jsObject, id: id);
}
return jsObject;
}
}
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';
}