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:async'; | |
70 import 'dart:html'; | |
71 import 'dart:isolate'; | |
72 import 'dart:mirrors'; | |
73 | |
74 // JavaScript bootstrapping code. | |
75 // TODO(vsm): Migrate this to use a builtin resource mechanism once we have | |
76 // one. | |
77 | |
78 // NOTE: Please re-run tools/create_bootstrap.dart on any modification of | |
79 // this bootstrap string. | |
vsm
2013/06/11 16:17:44
I just landed my CL with interop.js. Shall we mov
alexandre.ardhuin
2013/06/12 21:29:19
Done.
| |
80 final _JS_BOOTSTRAP = r""" | |
81 (function() { | |
82 // Proxy support for js.dart. | |
83 | |
84 var globalContext = window; | |
85 | |
86 // Support for binding the receiver (this) in proxied functions. | |
87 function bindIfFunction(f, _this) { | |
88 if (typeof(f) != "function") { | |
89 return f; | |
90 } else { | |
91 return new BoundFunction(_this, f); | |
92 } | |
93 } | |
94 | |
95 function unbind(obj) { | |
96 if (obj instanceof BoundFunction) { | |
97 return obj.object; | |
98 } else { | |
99 return obj; | |
100 } | |
101 } | |
102 | |
103 function BoundFunction(_this, object) { | |
104 this._this = _this; | |
105 this.object = object; | |
106 } | |
107 | |
108 // Table for local objects and functions that are proxied. | |
109 function ProxiedObjectTable() { | |
110 // Name for debugging. | |
111 this.name = 'js-ref'; | |
112 | |
113 // Table from IDs to JS objects. | |
114 this.map = {}; | |
115 | |
116 // Generator for new IDs. | |
117 this._nextId = 0; | |
118 | |
119 // Counter for deleted proxies. | |
120 this._deletedCount = 0; | |
121 | |
122 // Flag for one-time initialization. | |
123 this._initialized = false; | |
124 | |
125 // Ports for managing communication to proxies. | |
126 this.port = new ReceivePortSync(); | |
127 this.sendPort = this.port.toSendPort(); | |
128 | |
129 // Set of IDs that are global. | |
130 // These will not be freed on an exitScope(). | |
131 this.globalIds = {}; | |
132 | |
133 // Stack of scoped handles. | |
134 this.handleStack = []; | |
135 | |
136 // Stack of active scopes where each value is represented by the size of | |
137 // the handleStack at the beginning of the scope. When an active scope | |
138 // is popped, the handleStack is restored to where it was when the | |
139 // scope was entered. | |
140 this.scopeIndices = []; | |
141 } | |
142 | |
143 // Number of valid IDs. This is the number of objects (global and local) | |
144 // kept alive by this table. | |
145 ProxiedObjectTable.prototype.count = function () { | |
146 return Object.keys(this.map).length; | |
147 } | |
148 | |
149 // Number of total IDs ever allocated. | |
150 ProxiedObjectTable.prototype.total = function () { | |
151 return this.count() + this._deletedCount; | |
152 } | |
153 | |
154 // Adds an object to the table and return an ID for serialization. | |
155 ProxiedObjectTable.prototype.add = function (obj) { | |
156 if (this.scopeIndices.length == 0) { | |
157 throw "Cannot allocate a proxy outside of a scope."; | |
158 } | |
159 // TODO(vsm): Cache refs for each obj? | |
160 var ref = this.name + '-' + this._nextId++; | |
161 this.handleStack.push(ref); | |
162 this.map[ref] = obj; | |
163 return ref; | |
164 } | |
165 | |
166 ProxiedObjectTable.prototype._initializeOnce = function () { | |
167 if (!this._initialized) { | |
168 this._initialize(); | |
169 this._initialized = true; | |
170 } | |
171 } | |
172 | |
173 // Enters a new scope for this table. | |
174 ProxiedObjectTable.prototype.enterScope = function() { | |
175 this._initializeOnce(); | |
176 this.scopeIndices.push(this.handleStack.length); | |
177 } | |
178 | |
179 // Invalidates all non-global IDs in the current scope and | |
180 // exit the current scope. | |
181 ProxiedObjectTable.prototype.exitScope = function() { | |
182 var start = this.scopeIndices.pop(); | |
183 for (var i = start; i < this.handleStack.length; ++i) { | |
184 var key = this.handleStack[i]; | |
185 if (!this.globalIds.hasOwnProperty(key)) { | |
186 delete this.map[this.handleStack[i]]; | |
187 this._deletedCount++; | |
188 } | |
189 } | |
190 this.handleStack = this.handleStack.splice(0, start); | |
191 } | |
192 | |
193 // Makes this ID globally scope. It must be explicitly invalidated. | |
194 ProxiedObjectTable.prototype.globalize = function(id) { | |
195 this.globalIds[id] = true; | |
196 } | |
197 | |
198 // Invalidates this ID, potentially freeing its corresponding object. | |
199 ProxiedObjectTable.prototype.invalidate = function(id) { | |
200 var old = this.get(id); | |
201 delete this.globalIds[id]; | |
202 delete this.map[id]; | |
203 this._deletedCount++; | |
204 } | |
205 | |
206 // Gets the object or function corresponding to this ID. | |
207 ProxiedObjectTable.prototype.get = function (id) { | |
208 if (!this.map.hasOwnProperty(id)) { | |
209 throw 'Proxy ' + id + ' has been invalidated.' | |
210 } | |
211 return this.map[id]; | |
212 } | |
213 | |
214 ProxiedObjectTable.prototype._initialize = function () { | |
215 // Configure this table's port to forward methods, getters, and setters | |
216 // from the remote proxy to the local object. | |
217 var table = this; | |
218 | |
219 this.port.receive(function (message) { | |
220 // TODO(vsm): Support a mechanism to register a handler here. | |
221 try { | |
222 var object = table.get(message[0]); | |
223 var receiver = unbind(object); | |
224 var member = message[1]; | |
225 var kind = message[2]; | |
226 var args = message[3].map(deserialize); | |
227 if (kind == 'get') { | |
228 // Getter. | |
229 var field = member; | |
230 if (field in receiver && args.length == 0) { | |
231 var result = bindIfFunction(receiver[field], receiver); | |
232 return [ 'return', serialize(result) ]; | |
233 } | |
234 } else if (kind == 'set') { | |
235 // Setter. | |
236 var field = member; | |
237 if (args.length == 1) { | |
238 return [ 'return', serialize(receiver[field] = args[0]) ]; | |
239 } | |
240 } else if (kind == 'hasProperty') { | |
241 var field = member; | |
242 return [ 'return', field in receiver ]; | |
243 } else if (kind == 'apply') { | |
244 // Direct function invocation. | |
245 return [ 'return', serialize(receiver.apply(args[0], args.slice(1))) ] ; | |
246 } else if (member == '[]' && args.length == 1) { | |
247 // Index getter. | |
248 var result = bindIfFunction(receiver[args[0]], receiver); | |
249 return [ 'return', serialize(result) ]; | |
250 } else if (member == '[]=' && args.length == 2) { | |
251 // Index setter. | |
252 return [ 'return', serialize(receiver[args[0]] = args[1]) ]; | |
253 } else { | |
254 // Member function invocation. | |
255 var f = receiver[member]; | |
256 if (f) { | |
257 var result = f.apply(receiver, args); | |
258 return [ 'return', serialize(result) ]; | |
259 } | |
260 } | |
261 return [ 'none' ]; | |
262 } catch (e) { | |
263 return [ 'throws', e.toString() ]; | |
264 } | |
265 }); | |
266 } | |
267 | |
268 // Singleton for local proxied objects. | |
269 var proxiedObjectTable = new ProxiedObjectTable(); | |
270 | |
271 // Type for remote proxies to Dart objects. | |
272 function DartProxy(id, sendPort) { | |
273 this.id = id; | |
274 this.port = sendPort; | |
275 } | |
276 | |
277 // Serializes JS types to SendPortSync format: | |
278 // - primitives -> primitives | |
279 // - sendport -> sendport | |
280 // - Function -> [ 'funcref', function-id, sendport ] | |
281 // - Object -> [ 'objref', object-id, sendport ] | |
282 function serialize(message) { | |
283 if (message == null) { | |
284 return null; // Convert undefined to null. | |
285 } else if (typeof(message) == 'string' || | |
286 typeof(message) == 'number' || | |
287 typeof(message) == 'boolean') { | |
288 // Primitives are passed directly through. | |
289 return message; | |
290 } else if (message instanceof SendPortSync) { | |
291 // Non-proxied objects are serialized. | |
292 return message; | |
293 } else if (message instanceof BoundFunction && | |
294 typeof(message.object) == 'function') { | |
295 // Local function proxy. | |
296 return [ 'funcref', | |
297 proxiedObjectTable.add(message), | |
298 proxiedObjectTable.sendPort ]; | |
299 } else if (typeof(message) == 'function') { | |
300 if ('_dart_id' in message) { | |
301 // Remote function proxy. | |
302 var remoteId = message._dart_id; | |
303 var remoteSendPort = message._dart_port; | |
304 return [ 'funcref', remoteId, remoteSendPort ]; | |
305 } else { | |
306 // Local function proxy. | |
307 return [ 'funcref', | |
308 proxiedObjectTable.add(message), | |
309 proxiedObjectTable.sendPort ]; | |
310 } | |
311 } else if (message instanceof DartProxy) { | |
312 // Remote object proxy. | |
313 return [ 'objref', message.id, message.port ]; | |
314 } else { | |
315 // Local object proxy. | |
316 return [ 'objref', | |
317 proxiedObjectTable.add(message), | |
318 proxiedObjectTable.sendPort ]; | |
319 } | |
320 } | |
321 | |
322 function deserialize(message) { | |
323 if (message == null) { | |
324 return null; // Convert undefined to null. | |
325 } else if (typeof(message) == 'string' || | |
326 typeof(message) == 'number' || | |
327 typeof(message) == 'boolean') { | |
328 // Primitives are passed directly through. | |
329 return message; | |
330 } else if (message instanceof SendPortSync) { | |
331 // Serialized type. | |
332 return message; | |
333 } | |
334 var tag = message[0]; | |
335 switch (tag) { | |
336 case 'funcref': return deserializeFunction(message); | |
337 case 'objref': return deserializeObject(message); | |
338 } | |
339 throw 'Unsupported serialized data: ' + message; | |
340 } | |
341 | |
342 // Create a local function that forwards to the remote function. | |
343 function deserializeFunction(message) { | |
344 var id = message[1]; | |
345 var port = message[2]; | |
346 // TODO(vsm): Add a more robust check for a local SendPortSync. | |
347 if ("receivePort" in port) { | |
348 // Local function. | |
349 return unbind(proxiedObjectTable.get(id)); | |
350 } else { | |
351 // Remote function. Forward to its port. | |
352 var f = function () { | |
353 var depth = enterScope(); | |
354 try { | |
355 var args = Array.prototype.slice.apply(arguments); | |
356 args.splice(0, 0, this); | |
357 args = args.map(serialize); | |
358 var result = port.callSync([id, '#call', args]); | |
359 if (result[0] == 'throws') throw deserialize(result[1]); | |
360 return deserialize(result[1]); | |
361 } finally { | |
362 exitScope(depth); | |
363 } | |
364 }; | |
365 // Cache the remote id and port. | |
366 f._dart_id = id; | |
367 f._dart_port = port; | |
368 return f; | |
369 } | |
370 } | |
371 | |
372 // Creates a DartProxy to forwards to the remote object. | |
373 function deserializeObject(message) { | |
374 var id = message[1]; | |
375 var port = message[2]; | |
376 // TODO(vsm): Add a more robust check for a local SendPortSync. | |
377 if ("receivePort" in port) { | |
378 // Local object. | |
379 return proxiedObjectTable.get(id); | |
380 } else { | |
381 // Remote object. | |
382 return new DartProxy(id, port); | |
383 } | |
384 } | |
385 | |
386 // Remote handler to construct a new JavaScript object given its | |
387 // serialized constructor and arguments. | |
388 function construct(args) { | |
389 args = args.map(deserialize); | |
390 var constructor = unbind(args[0]); | |
391 args = Array.prototype.slice.call(args, 1); | |
392 | |
393 // Until 10 args, the 'new' operator is used. With more arguments we use a | |
394 // generic way that may not work, particularly when the constructor does not | |
395 // have an "apply" method. | |
396 var ret = null; | |
397 if (args.length === 0) { | |
398 ret = new constructor(); | |
399 } else if (args.length === 1) { | |
400 ret = new constructor(args[0]); | |
401 } else if (args.length === 2) { | |
402 ret = new constructor(args[0], args[1]); | |
403 } else if (args.length === 3) { | |
404 ret = new constructor(args[0], args[1], args[2]); | |
405 } else if (args.length === 4) { | |
406 ret = new constructor(args[0], args[1], args[2], args[3]); | |
407 } else if (args.length === 5) { | |
408 ret = new constructor(args[0], args[1], args[2], args[3], args[4]); | |
409 } else if (args.length === 6) { | |
410 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
411 args[5]); | |
412 } else if (args.length === 7) { | |
413 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
414 args[5], args[6]); | |
415 } else if (args.length === 8) { | |
416 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
417 args[5], args[6], args[7]); | |
418 } else if (args.length === 9) { | |
419 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
420 args[5], args[6], args[7], args[8]); | |
421 } else if (args.length === 10) { | |
422 ret = new constructor(args[0], args[1], args[2], args[3], args[4], | |
423 args[5], args[6], args[7], args[8], args[9]); | |
424 } else { | |
425 // Dummy Type with correct constructor. | |
426 var Type = function(){}; | |
427 Type.prototype = constructor.prototype; | |
428 | |
429 // Create a new instance | |
430 var instance = new Type(); | |
431 | |
432 // Call the original constructor. | |
433 ret = constructor.apply(instance, args); | |
434 ret = Object(ret) === ret ? ret : instance; | |
435 } | |
436 return serialize(ret); | |
437 } | |
438 | |
439 // Remote handler to return the top-level JavaScript context. | |
440 function context(data) { | |
441 return serialize(globalContext); | |
442 } | |
443 | |
444 // Remote handler to track number of live / allocated proxies. | |
445 function proxyCount() { | |
446 var live = proxiedObjectTable.count(); | |
447 var total = proxiedObjectTable.total(); | |
448 return [live, total]; | |
449 } | |
450 | |
451 // Return true if two JavaScript proxies are equal (==). | |
452 function proxyEquals(args) { | |
453 return deserialize(args[0]) == deserialize(args[1]); | |
454 } | |
455 | |
456 // Return true if a JavaScript proxy is instance of a given type (instanceof). | |
457 function proxyInstanceof(args) { | |
458 var obj = unbind(deserialize(args[0])); | |
459 var type = unbind(deserialize(args[1])); | |
460 return obj instanceof type; | |
461 } | |
462 | |
463 // Return true if a JavaScript proxy is instance of a given type (instanceof). | |
464 function proxyDeleteProperty(args) { | |
465 var obj = unbind(deserialize(args[0])); | |
466 var member = unbind(deserialize(args[1])); | |
467 delete obj[member]; | |
468 } | |
469 | |
470 function proxyConvert(args) { | |
471 return serialize(deserializeDataTree(args)); | |
472 } | |
473 | |
474 function deserializeDataTree(data) { | |
475 var type = data[0]; | |
476 var value = data[1]; | |
477 if (type === 'map') { | |
478 var obj = {}; | |
479 for (var i = 0; i < value.length; i++) { | |
480 obj[value[i][0]] = deserializeDataTree(value[i][1]); | |
481 } | |
482 return obj; | |
483 } else if (type === 'list') { | |
484 var list = []; | |
485 for (var i = 0; i < value.length; i++) { | |
486 list.push(deserializeDataTree(value[i])); | |
487 } | |
488 return list; | |
489 } else /* 'simple' */ { | |
490 return deserialize(value); | |
491 } | |
492 } | |
493 | |
494 function makeGlobalPort(name, f) { | |
495 var port = new ReceivePortSync(); | |
496 port.receive(f); | |
497 window.registerPort(name, port.toSendPort()); | |
498 } | |
499 | |
500 // Enters a new scope in the JavaScript context. | |
501 function enterJavaScriptScope() { | |
502 proxiedObjectTable.enterScope(); | |
503 } | |
504 | |
505 // Enters a new scope in both the JavaScript and Dart context. | |
506 var _dartEnterScopePort = null; | |
507 function enterScope() { | |
508 enterJavaScriptScope(); | |
509 if (!_dartEnterScopePort) { | |
510 _dartEnterScopePort = window.lookupPort('js-dart-enter-scope'); | |
511 } | |
512 return _dartEnterScopePort.callSync([]); | |
513 } | |
514 | |
515 // Exits the current scope (and invalidate local IDs) in the JavaScript | |
516 // context. | |
517 function exitJavaScriptScope() { | |
518 proxiedObjectTable.exitScope(); | |
519 } | |
520 | |
521 // Exits the current scope in both the JavaScript and Dart context. | |
522 var _dartExitScopePort = null; | |
523 function exitScope(depth) { | |
524 exitJavaScriptScope(); | |
525 if (!_dartExitScopePort) { | |
526 _dartExitScopePort = window.lookupPort('js-dart-exit-scope'); | |
527 } | |
528 return _dartExitScopePort.callSync([ depth ]); | |
529 } | |
530 | |
531 makeGlobalPort('dart-js-context', context); | |
532 makeGlobalPort('dart-js-create', construct); | |
533 makeGlobalPort('dart-js-proxy-count', proxyCount); | |
534 makeGlobalPort('dart-js-equals', proxyEquals); | |
535 makeGlobalPort('dart-js-instanceof', proxyInstanceof); | |
536 makeGlobalPort('dart-js-delete-property', proxyDeleteProperty); | |
537 makeGlobalPort('dart-js-convert', proxyConvert); | |
538 makeGlobalPort('dart-js-enter-scope', enterJavaScriptScope); | |
539 makeGlobalPort('dart-js-exit-scope', exitJavaScriptScope); | |
540 makeGlobalPort('dart-js-globalize', function(data) { | |
541 if (data[0] == "objref" || data[0] == "funcref") { | |
542 return proxiedObjectTable.globalize(data[1]); | |
543 } | |
544 throw 'Illegal type: ' + data[0]; | |
545 }); | |
546 makeGlobalPort('dart-js-invalidate', function(data) { | |
547 if (data[0] == "objref" || data[0] == "funcref") { | |
548 return proxiedObjectTable.invalidate(data[1]); | |
549 } | |
550 throw 'Illegal type: ' + data[0]; | |
551 }); | |
552 })(); | |
553 """; | |
554 | |
555 // Injects JavaScript source code onto the page. | |
556 // This is only used to load the bootstrapping code above. | |
557 void _inject(code) { | |
vsm
2013/06/11 16:17:44
We can also eliminate this.
alexandre.ardhuin
2013/06/12 21:29:19
Done.
| |
558 final script = new ScriptElement(); | |
559 script.type = 'text/javascript'; | |
560 script.innerHtml = code; | |
561 document.body.nodes.add(script); | |
562 } | |
563 | |
564 // Global ports to manage communication from Dart to JS. | |
565 SendPortSync _jsPortSync = null; | |
566 SendPortSync _jsPortCreate = null; | |
567 SendPortSync _jsPortProxyCount = null; | |
568 SendPortSync _jsPortEquals = null; | |
569 SendPortSync _jsPortInstanceof = null; | |
570 SendPortSync _jsPortDeleteProperty = null; | |
571 SendPortSync _jsPortConvert = null; | |
572 SendPortSync _jsEnterJavaScriptScope = null; | |
573 SendPortSync _jsExitJavaScriptScope = null; | |
574 SendPortSync _jsGlobalize = null; | |
575 SendPortSync _jsInvalidate = null; | |
576 | |
577 // Global ports to manage communication from JS to Dart. | |
578 ReceivePortSync _dartEnterDartScope = null; | |
579 ReceivePortSync _dartExitDartScope = null; | |
580 | |
581 // Initializes bootstrap code and ports. | |
582 void _initialize() { | |
583 if (_jsPortSync != null) return; | |
584 | |
585 // Test if the port is already defined. | |
586 try { | |
587 _jsPortSync = window.lookupPort('dart-js-context'); | |
588 } catch (e) { | |
589 // TODO(vsm): Suppress the exception until dartbug.com/5854 is fixed. | |
590 } | |
591 | |
592 // If not, try injecting the script. | |
593 if (_jsPortSync == null) { | |
594 _inject(_JS_BOOTSTRAP); | |
595 _jsPortSync = window.lookupPort('dart-js-context'); | |
596 } | |
597 | |
598 _jsPortCreate = window.lookupPort('dart-js-create'); | |
599 _jsPortProxyCount = window.lookupPort('dart-js-proxy-count'); | |
600 _jsPortEquals = window.lookupPort('dart-js-equals'); | |
601 _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); | |
602 _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property'); | |
603 _jsPortConvert = window.lookupPort('dart-js-convert'); | |
604 _jsEnterJavaScriptScope = window.lookupPort('dart-js-enter-scope'); | |
605 _jsExitJavaScriptScope = window.lookupPort('dart-js-exit-scope'); | |
606 _jsGlobalize = window.lookupPort('dart-js-globalize'); | |
607 _jsInvalidate = window.lookupPort('dart-js-invalidate'); | |
608 | |
609 _dartEnterDartScope = new ReceivePortSync() | |
610 ..receive((_) => _enterScope()); | |
611 _dartExitDartScope = new ReceivePortSync() | |
612 ..receive((args) => _exitScope(args[0])); | |
613 window.registerPort('js-dart-enter-scope', _dartEnterDartScope.toSendPort()); | |
614 window.registerPort('js-dart-exit-scope', _dartExitDartScope.toSendPort()); | |
615 } | |
616 | |
617 /** | |
618 * Returns a proxy to the global JavaScript context for this page. | |
619 */ | |
620 JsObject get context { | |
621 _enterScopeIfNeeded(); | |
622 return _deserialize(_jsPortSync.callSync([])); | |
623 } | |
624 | |
625 // Depth of current scope. Return 0 if no scope. | |
626 get _depth => _proxiedObjectTable._scopeIndices.length; | |
627 | |
628 // If we are not already in a scope, enter one and register a | |
629 // corresponding exit once we return to the event loop. | |
630 void _enterScopeIfNeeded() { | |
631 if (_depth == 0) { | |
632 var depth = _enterScope(); | |
633 runAsync(() => _exitScope(depth)); | |
634 } | |
635 } | |
636 | |
637 /** | |
638 * Executes the closure [f] within a scope. Any proxies created within this | |
639 * scope are invalidated afterward unless they are converted to a global proxy. | |
640 */ | |
641 scoped(f) { | |
642 var depth = _enterScope(); | |
643 try { | |
644 return f(); | |
645 } finally { | |
646 _exitScope(depth); | |
647 } | |
648 } | |
649 | |
650 int _enterScope() { | |
651 _initialize(); | |
652 _proxiedObjectTable.enterScope(); | |
653 _jsEnterJavaScriptScope.callSync([]); | |
654 return _proxiedObjectTable._scopeIndices.length; | |
655 } | |
656 | |
657 void _exitScope(int depth) { | |
658 assert(_proxiedObjectTable._scopeIndices.length == depth); | |
659 _jsExitJavaScriptScope.callSync([]); | |
660 _proxiedObjectTable.exitScope(); | |
661 } | |
662 | |
663 /** | |
664 * Retains the given [object] beyond the current scope. | |
665 * Instead, it will need to be explicitly released. | |
666 * The given [object] is returned for convenience. | |
667 */ | |
668 // TODO(aa) : change dynamic to Serializable<Proxy> if http://dartbug.com/9023 | |
669 // is fixed. | |
670 // TODO(aa) : change to "<T extends Serializable<Proxy>> T retain(T object)" | |
671 // once generic methods have landed. | |
672 dynamic retain(Serializable<JsObject> object) { | |
673 _jsGlobalize.callSync(_serialize(object.toJs())); | |
674 return object; | |
675 } | |
676 | |
677 /** | |
678 * Releases a retained [object]. | |
679 */ | |
680 void release(Serializable<JsObject> object) { | |
681 _jsInvalidate.callSync(_serialize(object.toJs())); | |
682 } | |
683 | |
684 /** | |
685 * Converts a json-like [data] to a JavaScript map or array and return a | |
686 * [JsObject] to it. | |
687 */ | |
688 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); | |
689 | |
690 /** | |
691 * Converts a local Dart function to a callback that can be passed to | |
692 * JavaScript. | |
693 * | |
694 * A callback can either be: | |
695 * | |
696 * - single-fire, in which case it is automatically invalidated after the first | |
697 * invocation, or | |
698 * - multi-fire, in which case it must be explicitly disposed. | |
699 */ | |
700 class Callback implements Serializable<JsFunction> { | |
701 final bool _manualDispose; | |
702 JsFunction _f; | |
703 | |
704 Callback._internal(this._manualDispose, Function f, bool withThis) { | |
705 final id = _proxiedObjectTable.add((List args) { | |
706 final arguments = new List.from(args); | |
707 if (!withThis) arguments.removeAt(0); | |
708 if (_manualDispose) { | |
709 return Function.apply(f, arguments); | |
710 } else { | |
711 try { | |
712 return Function.apply(f, arguments); | |
713 } finally { | |
714 _dispose(); | |
715 } | |
716 } | |
717 }); | |
718 _proxiedObjectTable.globalize(id); | |
719 _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id); | |
720 } | |
721 | |
722 _dispose() { | |
723 _proxiedObjectTable.invalidate(_f._id); | |
724 } | |
725 | |
726 JsFunction toJs() => _f; | |
727 | |
728 /** | |
729 * Disposes this [Callback] so that it may be collected. | |
730 * Once a [Callback] is disposed, it is an error to invoke it from JavaScript. | |
731 */ | |
732 dispose() { | |
733 assert(_manualDispose); | |
734 _dispose(); | |
735 } | |
736 | |
737 /** | |
738 * Creates a single-fire [Callback] that invokes [f]. The callback is | |
739 * automatically disposed after the first invocation. | |
740 */ | |
741 factory Callback.once(Function f, {bool withThis: false}) => | |
742 new Callback._internal(false, f, withThis); | |
743 | |
744 /** | |
745 * Creates a multi-fire [Callback] that invokes [f]. The callback must be | |
746 * explicitly disposed to avoid memory leaks. | |
747 */ | |
748 factory Callback.many(Function f, {bool withThis: false}) => | |
749 new Callback._internal(true, f, withThis); | |
750 } | |
751 | |
752 /** | |
753 * Proxies to JavaScript objects. | |
754 */ | |
755 class JsObject implements Serializable<JsObject> { | |
756 final SendPortSync _port; | |
757 final String _id; | |
758 | |
759 /** | |
760 * Constructs a [JsObject] to a new JavaScript object by invoking a (proxy to | |
761 * a) JavaScript [constructor]. The [arguments] list should contain either | |
762 * primitive values, DOM elements, or Proxies. | |
763 */ | |
764 factory JsObject(Serializable<JsFunction> constructor, [List arguments]) { | |
765 _enterScopeIfNeeded(); | |
766 final params = [constructor]; | |
767 if (arguments != null) params.addAll(arguments); | |
768 final serialized = params.map(_serialize).toList(); | |
769 final result = _jsPortCreate.callSync(serialized); | |
770 return _deserialize(result); | |
771 } | |
772 | |
773 /** | |
774 * Constructs a [JsObject] to a new JavaScript map or list created defined via | |
775 * Dart map or list. | |
776 */ | |
777 factory JsObject._json(data) { | |
778 _enterScopeIfNeeded(); | |
779 return _convert(data); | |
780 } | |
781 | |
782 static _convert(data) { | |
783 return _deserialize(_jsPortConvert.callSync(_serializeDataTree(data))); | |
784 } | |
785 | |
786 static _serializeDataTree(data) { | |
787 if (data is Map) { | |
788 final entries = new List(); | |
789 for (var key in data.keys) { | |
790 entries.add([key, _serializeDataTree(data[key])]); | |
791 } | |
792 return ['map', entries]; | |
793 } else if (data is Iterable) { | |
794 return ['list', data.map(_serializeDataTree).toList()]; | |
795 } else { | |
796 return ['simple', _serialize(data)]; | |
797 } | |
798 } | |
799 | |
800 JsObject._internal(this._port, this._id); | |
801 | |
802 JsObject toJs() => this; | |
803 | |
804 // Resolve whether this is needed. | |
805 operator[](arg) => _forward(this, '[]', 'method', [ arg ]); | |
806 | |
807 // Resolve whether this is needed. | |
808 operator[]=(key, value) => _forward(this, '[]=', 'method', [ key, value ]); | |
809 | |
810 // Test if this is equivalent to another Proxy. This essentially | |
811 // maps to JavaScript's == operator. | |
812 // TODO(vsm): Can we avoid forwarding to JS? | |
813 operator==(other) => identical(this, other) | |
814 ? true | |
815 : (other is JsObject && | |
816 _jsPortEquals.callSync([_serialize(this), _serialize(other)])); | |
817 | |
818 /** | |
819 * Check if this [JsObject] has a [name] property. | |
820 */ | |
821 bool hasProperty(String name) => _forward(this, name, 'hasProperty', []); | |
822 | |
823 /** | |
824 * Delete the [name] property. | |
825 */ | |
826 void deleteProperty(String name) { | |
827 _jsPortDeleteProperty.callSync([this, name].map(_serialize).toList()); | |
828 } | |
829 | |
830 /** | |
831 * Check if this [JsObject] is instance of [type]. | |
832 */ | |
833 bool instanceof(Serializable<JsFunction> type) => | |
834 _jsPortInstanceof.callSync([this, type].map(_serialize).toList()); | |
835 | |
836 String toString() { | |
837 try { | |
838 return _forward(this, 'toString', 'method', []); | |
839 } catch(e) { | |
840 return super.toString(); | |
841 } | |
842 } | |
843 | |
844 callMethod(String name, [List args]) { | |
845 return _forward(this, name, 'method', args != null ? args : []); | |
846 } | |
847 | |
848 // Forward member accesses to the backing JavaScript object. | |
849 static _forward(JsObject receiver, String member, String kind, List args) { | |
850 _enterScopeIfNeeded(); | |
851 var result = receiver._port.callSync([receiver._id, member, kind, | |
852 args.map(_serialize).toList()]); | |
853 switch (result[0]) { | |
854 case 'return': return _deserialize(result[1]); | |
855 case 'throws': throw _deserialize(result[1]); | |
856 case 'none': throw new NoSuchMethodError(receiver, member, args, {}); | |
857 default: throw 'Invalid return value'; | |
858 } | |
859 } | |
860 } | |
861 | |
862 /// A [JsObject] subtype to JavaScript functions. | |
863 class JsFunction extends JsObject implements Serializable<JsFunction> { | |
864 JsFunction._internal(SendPortSync port, String id) | |
865 : super._internal(port, id); | |
866 | |
867 apply(thisArg, [List args]) { | |
868 return JsObject._forward(this, '', 'apply', | |
869 [thisArg]..addAll(args == null ? [] : args)); | |
870 } | |
871 } | |
872 | |
873 /// Marker class used to indicate it is serializable to js. If a class is a | |
874 /// [Serializable] the "toJs" method will be called and the result will be used | |
875 /// as value. | |
876 abstract class Serializable<T> { | |
877 T toJs(); | |
878 } | |
879 | |
880 // A table to managed local Dart objects that are proxied in JavaScript. | |
881 class _ProxiedObjectTable { | |
882 // Debugging name. | |
883 final String _name; | |
884 | |
885 // Generator for unique IDs. | |
886 int _nextId; | |
887 | |
888 // Counter for invalidated IDs for debugging. | |
889 int _deletedCount; | |
890 | |
891 // Table of IDs to Dart objects. | |
892 final Map<String, Object> _registry; | |
893 | |
894 // Port to handle and forward requests to the underlying Dart objects. | |
895 // A remote proxy is uniquely identified by an ID and SendPortSync. | |
896 final ReceivePortSync _port; | |
897 | |
898 // The set of IDs that are global. These must be explicitly invalidated. | |
899 final Set<String> _globalIds; | |
900 | |
901 // The stack of valid IDs. | |
902 final List<String> _handleStack; | |
903 | |
904 // The stack of scopes, where each scope is represented by an index into the | |
905 // handleStack. | |
906 final List<int> _scopeIndices; | |
907 | |
908 // Enters a new scope. | |
909 enterScope() { | |
910 _scopeIndices.add(_handleStack.length); | |
911 } | |
912 | |
913 // Invalidates non-global IDs created in the current scope and | |
914 // restore to the previous scope. | |
915 exitScope() { | |
916 int start = _scopeIndices.removeLast(); | |
917 for (int i = start; i < _handleStack.length; ++i) { | |
918 String key = _handleStack[i]; | |
919 if (!_globalIds.contains(key)) { | |
920 _registry.remove(_handleStack[i]); | |
921 _deletedCount++; | |
922 } | |
923 } | |
924 if (start != _handleStack.length) { | |
925 _handleStack.removeRange(start, _handleStack.length - start); | |
926 } | |
927 } | |
928 | |
929 // Converts an ID to a global. | |
930 globalize(id) => _globalIds.add(id); | |
931 | |
932 // Invalidates an ID. | |
933 invalidate(id) { | |
934 var old = _registry[id]; | |
935 _globalIds.remove(id); | |
936 _registry.remove(id); | |
937 _deletedCount++; | |
938 return old; | |
939 } | |
940 | |
941 // Replaces the object referenced by an ID. | |
942 _replace(id, x) { | |
943 _registry[id] = x; | |
944 } | |
945 | |
946 _ProxiedObjectTable() : | |
947 _name = 'dart-ref', | |
948 _nextId = 0, | |
949 _deletedCount = 0, | |
950 _registry = {}, | |
951 _port = new ReceivePortSync(), | |
952 _handleStack = new List<String>(), | |
953 _scopeIndices = new List<int>(), | |
954 _globalIds = new Set<String>() { | |
955 _port.receive((msg) { | |
956 try { | |
957 final receiver = _registry[msg[0]]; | |
958 final method = msg[1]; | |
959 final args = msg[2].map(_deserialize).toList(); | |
960 if (method == '#call') { | |
961 final func = receiver as Function; | |
962 var result = _serialize(func(args)); | |
963 return ['return', result]; | |
964 } else { | |
965 // TODO(vsm): Support a mechanism to register a handler here. | |
966 throw 'Invocation unsupported on non-function Dart proxies'; | |
967 } | |
968 } catch (e) { | |
969 // TODO(vsm): callSync should just handle exceptions itself. | |
970 return ['throws', '$e']; | |
971 } | |
972 }); | |
973 } | |
974 | |
975 // Adds a new object to the table and return a new ID for it. | |
976 String add(x) { | |
977 _enterScopeIfNeeded(); | |
978 // TODO(vsm): Cache x and reuse id. | |
979 final id = '$_name-${_nextId++}'; | |
980 _registry[id] = x; | |
981 _handleStack.add(id); | |
982 return id; | |
983 } | |
984 | |
985 // Gets an object by ID. | |
986 Object get(String id) { | |
987 return _registry[id]; | |
988 } | |
989 | |
990 // Gets the current number of objects kept alive by this table. | |
991 get count => _registry.length; | |
992 | |
993 // Gets the total number of IDs ever allocated. | |
994 get total => count + _deletedCount; | |
995 | |
996 // Gets a send port for this table. | |
997 get sendPort => _port.toSendPort(); | |
998 } | |
999 | |
1000 // The singleton to manage proxied Dart objects. | |
1001 _ProxiedObjectTable _proxiedObjectTable = new _ProxiedObjectTable(); | |
1002 | |
1003 /// End of proxy implementation. | |
1004 | |
1005 // Dart serialization support. | |
1006 | |
1007 _serialize(var message) { | |
1008 if (message == null) { | |
1009 return null; // Convert undefined to null. | |
1010 } else if (message is String || | |
1011 message is num || | |
1012 message is bool) { | |
1013 // Primitives are passed directly through. | |
1014 return message; | |
1015 } else if (message is SendPortSync) { | |
1016 // Non-proxied objects are serialized. | |
1017 return message; | |
1018 } else if (message is JsFunction) { | |
1019 // Remote function proxy. | |
1020 return [ 'funcref', message._id, message._port ]; | |
1021 } else if (message is JsObject) { | |
1022 // Remote object proxy. | |
1023 return [ 'objref', message._id, message._port ]; | |
1024 } else if (message is Serializable) { | |
1025 // use of result of toJs() | |
1026 return _serialize(message.toJs()); | |
1027 } else { | |
1028 // Local object proxy. | |
1029 return [ 'objref', | |
1030 _proxiedObjectTable.add(message), | |
1031 _proxiedObjectTable.sendPort ]; | |
1032 } | |
1033 } | |
1034 | |
1035 _deserialize(var message) { | |
1036 deserializeFunction(message) { | |
1037 var id = message[1]; | |
1038 var port = message[2]; | |
1039 if (port == _proxiedObjectTable.sendPort) { | |
1040 // Local function. | |
1041 return _proxiedObjectTable.get(id); | |
1042 } else { | |
1043 // Remote function. Forward to its port. | |
1044 return new JsFunction._internal(port, id); | |
1045 } | |
1046 } | |
1047 | |
1048 deserializeObject(message) { | |
1049 var id = message[1]; | |
1050 var port = message[2]; | |
1051 if (port == _proxiedObjectTable.sendPort) { | |
1052 // Local object. | |
1053 return _proxiedObjectTable.get(id); | |
1054 } else { | |
1055 // Remote object. | |
1056 return new JsObject._internal(port, id); | |
1057 } | |
1058 } | |
1059 | |
1060 if (message == null) { | |
1061 return null; // Convert undefined to null. | |
1062 } else if (message is String || | |
1063 message is num || | |
1064 message is bool) { | |
1065 // Primitives are passed directly through. | |
1066 return message; | |
1067 } else if (message is SendPortSync) { | |
1068 // Serialized type. | |
1069 return message; | |
1070 } | |
1071 var tag = message[0]; | |
1072 switch (tag) { | |
1073 case 'funcref': return deserializeFunction(message); | |
1074 case 'objref': return deserializeObject(message); | |
1075 } | |
1076 throw 'Unsupported serialized data: $message'; | |
1077 } | |
1078 | |
1079 // Fetch the number of proxies to JavaScript objects. | |
1080 // This returns a 2 element list. The first is the number of currently | |
1081 // live proxies. The second is the total number of proxies ever | |
1082 // allocated. | |
1083 List _proxyCountJavaScript() => _jsPortProxyCount.callSync([]); | |
1084 | |
1085 /** | |
1086 * Returns the number of allocated proxy objects matching the given | |
1087 * conditions. By default, the total number of live proxy objects are | |
1088 * return. In a well behaved program, this should stay below a small | |
1089 * bound. | |
1090 * | |
1091 * Set [all] to true to return the total number of proxies ever allocated. | |
1092 * Set [dartOnly] to only count proxies to Dart objects (live or all). | |
1093 * Set [jsOnly] to only count proxies to JavaScript objects (live or all). | |
1094 */ | |
1095 int proxyCount({all: false, dartOnly: false, jsOnly: false}) { | |
1096 final js = !dartOnly; | |
1097 final dart = !jsOnly; | |
1098 final jsCounts = js ? _proxyCountJavaScript() : null; | |
1099 var sum = 0; | |
1100 if (!all) { | |
1101 if (js) | |
1102 sum += jsCounts[0]; | |
1103 if (dart) | |
1104 sum += _proxiedObjectTable.count; | |
1105 } else { | |
1106 if (js) | |
1107 sum += jsCounts[1]; | |
1108 if (dart) | |
1109 sum += _proxiedObjectTable.total; | |
1110 } | |
1111 return sum; | |
1112 } | |
1113 | |
1114 // Prints the number of live handles in Dart and JavaScript. This is for | |
1115 // debugging / profiling purposes. | |
1116 void _proxyDebug([String message = '']) { | |
1117 print('Proxy status $message:'); | |
1118 var dartLive = proxyCount(dartOnly: true); | |
1119 var dartTotal = proxyCount(dartOnly: true, all: true); | |
1120 var jsLive = proxyCount(jsOnly: true); | |
1121 var jsTotal = proxyCount(jsOnly: true, all: true); | |
1122 print(' Dart objects Live : $dartLive (out of $dartTotal ever allocated).'); | |
1123 print(' JS objects Live : $jsLive (out of $jsTotal ever allocated).'); | |
1124 } | |
OLD | NEW |