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