Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(576)

Side by Side Diff: sdk/lib/js/dartium/js_dartium.dart

Issue 15782009: RFC: introduce dart:js (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: remove Element handling + fix js_dartium for passing tests Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698