OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * The js.dart library provides simple JavaScript invocation from Dart that |
| 7 * works on both Dartium and on other modern browsers via Dart2JS. |
| 8 * |
| 9 * It provides a model based on scoped [JsObject] objects. Proxies give Dart |
| 10 * code access to JavaScript objects, fields, and functions as well as the |
| 11 * ability to pass Dart objects and functions to JavaScript functions. Scopes |
| 12 * enable developers to use proxies without memory leaks - a common challenge |
| 13 * with cross-runtime interoperation. |
| 14 * |
| 15 * The top-level [context] getter provides a [JsObject] to the global JavaScript |
| 16 * context for the page your Dart code is running on. In the following example: |
| 17 * |
| 18 * import 'dart:js'; |
| 19 * |
| 20 * void main() { |
| 21 * context.callMethod('alert', ['Hello from Dart via JavaScript']); |
| 22 * } |
| 23 * |
| 24 * context['alert'] creates a proxy to the top-level alert function in |
| 25 * JavaScript. It is invoked from Dart as a regular function that forwards to |
| 26 * the underlying JavaScript one. By default, proxies are released when |
| 27 * the currently executing event completes, e.g., when main is completes |
| 28 * in this example. |
| 29 * |
| 30 * The library also enables JavaScript proxies to Dart objects and functions. |
| 31 * For example, the following Dart code: |
| 32 * |
| 33 * context['dartCallback'] = new Callback.once((x) => print(x*2)); |
| 34 * |
| 35 * defines a top-level JavaScript function 'dartCallback' that is a proxy to |
| 36 * the corresponding Dart function. The [Callback.once] constructor allows the |
| 37 * proxy to the Dart function to be retained across multiple events; |
| 38 * instead it is released after the first invocation. (This is a common |
| 39 * pattern for asychronous callbacks.) |
| 40 * |
| 41 * Note, parameters and return values are intuitively passed by value for |
| 42 * primitives and by reference for non-primitives. In the latter case, the |
| 43 * references are automatically wrapped and unwrapped as proxies by the library. |
| 44 * |
| 45 * This library also allows construction of JavaScripts objects given a |
| 46 * [JsObject] to a corresponding JavaScript constructor. For example, if the |
| 47 * following JavaScript is loaded on the page: |
| 48 * |
| 49 * function Foo(x) { |
| 50 * this.x = x; |
| 51 * } |
| 52 * |
| 53 * Foo.prototype.add = function(other) { |
| 54 * return new Foo(this.x + other.x); |
| 55 * } |
| 56 * |
| 57 * then, the following Dart: |
| 58 * |
| 59 * var foo = new JsObject(context['Foo'], [42]); |
| 60 * var foo2 = foo.callMethod('add', [foo]); |
| 61 * print(foo2['x']); |
| 62 * |
| 63 * will construct a JavaScript Foo object with the parameter 42, invoke its |
| 64 * add method, and return a [JsObject] to a new Foo object whose x field is 84. |
| 65 */ |
| 66 |
| 67 library dart.js; |
| 68 |
| 69 import 'dart:html'; |
| 70 import 'dart:isolate'; |
| 71 |
| 72 // Global ports to manage communication from Dart to JS. |
| 73 SendPortSync _jsPortSync = window.lookupPort('dart-js-context'); |
| 74 SendPortSync _jsPortCreate = window.lookupPort('dart-js-create'); |
| 75 SendPortSync _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); |
| 76 SendPortSync _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property'
); |
| 77 SendPortSync _jsPortConvert = window.lookupPort('dart-js-convert'); |
| 78 |
| 79 /** |
| 80 * Returns a proxy to the global JavaScript context for this page. |
| 81 */ |
| 82 JsObject get context => _deserialize(_jsPortSync.callSync([])); |
| 83 |
| 84 /** |
| 85 * Converts a json-like [data] to a JavaScript map or array and return a |
| 86 * [JsObject] to it. |
| 87 */ |
| 88 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); |
| 89 |
| 90 /** |
| 91 * Converts a local Dart function to a callback that can be passed to |
| 92 * JavaScript. |
| 93 */ |
| 94 class Callback implements Serializable<JsFunction> { |
| 95 JsFunction _f; |
| 96 |
| 97 Callback._(Function f, bool withThis) { |
| 98 final id = _proxiedObjectTable.add((List args) { |
| 99 final arguments = new List.from(args); |
| 100 if (!withThis) arguments.removeAt(0); |
| 101 return Function.apply(f, arguments); |
| 102 }); |
| 103 _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id); |
| 104 } |
| 105 |
| 106 factory Callback(Function f) => new Callback._(f, false); |
| 107 factory Callback.withThis(Function f) => new Callback._(f, true); |
| 108 |
| 109 JsFunction toJs() => _f; |
| 110 } |
| 111 |
| 112 /** |
| 113 * Proxies to JavaScript objects. |
| 114 */ |
| 115 class JsObject implements Serializable<JsObject> { |
| 116 final SendPortSync _port; |
| 117 final String _id; |
| 118 |
| 119 /** |
| 120 * Constructs a [JsObject] to a new JavaScript object by invoking a (proxy to |
| 121 * a) JavaScript [constructor]. The [arguments] list should contain either |
| 122 * primitive values, DOM elements, or Proxies. |
| 123 */ |
| 124 factory JsObject(Serializable<JsFunction> constructor, [List arguments]) { |
| 125 final params = [constructor]; |
| 126 if (arguments != null) params.addAll(arguments); |
| 127 final serialized = params.map(_serialize).toList(); |
| 128 final result = _jsPortCreate.callSync(serialized); |
| 129 return _deserialize(result); |
| 130 } |
| 131 |
| 132 /** |
| 133 * Constructs a [JsObject] to a new JavaScript map or list created defined via |
| 134 * Dart map or list. |
| 135 */ |
| 136 factory JsObject._json(data) => _convert(data); |
| 137 |
| 138 static _convert(data) => |
| 139 _deserialize(_jsPortConvert.callSync(_serializeDataTree(data))); |
| 140 |
| 141 static _serializeDataTree(data) { |
| 142 if (data is Map) { |
| 143 final entries = new List(); |
| 144 for (var key in data.keys) { |
| 145 entries.add([key, _serializeDataTree(data[key])]); |
| 146 } |
| 147 return ['map', entries]; |
| 148 } else if (data is Iterable) { |
| 149 return ['list', data.map(_serializeDataTree).toList()]; |
| 150 } else { |
| 151 return ['simple', _serialize(data)]; |
| 152 } |
| 153 } |
| 154 |
| 155 JsObject._internal(this._port, this._id); |
| 156 |
| 157 JsObject toJs() => this; |
| 158 |
| 159 // Resolve whether this is needed. |
| 160 operator[](arg) => _forward(this, '[]', 'method', [ arg ]); |
| 161 |
| 162 // Resolve whether this is needed. |
| 163 operator[]=(key, value) => _forward(this, '[]=', 'method', [ key, value ]); |
| 164 |
| 165 int get hashCode => _id.hashCode; |
| 166 |
| 167 // Test if this is equivalent to another Proxy. This essentially |
| 168 // maps to JavaScript's === operator. |
| 169 operator==(other) => other is JsObject && this._id == other._id; |
| 170 |
| 171 /** |
| 172 * Check if this [JsObject] has a [name] property. |
| 173 */ |
| 174 bool hasProperty(String name) => _forward(this, name, 'hasProperty', []); |
| 175 |
| 176 /** |
| 177 * Delete the [name] property. |
| 178 */ |
| 179 void deleteProperty(String name) { |
| 180 _jsPortDeleteProperty.callSync([this, name].map(_serialize).toList()); |
| 181 } |
| 182 |
| 183 /** |
| 184 * Check if this [JsObject] is instance of [type]. |
| 185 */ |
| 186 bool instanceof(Serializable<JsFunction> type) => |
| 187 _jsPortInstanceof.callSync([this, type].map(_serialize).toList()); |
| 188 |
| 189 String toString() { |
| 190 try { |
| 191 return _forward(this, 'toString', 'method', []); |
| 192 } catch(e) { |
| 193 return super.toString(); |
| 194 } |
| 195 } |
| 196 |
| 197 callMethod(String name, [List args]) { |
| 198 return _forward(this, name, 'method', args != null ? args : []); |
| 199 } |
| 200 |
| 201 // Forward member accesses to the backing JavaScript object. |
| 202 static _forward(JsObject receiver, String member, String kind, List args) { |
| 203 var result = receiver._port.callSync([receiver._id, member, kind, |
| 204 args.map(_serialize).toList()]); |
| 205 switch (result[0]) { |
| 206 case 'return': return _deserialize(result[1]); |
| 207 case 'throws': throw _deserialize(result[1]); |
| 208 case 'none': throw new NoSuchMethodError(receiver, member, args, {}); |
| 209 default: throw 'Invalid return value'; |
| 210 } |
| 211 } |
| 212 } |
| 213 |
| 214 /// A [JsObject] subtype to JavaScript functions. |
| 215 class JsFunction extends JsObject implements Serializable<JsFunction> { |
| 216 JsFunction._internal(SendPortSync port, String id) |
| 217 : super._internal(port, id); |
| 218 |
| 219 apply(thisArg, [List args]) { |
| 220 return JsObject._forward(this, '', 'apply', |
| 221 [thisArg]..addAll(args == null ? [] : args)); |
| 222 } |
| 223 } |
| 224 |
| 225 /// Marker class used to indicate it is serializable to js. If a class is a |
| 226 /// [Serializable] the "toJs" method will be called and the result will be used |
| 227 /// as value. |
| 228 abstract class Serializable<T> { |
| 229 T toJs(); |
| 230 } |
| 231 |
| 232 // A table to managed local Dart objects that are proxied in JavaScript. |
| 233 class _ProxiedObjectTable { |
| 234 // Debugging name. |
| 235 final String _name; |
| 236 |
| 237 // Generator for unique IDs. |
| 238 int _nextId; |
| 239 |
| 240 // Table of IDs to Dart objects. |
| 241 final Map<String, Object> _registry; |
| 242 |
| 243 // Port to handle and forward requests to the underlying Dart objects. |
| 244 // A remote proxy is uniquely identified by an ID and SendPortSync. |
| 245 final ReceivePortSync _port; |
| 246 |
| 247 _ProxiedObjectTable() : |
| 248 _name = 'dart-ref', |
| 249 _nextId = 0, |
| 250 _registry = {}, |
| 251 _port = new ReceivePortSync() { |
| 252 _port.receive((msg) { |
| 253 try { |
| 254 final receiver = _registry[msg[0]]; |
| 255 final method = msg[1]; |
| 256 final args = msg[2].map(_deserialize).toList(); |
| 257 if (method == '#call') { |
| 258 final func = receiver as Function; |
| 259 var result = _serialize(func(args)); |
| 260 return ['return', result]; |
| 261 } else { |
| 262 // TODO(vsm): Support a mechanism to register a handler here. |
| 263 throw 'Invocation unsupported on non-function Dart proxies'; |
| 264 } |
| 265 } catch (e) { |
| 266 // TODO(vsm): callSync should just handle exceptions itself. |
| 267 return ['throws', '$e']; |
| 268 } |
| 269 }); |
| 270 } |
| 271 |
| 272 // Adds a new object to the table and return a new ID for it. |
| 273 String add(x) { |
| 274 // TODO(vsm): Cache x and reuse id. |
| 275 final id = '$_name-${_nextId++}'; |
| 276 _registry[id] = x; |
| 277 return id; |
| 278 } |
| 279 |
| 280 // Gets an object by ID. |
| 281 Object get(String id) { |
| 282 return _registry[id]; |
| 283 } |
| 284 |
| 285 // Gets the current number of objects kept alive by this table. |
| 286 get count => _registry.length; |
| 287 |
| 288 // Gets a send port for this table. |
| 289 get sendPort => _port.toSendPort(); |
| 290 } |
| 291 |
| 292 // The singleton to manage proxied Dart objects. |
| 293 _ProxiedObjectTable _proxiedObjectTable = new _ProxiedObjectTable(); |
| 294 |
| 295 /// End of proxy implementation. |
| 296 |
| 297 // Dart serialization support. |
| 298 |
| 299 _serialize(var message) { |
| 300 if (message == null) { |
| 301 return null; // Convert undefined to null. |
| 302 } else if (message is String || |
| 303 message is num || |
| 304 message is bool) { |
| 305 // Primitives are passed directly through. |
| 306 return message; |
| 307 } else if (message is SendPortSync) { |
| 308 // Non-proxied objects are serialized. |
| 309 return message; |
| 310 } else if (message is JsFunction) { |
| 311 // Remote function proxy. |
| 312 return [ 'funcref', message._id, message._port ]; |
| 313 } else if (message is JsObject) { |
| 314 // Remote object proxy. |
| 315 return [ 'objref', message._id, message._port ]; |
| 316 } else if (message is Serializable) { |
| 317 // use of result of toJs() |
| 318 return _serialize(message.toJs()); |
| 319 } else if (message is Function) { |
| 320 return _serialize(new Callback(message)); |
| 321 } else { |
| 322 // Local object proxy. |
| 323 return [ 'objref', |
| 324 _proxiedObjectTable.add(message), |
| 325 _proxiedObjectTable.sendPort ]; |
| 326 } |
| 327 } |
| 328 |
| 329 _deserialize(var message) { |
| 330 deserializeFunction(message) { |
| 331 var id = message[1]; |
| 332 var port = message[2]; |
| 333 if (port == _proxiedObjectTable.sendPort) { |
| 334 // Local function. |
| 335 return _proxiedObjectTable.get(id); |
| 336 } else { |
| 337 // Remote function. Forward to its port. |
| 338 return new JsFunction._internal(port, id); |
| 339 } |
| 340 } |
| 341 |
| 342 deserializeObject(message) { |
| 343 var id = message[1]; |
| 344 var port = message[2]; |
| 345 if (port == _proxiedObjectTable.sendPort) { |
| 346 // Local object. |
| 347 return _proxiedObjectTable.get(id); |
| 348 } else { |
| 349 // Remote object. |
| 350 return new JsObject._internal(port, id); |
| 351 } |
| 352 } |
| 353 |
| 354 if (message == null) { |
| 355 return null; // Convert undefined to null. |
| 356 } else if (message is String || |
| 357 message is num || |
| 358 message is bool) { |
| 359 // Primitives are passed directly through. |
| 360 return message; |
| 361 } else if (message is SendPortSync) { |
| 362 // Serialized type. |
| 363 return message; |
| 364 } |
| 365 var tag = message[0]; |
| 366 switch (tag) { |
| 367 case 'funcref': return deserializeFunction(message); |
| 368 case 'objref': return deserializeObject(message); |
| 369 } |
| 370 throw 'Unsupported serialized data: $message'; |
| 371 } |
OLD | NEW |