| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * The js.dart library provides simple JavaScript invocation from Dart that | 6 * The js.dart library provides simple JavaScript invocation from Dart that |
| 7 * works on both Dartium and on other modern browsers via Dart2JS. | 7 * works on both Dartium and on other modern browsers via Dart2JS. |
| 8 * | 8 * |
| 9 * It provides a model based on scoped [JsObject] objects. Proxies give Dart | 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 | 10 * code access to JavaScript objects, fields, and functions as well as the |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 59 * var foo = new JsObject(context['Foo'], [42]); | 59 * var foo = new JsObject(context['Foo'], [42]); |
| 60 * var foo2 = foo.callMethod('add', [foo]); | 60 * var foo2 = foo.callMethod('add', [foo]); |
| 61 * print(foo2['x']); | 61 * print(foo2['x']); |
| 62 * | 62 * |
| 63 * will construct a JavaScript Foo object with the parameter 42, invoke its | 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. | 64 * add method, and return a [JsObject] to a new Foo object whose x field is 84. |
| 65 */ | 65 */ |
| 66 | 66 |
| 67 library dart.js; | 67 library dart.js; |
| 68 | 68 |
| 69 import 'dart:collection' show HashMap; | |
| 70 import 'dart:html'; | 69 import 'dart:html'; |
| 71 import 'dart:isolate'; | 70 import 'dart:isolate'; |
| 72 | 71 |
| 73 // Global ports to manage communication from Dart to JS. | 72 // Global ports to manage communication from Dart to JS. |
| 74 | |
| 75 SendPortSync _jsPortSync = window.lookupPort('dart-js-context'); | 73 SendPortSync _jsPortSync = window.lookupPort('dart-js-context'); |
| 76 SendPortSync _jsPortCreate = window.lookupPort('dart-js-create'); | 74 SendPortSync _jsPortCreate = window.lookupPort('dart-js-create'); |
| 77 SendPortSync _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); | 75 SendPortSync _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); |
| 78 SendPortSync _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property'
); | 76 SendPortSync _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property'
); |
| 79 SendPortSync _jsPortConvert = window.lookupPort('dart-js-convert'); | 77 SendPortSync _jsPortConvert = window.lookupPort('dart-js-convert'); |
| 80 | 78 |
| 81 final String _objectIdPrefix = 'dart-obj-ref'; | |
| 82 final String _functionIdPrefix = 'dart-fun-ref'; | |
| 83 final _objectTable = new _ObjectTable(); | |
| 84 final _functionTable = new _ObjectTable.forFunctions(); | |
| 85 | |
| 86 // Port to handle and forward requests to the underlying Dart objects. | |
| 87 // A remote proxy is uniquely identified by an ID and SendPortSync. | |
| 88 ReceivePortSync _port = new ReceivePortSync() | |
| 89 ..receive((msg) { | |
| 90 try { | |
| 91 var id = msg[0]; | |
| 92 var method = msg[1]; | |
| 93 if (method == '#call') { | |
| 94 var receiver = _getObjectTable(id).get(id); | |
| 95 var result; | |
| 96 if (receiver is Function) { | |
| 97 // remove the first argument, which is 'this', but never | |
| 98 // used for a raw function | |
| 99 var args = msg[2].sublist(1).map(_deserialize).toList(); | |
| 100 result = Function.apply(receiver, args); | |
| 101 } else if (receiver is Callback) { | |
| 102 var args = msg[2].map(_deserialize).toList(); | |
| 103 result = receiver._call(args); | |
| 104 } else { | |
| 105 throw new StateError('bad function type: $receiver'); | |
| 106 } | |
| 107 return ['return', _serialize(result)]; | |
| 108 } else { | |
| 109 // TODO(vsm): Support a mechanism to register a handler here. | |
| 110 throw 'Invocation unsupported on non-function Dart proxies'; | |
| 111 } | |
| 112 } catch (e) { | |
| 113 // TODO(vsm): callSync should just handle exceptions itself. | |
| 114 return ['throws', '$e']; | |
| 115 } | |
| 116 }); | |
| 117 | |
| 118 _ObjectTable _getObjectTable(String id) { | |
| 119 if (id.startsWith(_functionIdPrefix)) return _functionTable; | |
| 120 if (id.startsWith(_objectIdPrefix)) return _objectTable; | |
| 121 throw new ArgumentError('internal error: invalid object id: $id'); | |
| 122 } | |
| 123 | 79 |
| 124 JsObject _context; | 80 JsObject _context; |
| 125 | 81 |
| 126 /** | 82 /** |
| 127 * Returns a proxy to the global JavaScript context for this page. | 83 * Returns a proxy to the global JavaScript context for this page. |
| 128 */ | 84 */ |
| 129 JsObject get context { | 85 JsObject get context { |
| 130 if (_context == null) { | 86 if (_context == null) { |
| 131 var port = _jsPortSync; | 87 var port = _jsPortSync; |
| 132 if (port == null) { | 88 if (port == null) { |
| 133 return null; | 89 return null; |
| 134 } | 90 } |
| 135 _context = _deserialize(_jsPortSync.callSync([])); | 91 _context = _deserialize(_jsPortSync.callSync([])); |
| 136 } | 92 } |
| 137 return _context; | 93 return _context; |
| 138 } | 94 } |
| 139 | 95 |
| 140 /** | 96 /** |
| 141 * Converts a json-like [data] to a JavaScript map or array and return a | 97 * Converts a json-like [data] to a JavaScript map or array and return a |
| 142 * [JsObject] to it. | 98 * [JsObject] to it. |
| 143 */ | 99 */ |
| 144 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); | 100 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); |
| 145 | 101 |
| 146 /** | 102 /** |
| 147 * Converts a local Dart function to a callback that can be passed to | 103 * Converts a local Dart function to a callback that can be passed to |
| 148 * JavaScript. | 104 * JavaScript. |
| 149 */ | 105 */ |
| 150 class Callback implements Serializable<JsFunction> { | 106 class Callback implements Serializable<JsFunction> { |
| 151 final bool _withThis; | 107 JsFunction _f; |
| 152 final Function _function; | |
| 153 JsFunction _jsFunction; | |
| 154 | 108 |
| 155 Callback._(this._function, this._withThis) { | 109 Callback._(Function f, bool withThis) { |
| 156 var id = _functionTable.add(this); | 110 final id = _proxiedObjectTable.add((List args) { |
| 157 _jsFunction = new JsFunction._internal(_port.toSendPort(), id); | 111 final arguments = new List.from(args); |
| 112 if (!withThis) arguments.removeAt(0); |
| 113 return Function.apply(f, arguments); |
| 114 }); |
| 115 _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id); |
| 158 } | 116 } |
| 159 | 117 |
| 160 factory Callback(Function f) => new Callback._(f, false); | 118 factory Callback(Function f) => new Callback._(f, false); |
| 161 factory Callback.withThis(Function f) => new Callback._(f, true); | 119 factory Callback.withThis(Function f) => new Callback._(f, true); |
| 162 | 120 |
| 163 dynamic _call(List args) { | 121 JsFunction toJs() => _f; |
| 164 var arguments = (_withThis) ? args : args.sublist(1); | |
| 165 return Function.apply(_function, arguments); | |
| 166 } | |
| 167 | |
| 168 JsFunction toJs() => _jsFunction; | |
| 169 } | 122 } |
| 170 | 123 |
| 171 /** | 124 /** |
| 172 * Proxies to JavaScript objects. | 125 * Proxies to JavaScript objects. |
| 173 */ | 126 */ |
| 174 class JsObject implements Serializable<JsObject> { | 127 class JsObject implements Serializable<JsObject> { |
| 175 final SendPortSync _port; | 128 final SendPortSync _port; |
| 176 final String _id; | 129 final String _id; |
| 177 | 130 |
| 178 /** | 131 /** |
| (...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 282 } | 235 } |
| 283 } | 236 } |
| 284 | 237 |
| 285 /// Marker class used to indicate it is serializable to js. If a class is a | 238 /// Marker class used to indicate it is serializable to js. If a class is a |
| 286 /// [Serializable] the "toJs" method will be called and the result will be used | 239 /// [Serializable] the "toJs" method will be called and the result will be used |
| 287 /// as value. | 240 /// as value. |
| 288 abstract class Serializable<T> { | 241 abstract class Serializable<T> { |
| 289 T toJs(); | 242 T toJs(); |
| 290 } | 243 } |
| 291 | 244 |
| 292 class _ObjectTable { | 245 // A table to managed local Dart objects that are proxied in JavaScript. |
| 293 final String name; | 246 class _ProxiedObjectTable { |
| 294 final Map<String, Object> objects; | 247 // Debugging name. |
| 295 final Map<Object, String> ids; | 248 final String _name; |
| 296 int nextId = 0; | |
| 297 | 249 |
| 298 // Creates a table that uses an identity Map to store IDs | 250 // Generator for unique IDs. |
| 299 _ObjectTable() | 251 int _nextId; |
| 300 : name = _objectIdPrefix, | |
| 301 objects = new HashMap<String, Object>(), | |
| 302 ids = new HashMap<Object, String>.identity(); | |
| 303 | 252 |
| 304 // Creates a table that uses an equality-based Map to store IDs, since | 253 // Table of IDs to Dart objects. |
| 305 // closurized methods may be equal, but not identical | 254 final Map<String, Object> _registry; |
| 306 _ObjectTable.forFunctions() | |
| 307 : name = _functionIdPrefix, | |
| 308 objects = new HashMap<String, Object>(), | |
| 309 ids = new HashMap<Object, String>(); | |
| 310 | 255 |
| 311 // Adds a new object to the table. If [id] is not given, a new unique ID is | 256 // Port to handle and forward requests to the underlying Dart objects. |
| 312 // generated. Returns the ID. | 257 // A remote proxy is uniquely identified by an ID and SendPortSync. |
| 313 String add(Object o, {String id}) { | 258 final ReceivePortSync _port; |
| 259 |
| 260 _ProxiedObjectTable() : |
| 261 _name = 'dart-ref', |
| 262 _nextId = 0, |
| 263 _registry = {}, |
| 264 _port = new ReceivePortSync() { |
| 265 _port.receive((msg) { |
| 266 try { |
| 267 final receiver = _registry[msg[0]]; |
| 268 final method = msg[1]; |
| 269 final args = msg[2].map(_deserialize).toList(); |
| 270 if (method == '#call') { |
| 271 final func = receiver as Function; |
| 272 var result = _serialize(func(args)); |
| 273 return ['return', result]; |
| 274 } else { |
| 275 // TODO(vsm): Support a mechanism to register a handler here. |
| 276 throw 'Invocation unsupported on non-function Dart proxies'; |
| 277 } |
| 278 } catch (e) { |
| 279 // TODO(vsm): callSync should just handle exceptions itself. |
| 280 return ['throws', '$e']; |
| 281 } |
| 282 }); |
| 283 } |
| 284 |
| 285 // Adds a new object to the table and return a new ID for it. |
| 286 String add(x) { |
| 314 // TODO(vsm): Cache x and reuse id. | 287 // TODO(vsm): Cache x and reuse id. |
| 315 if (id == null) id = ids[o]; | 288 final id = '$_name-${_nextId++}'; |
| 316 if (id == null) id = '$name-${nextId++}'; | 289 _registry[id] = x; |
| 317 ids[o] = id; | |
| 318 objects[id] = o; | |
| 319 return id; | 290 return id; |
| 320 } | 291 } |
| 321 | 292 |
| 322 // Gets an object by ID. | 293 // Gets an object by ID. |
| 323 Object get(String id) => objects[id]; | 294 Object get(String id) { |
| 324 | 295 return _registry[id]; |
| 325 bool contains(String id) => objects.containsKey(id); | 296 } |
| 326 | |
| 327 String getId(Object o) => ids[o]; | |
| 328 | 297 |
| 329 // Gets the current number of objects kept alive by this table. | 298 // Gets the current number of objects kept alive by this table. |
| 330 get count => objects.length; | 299 get count => _registry.length; |
| 300 |
| 301 // Gets a send port for this table. |
| 302 get sendPort => _port.toSendPort(); |
| 331 } | 303 } |
| 332 | 304 |
| 305 // The singleton to manage proxied Dart objects. |
| 306 _ProxiedObjectTable _proxiedObjectTable = new _ProxiedObjectTable(); |
| 307 |
| 308 /// End of proxy implementation. |
| 309 |
| 333 // Dart serialization support. | 310 // Dart serialization support. |
| 334 | 311 |
| 335 _serialize(var message) { | 312 _serialize(var message) { |
| 336 if (message == null) { | 313 if (message == null) { |
| 337 return null; // Convert undefined to null. | 314 return null; // Convert undefined to null. |
| 338 } else if (message is String || | 315 } else if (message is String || |
| 339 message is num || | 316 message is num || |
| 340 message is bool) { | 317 message is bool) { |
| 341 // Primitives are passed directly through. | 318 // Primitives are passed directly through. |
| 342 return message; | 319 return message; |
| 343 } else if (message is SendPortSync) { | 320 } else if (message is SendPortSync) { |
| 344 // Non-proxied objects are serialized. | 321 // Non-proxied objects are serialized. |
| 345 return message; | 322 return message; |
| 346 } else if (message is JsFunction) { | 323 } else if (message is JsFunction) { |
| 347 // Remote function proxy. | 324 // Remote function proxy. |
| 348 return ['funcref', message._id, message._port]; | 325 return [ 'funcref', message._id, message._port ]; |
| 349 } else if (message is JsObject) { | 326 } else if (message is JsObject) { |
| 350 // Remote object proxy. | 327 // Remote object proxy. |
| 351 return ['objref', message._id, message._port]; | 328 return [ 'objref', message._id, message._port ]; |
| 352 } else if (message is Serializable) { | 329 } else if (message is Serializable) { |
| 353 // use of result of toJs() | 330 // use of result of toJs() |
| 354 return _serialize(message.toJs()); | 331 return _serialize(message.toJs()); |
| 355 } else if (message is Function) { | 332 } else if (message is Function) { |
| 356 var id = _functionTable.getId(message); | 333 return _serialize(new Callback(message)); |
| 357 if (id != null) { | |
| 358 return ['funcref', id, _port.toSendPort()]; | |
| 359 } | |
| 360 id = _functionTable.add(message); | |
| 361 return ['funcref', id, _port.toSendPort()]; | |
| 362 } else { | 334 } else { |
| 363 // Local object proxy. | 335 // Local object proxy. |
| 364 return ['objref', _objectTable.add(message), _port.toSendPort()]; | 336 return [ 'objref', |
| 337 _proxiedObjectTable.add(message), |
| 338 _proxiedObjectTable.sendPort ]; |
| 365 } | 339 } |
| 366 } | 340 } |
| 367 | 341 |
| 368 _deserialize(var message) { | 342 _deserialize(var message) { |
| 369 deserializeFunction(message) { | 343 deserializeFunction(message) { |
| 370 var id = message[1]; | 344 var id = message[1]; |
| 371 var port = message[2]; | 345 var port = message[2]; |
| 372 if (port == _port.toSendPort()) { | 346 if (port == _proxiedObjectTable.sendPort) { |
| 373 // Local function. | 347 // Local function. |
| 374 return _functionTable.get(id); | 348 return _proxiedObjectTable.get(id); |
| 375 } else { | 349 } else { |
| 376 // Remote function. | 350 // Remote function. Forward to its port. |
| 377 var jsFunction = _functionTable.get(id); | 351 return new JsFunction._internal(port, id); |
| 378 if (jsFunction == null) { | |
| 379 jsFunction = new JsFunction._internal(port, id); | |
| 380 _functionTable.add(jsFunction, id: id); | |
| 381 } | |
| 382 return jsFunction; | |
| 383 } | 352 } |
| 384 } | 353 } |
| 385 | 354 |
| 386 deserializeObject(message) { | 355 deserializeObject(message) { |
| 387 var id = message[1]; | 356 var id = message[1]; |
| 388 var port = message[2]; | 357 var port = message[2]; |
| 389 if (port == _port.toSendPort()) { | 358 if (port == _proxiedObjectTable.sendPort) { |
| 390 // Local object. | 359 // Local object. |
| 391 return _objectTable.get(id); | 360 return _proxiedObjectTable.get(id); |
| 392 } else { | 361 } else { |
| 393 // Remote object. | 362 // Remote object. |
| 394 var jsObject = _objectTable.get(id); | 363 return new JsObject._internal(port, id); |
| 395 if (jsObject == null) { | |
| 396 jsObject = new JsObject._internal(port, id); | |
| 397 _objectTable.add(jsObject, id: id); | |
| 398 } | |
| 399 return jsObject; | |
| 400 } | 364 } |
| 401 } | 365 } |
| 402 | 366 |
| 403 if (message == null) { | 367 if (message == null) { |
| 404 return null; // Convert undefined to null. | 368 return null; // Convert undefined to null. |
| 405 } else if (message is String || | 369 } else if (message is String || |
| 406 message is num || | 370 message is num || |
| 407 message is bool) { | 371 message is bool) { |
| 408 // Primitives are passed directly through. | 372 // Primitives are passed directly through. |
| 409 return message; | 373 return message; |
| 410 } else if (message is SendPortSync) { | 374 } else if (message is SendPortSync) { |
| 411 // Serialized type. | 375 // Serialized type. |
| 412 return message; | 376 return message; |
| 413 } | 377 } |
| 414 var tag = message[0]; | 378 var tag = message[0]; |
| 415 switch (tag) { | 379 switch (tag) { |
| 416 case 'funcref': return deserializeFunction(message); | 380 case 'funcref': return deserializeFunction(message); |
| 417 case 'objref': return deserializeObject(message); | 381 case 'objref': return deserializeObject(message); |
| 418 } | 382 } |
| 419 throw 'Unsupported serialized data: $message'; | 383 throw 'Unsupported serialized data: $message'; |
| 420 } | 384 } |
| OLD | NEW |