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 |