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