OLD | NEW |
---|---|
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /** | |
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; | 5 library dart.js; |
68 | 6 |
69 import 'dart:collection' show HashMap; | 7 import 'dart:nativewrappers'; |
70 import 'dart:html'; | |
71 import 'dart:isolate'; | |
72 | 8 |
73 // Global ports to manage communication from Dart to JS. | 9 JsObject _cachedContext; |
74 | 10 |
75 SendPortSync _jsPortSync = window.lookupPort('dart-js-context'); | 11 JsObject get _context native "Js_context_Callback"; |
76 SendPortSync _jsPortCreate = window.lookupPort('dart-js-create'); | |
77 SendPortSync _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); | |
78 SendPortSync _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property' ); | |
79 SendPortSync _jsPortConvert = window.lookupPort('dart-js-convert'); | |
80 | 12 |
81 final String _objectIdPrefix = 'dart-obj-ref'; | 13 JsObject get context { |
82 final String _functionIdPrefix = 'dart-fun-ref'; | 14 if (_cachedContext == null) { |
83 final _objectTable = new _ObjectTable(); | 15 _cachedContext = _context; |
84 final _functionTable = new _ObjectTable.forFunctions(); | 16 } |
85 | 17 return _cachedContext; |
86 // Port to handle and forward requests to the underlying Dart objects. | |
87 // A remote proxy is uniquely identified by an ID and SendPortSync. | |
88 ReceivePortSync _port = new ReceivePortSync() | |
89 ..receive((msg) { | |
90 try { | |
91 var id = msg[0]; | |
92 var method = msg[1]; | |
93 if (method == '#call') { | |
94 var receiver = _getObjectTable(id).get(id); | |
95 var result; | |
96 if (receiver is Function) { | |
97 // remove the first argument, which is 'this', but never | |
98 // used for a raw function | |
99 var args = msg[2].sublist(1).map(_deserialize).toList(); | |
100 result = Function.apply(receiver, args); | |
101 } else if (receiver is Callback) { | |
102 var args = msg[2].map(_deserialize).toList(); | |
103 result = receiver._call(args); | |
104 } else { | |
105 throw new StateError('bad function type: $receiver'); | |
106 } | |
107 return ['return', _serialize(result)]; | |
108 } else { | |
109 // TODO(vsm): Support a mechanism to register a handler here. | |
110 throw 'Invocation unsupported on non-function Dart proxies'; | |
111 } | |
112 } catch (e) { | |
113 // TODO(vsm): callSync should just handle exceptions itself. | |
114 return ['throws', '$e']; | |
115 } | |
116 }); | |
117 | |
118 _ObjectTable _getObjectTable(String id) { | |
119 if (id.startsWith(_functionIdPrefix)) return _functionTable; | |
120 if (id.startsWith(_objectIdPrefix)) return _objectTable; | |
121 throw new ArgumentError('internal error: invalid object id: $id'); | |
122 } | 18 } |
123 | 19 |
124 JsObject _context; | 20 class JsObject extends NativeFieldWrapperClass2 { |
21 JsObject.internal(); | |
125 | 22 |
126 /** | 23 factory JsObject(JsFunction constructor, [List arguments]) => _create(construc tor, arguments); |
127 * Returns a proxy to the global JavaScript context for this page. | 24 |
128 */ | 25 static JsObject _create(JsFunction constructor, arguments) native "JsObject_co nstructorCallback"; |
129 JsObject get context { | 26 |
130 if (_context == null) { | 27 /** |
131 var port = _jsPortSync; | 28 * Expert users only: |
132 if (port == null) { | 29 * Use this constructor only if you want to gain access to JS expandos |
133 return null; | 30 * attached to a browser native object such as a Node. |
31 * Not all native browser objects can be converted using fromBrowserObject. | |
32 * Currently the following types are supported: | |
33 * * Node | |
34 * * ArrayBuffer | |
35 * * Blob | |
36 * * ImageData | |
37 * * IDBKeyRange | |
38 * TODO(jacobr): support Event, Window and NodeList as well. | |
39 */ | |
40 factory JsObject.fromBrowserObject(var object) { | |
41 if (object is num || object is String || object is bool || object == null) { | |
42 throw new ArgumentError( | |
43 "object cannot be a num, string, bool, or null"); | |
134 } | 44 } |
135 _context = _deserialize(_jsPortSync.callSync([])); | 45 return _fromBrowserObject(object); |
136 } | |
137 return _context; | |
138 } | |
139 | |
140 /** | |
141 * Converts a json-like [data] to a JavaScript map or array and return a | |
142 * [JsObject] to it. | |
143 */ | |
144 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); | |
145 | |
146 /** | |
147 * Converts a local Dart function to a callback that can be passed to | |
148 * JavaScript. | |
149 */ | |
150 class Callback implements Serializable<JsFunction> { | |
151 final bool _withThis; | |
152 final Function _function; | |
153 JsFunction _jsFunction; | |
154 | |
155 Callback._(this._function, this._withThis) { | |
156 var id = _functionTable.add(this); | |
157 _jsFunction = new JsFunction._internal(_port.toSendPort(), id); | |
158 } | |
159 | |
160 factory Callback(Function f) => new Callback._(f, false); | |
161 factory Callback.withThis(Function f) => new Callback._(f, true); | |
162 | |
163 dynamic _call(List args) { | |
164 var arguments = (_withThis) ? args : args.sublist(1); | |
165 return Function.apply(_function, arguments); | |
166 } | |
167 | |
168 JsFunction toJs() => _jsFunction; | |
169 } | |
170 | |
171 /** | |
172 * Proxies to JavaScript objects. | |
173 */ | |
174 class JsObject implements Serializable<JsObject> { | |
175 final SendPortSync _port; | |
176 final String _id; | |
177 | |
178 /** | |
179 * Constructs a [JsObject] to a new JavaScript object by invoking a (proxy to | |
180 * a) JavaScript [constructor]. The [arguments] list should contain either | |
181 * primitive values, DOM elements, or Proxies. | |
182 */ | |
183 factory JsObject(Serializable<JsFunction> constructor, [List arguments]) { | |
184 final params = [constructor]; | |
185 if (arguments != null) params.addAll(arguments); | |
186 final serialized = params.map(_serialize).toList(); | |
187 final result = _jsPortCreate.callSync(serialized); | |
188 return _deserialize(result); | |
189 } | 46 } |
190 | 47 |
191 /** | 48 /** |
192 * Constructs a [JsObject] to a new JavaScript map or list created defined via | 49 * Converts a json-like [object] to a JavaScript map or array and return a |
193 * Dart map or list. | 50 * [JsObject] to it. |
194 */ | 51 */ |
195 factory JsObject._json(data) => _convert(data); | 52 factory JsObject.jsify(object) { |
196 | 53 if ((object is! Map) && (object is! Iterable)) { |
197 static _convert(data) => | 54 throw new ArgumentError("object must be a Map or Iterable"); |
198 _deserialize(_jsPortConvert.callSync(_serializeDataTree(data))); | |
199 | |
200 static _serializeDataTree(data) { | |
201 if (data is Map) { | |
202 final entries = new List(); | |
203 for (var key in data.keys) { | |
204 entries.add([key, _serializeDataTree(data[key])]); | |
205 } | |
206 return ['map', entries]; | |
207 } else if (data is Iterable) { | |
208 return ['list', data.map(_serializeDataTree).toList()]; | |
209 } else { | |
210 return ['simple', _serialize(data)]; | |
211 } | 55 } |
56 return _jsify(object); | |
212 } | 57 } |
213 | 58 |
214 JsObject._internal(this._port, this._id); | 59 static JSObject _jsify(object) native "JsObject_jsify"; |
215 | 60 |
216 JsObject toJs() => this; | 61 static JsObject _fromBrowserObject(object) native "JsObject_fromBrowserObject" ; |
217 | 62 |
218 // Resolve whether this is needed. | 63 operator[](key) native "JsObject_[]"; |
219 operator[](arg) => _forward(this, '[]', 'method', [ arg ]); | 64 operator[]=(key, value) native "JsObject_[]="; |
220 | 65 |
221 // Resolve whether this is needed. | 66 int get hashCode native "JsObject_hashCode"; |
222 operator[]=(key, value) => _forward(this, '[]=', 'method', [ key, value ]); | |
223 | 67 |
224 int get hashCode => _id.hashCode; | 68 operator==(other) => other is JsObject && _identityEquality(this, other); |
225 | 69 |
226 // Test if this is equivalent to another Proxy. This essentially | 70 static bool _identityEquality(JsObject a, JsObject b) native "JsObject_identit yEquality"; |
227 // maps to JavaScript's === operator. | |
228 operator==(other) => other is JsObject && this._id == other._id; | |
229 | 71 |
230 /** | 72 bool hasProperty(String property) native "JsObject_hasProperty"; |
alexandre.ardhuin
2013/10/20 07:17:00
`property` should be dynamic like in js_dart2js.da
| |
231 * Check if this [JsObject] has a [name] property. | |
232 */ | |
233 bool hasProperty(String name) => _forward(this, name, 'hasProperty', []); | |
234 | 73 |
235 /** | 74 void deleteProperty(JsFunction name) native "JsObject_deleteProperty"; |
alexandre.ardhuin
2013/10/20 07:17:00
`name` should be dynamic like in js_dart2js.dart
| |
236 * Delete the [name] property. | |
237 */ | |
238 void deleteProperty(String name) { | |
239 _jsPortDeleteProperty.callSync([this, name].map(_serialize).toList()); | |
240 } | |
241 | 75 |
242 /** | 76 bool instanceof(JsFunction type) native "JsObject_instanceof"; |
243 * Check if this [JsObject] is instance of [type]. | |
244 */ | |
245 bool instanceof(Serializable<JsFunction> type) => | |
246 _jsPortInstanceof.callSync([this, type].map(_serialize).toList()); | |
247 | 77 |
248 String toString() { | 78 String toString() { |
249 try { | 79 try { |
250 return _forward(this, 'toString', 'method', []); | 80 return _toString(); |
251 } catch(e) { | 81 } catch(e) { |
252 return super.toString(); | 82 return super.toString(); |
253 } | 83 } |
254 } | 84 } |
255 | 85 |
86 String _toString() native "JsObject_toString"; | |
87 | |
256 callMethod(String name, [List args]) { | 88 callMethod(String name, [List args]) { |
257 return _forward(this, name, 'method', args != null ? args : []); | 89 try { |
258 } | 90 return _callMethod(name, args); |
259 | 91 } catch(e) { |
260 // Forward member accesses to the backing JavaScript object. | 92 if (hasProperty(name)) { |
261 static _forward(JsObject receiver, String member, String kind, List args) { | 93 rethrow; |
262 var result = receiver._port.callSync([receiver._id, member, kind, | 94 } else { |
263 args.map(_serialize).toList()]); | 95 throw new NoSuchMethodError(this, new Symbol(name), args, null); |
264 switch (result[0]) { | |
265 case 'return': return _deserialize(result[1]); | |
266 case 'throws': throw _deserialize(result[1]); | |
267 case 'none': | |
268 throw new NoSuchMethodError(receiver, new Symbol(member), args, {}); | |
269 default: throw 'Invalid return value'; | |
270 } | |
271 } | |
272 } | |
273 | |
274 /// A [JsObject] subtype to JavaScript functions. | |
275 class JsFunction extends JsObject implements Serializable<JsFunction> { | |
276 JsFunction._internal(SendPortSync port, String id) | |
277 : super._internal(port, id); | |
278 | |
279 apply(thisArg, [List args]) { | |
280 return JsObject._forward(this, '', 'apply', | |
281 [thisArg]..addAll(args == null ? [] : args)); | |
282 } | |
283 } | |
284 | |
285 /// Marker class used to indicate it is serializable to js. If a class is a | |
286 /// [Serializable] the "toJs" method will be called and the result will be used | |
287 /// as value. | |
288 abstract class Serializable<T> { | |
289 T toJs(); | |
290 } | |
291 | |
292 class _ObjectTable { | |
293 final String name; | |
294 final Map<String, Object> objects; | |
295 final Map<Object, String> ids; | |
296 int nextId = 0; | |
297 | |
298 // Creates a table that uses an identity Map to store IDs | |
299 _ObjectTable() | |
300 : name = _objectIdPrefix, | |
301 objects = new HashMap<String, Object>(), | |
302 ids = new HashMap<Object, String>.identity(); | |
303 | |
304 // Creates a table that uses an equality-based Map to store IDs, since | |
305 // closurized methods may be equal, but not identical | |
306 _ObjectTable.forFunctions() | |
307 : name = _functionIdPrefix, | |
308 objects = new HashMap<String, Object>(), | |
309 ids = new HashMap<Object, String>(); | |
310 | |
311 // Adds a new object to the table. If [id] is not given, a new unique ID is | |
312 // generated. Returns the ID. | |
313 String add(Object o, {String id}) { | |
314 // TODO(vsm): Cache x and reuse id. | |
315 if (id == null) id = ids[o]; | |
316 if (id == null) id = '$name-${nextId++}'; | |
317 ids[o] = id; | |
318 objects[id] = o; | |
319 return id; | |
320 } | |
321 | |
322 // Gets an object by ID. | |
323 Object get(String id) => objects[id]; | |
324 | |
325 bool contains(String id) => objects.containsKey(id); | |
326 | |
327 String getId(Object o) => ids[o]; | |
328 | |
329 // Gets the current number of objects kept alive by this table. | |
330 get count => objects.length; | |
331 } | |
332 | |
333 // Dart serialization support. | |
334 | |
335 _serialize(var message) { | |
336 if (message == null) { | |
337 return null; // Convert undefined to null. | |
338 } else if (message is String || | |
339 message is num || | |
340 message is bool) { | |
341 // Primitives are passed directly through. | |
342 return message; | |
343 } else if (message is SendPortSync) { | |
344 // Non-proxied objects are serialized. | |
345 return message; | |
346 } else if (message is JsFunction) { | |
347 // Remote function proxy. | |
348 return ['funcref', message._id, message._port]; | |
349 } else if (message is JsObject) { | |
350 // Remote object proxy. | |
351 return ['objref', message._id, message._port]; | |
352 } else if (message is Serializable) { | |
353 // use of result of toJs() | |
354 return _serialize(message.toJs()); | |
355 } else if (message is Function) { | |
356 var id = _functionTable.getId(message); | |
357 if (id != null) { | |
358 return ['funcref', id, _port.toSendPort()]; | |
359 } | |
360 id = _functionTable.add(message); | |
361 return ['funcref', id, _port.toSendPort()]; | |
362 } else { | |
363 // Local object proxy. | |
364 return ['objref', _objectTable.add(message), _port.toSendPort()]; | |
365 } | |
366 } | |
367 | |
368 _deserialize(var message) { | |
369 deserializeFunction(message) { | |
370 var id = message[1]; | |
371 var port = message[2]; | |
372 if (port == _port.toSendPort()) { | |
373 // Local function. | |
374 return _functionTable.get(id); | |
375 } else { | |
376 // Remote function. | |
377 var jsFunction = _functionTable.get(id); | |
378 if (jsFunction == null) { | |
379 jsFunction = new JsFunction._internal(port, id); | |
380 _functionTable.add(jsFunction, id: id); | |
381 } | 96 } |
382 return jsFunction; | |
383 } | 97 } |
384 } | 98 } |
385 | 99 |
386 deserializeObject(message) { | 100 _callMethod(String name, List args) native "JsObject_callMethod"; |
387 var id = message[1]; | 101 } |
388 var port = message[2]; | |
389 if (port == _port.toSendPort()) { | |
390 // Local object. | |
391 return _objectTable.get(id); | |
392 } else { | |
393 // Remote object. | |
394 var jsObject = _objectTable.get(id); | |
395 if (jsObject == null) { | |
396 jsObject = new JsObject._internal(port, id); | |
397 _objectTable.add(jsObject, id: id); | |
398 } | |
399 return jsObject; | |
400 } | |
401 } | |
402 | 102 |
403 if (message == null) { | 103 class JsFunction extends JsObject { |
404 return null; // Convert undefined to null. | 104 JsFunction.internal(); |
405 } else if (message is String || | 105 |
406 message is num || | 106 /** |
407 message is bool) { | 107 * Returns a [JsFunction] that captures its 'this' binding and calls [f] |
408 // Primitives are passed directly through. | 108 * with the value of this passed as the first argument. |
409 return message; | 109 */ |
410 } else if (message is SendPortSync) { | 110 factory JsFunction.withThis(Function f) => _withThis(f); |
411 // Serialized type. | 111 |
412 return message; | 112 apply(List args, {thisArg}) native "JsFunction_apply"; |
413 } | 113 |
414 var tag = message[0]; | 114 static JsFunction _withThis(Function f) native "JsFunction_withThis"; |
415 switch (tag) { | |
416 case 'funcref': return deserializeFunction(message); | |
417 case 'objref': return deserializeObject(message); | |
418 } | |
419 throw 'Unsupported serialized data: $message'; | |
420 } | 115 } |
OLD | NEW |