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

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

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

Powered by Google App Engine
This is Rietveld 408576698