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 /** | 5 library dart.Js; |
Jennifer Messerly
2013/10/11 19:21:09
did you mean to uppercase the "J"?
Jacob
2013/10/15 22:39:44
Done.
lowercased.
| |
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 | 6 |
67 library dart.js; | 7 import 'dart:nativewrappers'; |
68 | 8 |
69 import 'dart:html'; | 9 JsObject get context native "Js_context_Callback"; |
70 import 'dart:isolate'; | 10 |
71 | |
72 // Global ports to manage communication from Dart to JS. | |
73 SendPortSync _jsPortSync = window.lookupPort('dart-js-context'); | |
74 SendPortSync _jsPortCreate = window.lookupPort('dart-js-create'); | |
75 SendPortSync _jsPortInstanceof = window.lookupPort('dart-js-instanceof'); | |
76 SendPortSync _jsPortDeleteProperty = window.lookupPort('dart-js-delete-property' ); | |
77 SendPortSync _jsPortConvert = window.lookupPort('dart-js-convert'); | |
78 | |
79 | |
80 JsObject _context; | |
81 | |
82 /** | |
83 * Returns a proxy to the global JavaScript context for this page. | |
84 */ | |
85 JsObject get context { | |
86 if (_context == null) { | |
87 var port = _jsPortSync; | |
88 if (port == null) { | |
89 return null; | |
90 } | |
91 _context = _deserialize(_jsPortSync.callSync([])); | |
92 } | |
93 return _context; | |
94 } | |
95 | |
96 /** | 11 /** |
97 * Converts a json-like [data] to a JavaScript map or array and return a | 12 * Converts a json-like [data] to a JavaScript map or array and return a |
98 * [JsObject] to it. | 13 * [JsObject] to it. |
99 */ | 14 */ |
100 JsObject jsify(dynamic data) => data == null ? null : new JsObject._json(data); | 15 JsObject jsify(dynamic data) native "Js_jsify"; |
justinfagnani
2013/10/11 18:30:59
I think we should move this to a constructor on Js
Jacob
2013/10/15 22:39:44
Done. Although we still need a clearer name.
| |
101 | 16 |
102 /** | 17 /** |
103 * Converts a local Dart function to a callback that can be passed to | 18 * Returns a [JsFunction] that captures its 'this' binding and calls [f] |
104 * JavaScript. | 19 * with the value of this passed as the first argument. |
105 */ | 20 */ |
106 class Callback implements Serializable<JsFunction> { | 21 JsFunction captureThis(Function f) native "Js_captureThis"; |
107 JsFunction _f; | |
108 | 22 |
109 Callback._(Function f, bool withThis) { | 23 class JsObject extends NativeFieldWrapperClass1 { |
justinfagnani
2013/10/11 18:30:59
will NativeFieldWrapperClass1 be visible in the ob
Jacob
2013/10/15 22:39:44
Discussed offline.
I wouldn't worry too much about
| |
110 final id = _proxiedObjectTable.add((List args) { | 24 |
111 final arguments = new List.from(args); | 25 factory JsObject(JsFunction constructor, [List arguments]) => _create(construc tor, arguments); |
112 if (!withThis) arguments.removeAt(0); | 26 |
113 return Function.apply(f, arguments); | 27 static JsObject _create(JsFunction constructor, arguments) native "JsObject_co nstructorCallback"; |
114 }); | 28 |
115 _f = new JsFunction._internal(_proxiedObjectTable.sendPort, id); | 29 /** |
30 * Expert users only: | |
31 * Use this constructor only if you wish to get access to JS expandos | |
32 * attached to a WebKit native object such as a Node. | |
33 * An exception will be thrown if a primitive type is passed in passing one | |
34 * of these types to this method indicates an error. | |
35 */ | |
36 factory JsObject.fromDartObject(var object) { | |
vsm
2013/10/15 21:05:34
Perhaps fromBrowserObject? Will this throw if obj
Jacob
2013/10/15 22:39:44
one step ahead of me. :) just wrote a comment that
| |
37 if (object is num || object is String || object == null) { | |
justinfagnani
2013/10/11 18:30:59
add bool
Jacob
2013/10/15 22:39:44
synced offline. Instead restricting further and t
Jacob
2013/10/15 22:39:44
Synced offline. We'll restrict which types can be
| |
38 throw new IllegalArgumentException("object cannot be a num, string, or nu ll"); | |
justinfagnani
2013/10/11 18:30:59
what about returning a proxy to the boxed primitiv
Jacob
2013/10/15 22:39:44
same note as previous comment.
| |
39 } | |
40 return _fromDartObject(object); | |
116 } | 41 } |
117 | 42 |
118 factory Callback(Function f) => new Callback._(f, false); | 43 static JsObject _fromDartObject(object) native "JsObject_fromDartObject"; |
119 factory Callback.withThis(Function f) => new Callback._(f, true); | |
120 | 44 |
121 JsFunction toJs() => _f; | 45 operator[](key) native "JsObject_[]"; |
122 } | 46 operator[]=(key, value) native "JsObject_[]="; |
123 | 47 |
124 /** | 48 int get hashCode => 0; |
justinfagnani
2013/10/11 18:30:59
we talked about an identity-based hashCode
Jacob
2013/10/15 22:39:44
added that to this cl.
| |
125 * Proxies to JavaScript objects. | |
126 */ | |
127 class JsObject implements Serializable<JsObject> { | |
128 final SendPortSync _port; | |
129 final String _id; | |
130 | 49 |
131 /** | 50 operator==(other) => other is JsObject && _identityEquality(this, other); |
132 * Constructs a [JsObject] to a new JavaScript object by invoking a (proxy to | |
133 * a) JavaScript [constructor]. The [arguments] list should contain either | |
134 * primitive values, DOM elements, or Proxies. | |
135 */ | |
136 factory JsObject(Serializable<JsFunction> constructor, [List arguments]) { | |
137 final params = [constructor]; | |
138 if (arguments != null) params.addAll(arguments); | |
139 final serialized = params.map(_serialize).toList(); | |
140 final result = _jsPortCreate.callSync(serialized); | |
141 return _deserialize(result); | |
142 } | |
143 | 51 |
144 /** | 52 static bool _identityEquality(JsObject a, JsObject b) native "JsObject_identit yEquality"; |
145 * Constructs a [JsObject] to a new JavaScript map or list created defined via | |
146 * Dart map or list. | |
147 */ | |
148 factory JsObject._json(data) => _convert(data); | |
149 | 53 |
150 static _convert(data) => | 54 bool hasProperty(String property) native "JsObject_hasProperty"; |
151 _deserialize(_jsPortConvert.callSync(_serializeDataTree(data))); | |
152 | 55 |
153 static _serializeDataTree(data) { | 56 void deleteProperty(JsFunction name) native "JsObject_deleteProperty"; |
154 if (data is Map) { | |
155 final entries = new List(); | |
156 for (var key in data.keys) { | |
157 entries.add([key, _serializeDataTree(data[key])]); | |
158 } | |
159 return ['map', entries]; | |
160 } else if (data is Iterable) { | |
161 return ['list', data.map(_serializeDataTree).toList()]; | |
162 } else { | |
163 return ['simple', _serialize(data)]; | |
164 } | |
165 } | |
166 | 57 |
167 JsObject._internal(this._port, this._id); | 58 bool instanceof(JsFunction type) native "JsObject_instanceof"; |
168 | |
169 JsObject toJs() => this; | |
170 | |
171 // Resolve whether this is needed. | |
172 operator[](arg) => _forward(this, '[]', 'method', [ arg ]); | |
173 | |
174 // Resolve whether this is needed. | |
175 operator[]=(key, value) => _forward(this, '[]=', 'method', [ key, value ]); | |
176 | |
177 int get hashCode => _id.hashCode; | |
178 | |
179 // Test if this is equivalent to another Proxy. This essentially | |
180 // maps to JavaScript's === operator. | |
181 operator==(other) => other is JsObject && this._id == other._id; | |
182 | |
183 /** | |
184 * Check if this [JsObject] has a [name] property. | |
185 */ | |
186 bool hasProperty(String name) => _forward(this, name, 'hasProperty', []); | |
187 | |
188 /** | |
189 * Delete the [name] property. | |
190 */ | |
191 void deleteProperty(String name) { | |
192 _jsPortDeleteProperty.callSync([this, name].map(_serialize).toList()); | |
193 } | |
194 | |
195 /** | |
196 * Check if this [JsObject] is instance of [type]. | |
197 */ | |
198 bool instanceof(Serializable<JsFunction> type) => | |
199 _jsPortInstanceof.callSync([this, type].map(_serialize).toList()); | |
200 | 59 |
201 String toString() { | 60 String toString() { |
202 try { | 61 try { |
203 return _forward(this, 'toString', 'method', []); | 62 return _toString(); |
204 } catch(e) { | 63 } catch(e) { |
205 return super.toString(); | 64 return super.toString(); |
206 } | 65 } |
207 } | 66 } |
208 | 67 |
209 callMethod(String name, [List args]) { | 68 // FIXME: should we remove this? It seems like a a JS mis-feature we |
210 return _forward(this, name, 'method', args != null ? args : []); | 69 // shouldn't be exposing. If someone really wants it they can expose |
211 } | 70 // it themselves. If you have a JsObject then the value of jsType |
71 // is unlikely to be interesting as it should just be "object" or | |
72 // "function" unless you used the pro-user only forceToJs method. | |
73 // String get jsType native "JsObject_jsType"; | |
justinfagnani
2013/10/11 18:30:59
yes, remove it, it's not in current dart:js anyway
Jacob
2013/10/15 22:39:44
yay :) done.
| |
212 | 74 |
213 // Forward member accesses to the backing JavaScript object. | 75 String _toString() native "JsObject_toString"; |
214 static _forward(JsObject receiver, String member, String kind, List args) { | 76 |
215 var result = receiver._port.callSync([receiver._id, member, kind, | 77 callMethod(String name, [List args]) native "JsObject_callMethod"; |
216 args.map(_serialize).toList()]); | |
217 switch (result[0]) { | |
218 case 'return': return _deserialize(result[1]); | |
219 case 'throws': throw _deserialize(result[1]); | |
220 case 'none': | |
221 throw new NoSuchMethodError(receiver, new Symbol(member), args, {}); | |
222 default: throw 'Invalid return value'; | |
223 } | |
224 } | |
225 } | 78 } |
226 | 79 |
227 /// A [JsObject] subtype to JavaScript functions. | 80 class JsFunction extends JsObject { |
228 class JsFunction extends JsObject implements Serializable<JsFunction> { | 81 apply(thisArg, [List args]) native "JsFunction_apply"; |
justinfagnani
2013/10/11 18:30:59
I think we should make thisArg optional
| |
229 JsFunction._internal(SendPortSync port, String id) | |
230 : super._internal(port, id); | |
231 | |
232 apply(thisArg, [List args]) { | |
233 return JsObject._forward(this, '', 'apply', | |
234 [thisArg]..addAll(args == null ? [] : args)); | |
235 } | |
236 } | 82 } |
237 | |
238 /// Marker class used to indicate it is serializable to js. If a class is a | |
239 /// [Serializable] the "toJs" method will be called and the result will be used | |
240 /// as value. | |
241 abstract class Serializable<T> { | |
242 T toJs(); | |
243 } | |
244 | |
245 // A table to managed local Dart objects that are proxied in JavaScript. | |
246 class _ProxiedObjectTable { | |
247 // Debugging name. | |
248 final String _name; | |
249 | |
250 // Generator for unique IDs. | |
251 int _nextId; | |
252 | |
253 // Table of IDs to Dart objects. | |
254 final Map<String, Object> _registry; | |
255 | |
256 // Port to handle and forward requests to the underlying Dart objects. | |
257 // A remote proxy is uniquely identified by an ID and SendPortSync. | |
258 final ReceivePortSync _port; | |
259 | |
260 _ProxiedObjectTable() : | |
261 _name = 'dart-ref', | |
262 _nextId = 0, | |
263 _registry = {}, | |
264 _port = new ReceivePortSync() { | |
265 _port.receive((msg) { | |
266 try { | |
267 final receiver = _registry[msg[0]]; | |
268 final method = msg[1]; | |
269 final args = msg[2].map(_deserialize).toList(); | |
270 if (method == '#call') { | |
271 final func = receiver as Function; | |
272 var result = _serialize(func(args)); | |
273 return ['return', result]; | |
274 } else { | |
275 // TODO(vsm): Support a mechanism to register a handler here. | |
276 throw 'Invocation unsupported on non-function Dart proxies'; | |
277 } | |
278 } catch (e) { | |
279 // TODO(vsm): callSync should just handle exceptions itself. | |
280 return ['throws', '$e']; | |
281 } | |
282 }); | |
283 } | |
284 | |
285 // Adds a new object to the table and return a new ID for it. | |
286 String add(x) { | |
287 // TODO(vsm): Cache x and reuse id. | |
288 final id = '$_name-${_nextId++}'; | |
289 _registry[id] = x; | |
290 return id; | |
291 } | |
292 | |
293 // Gets an object by ID. | |
294 Object get(String id) { | |
295 return _registry[id]; | |
296 } | |
297 | |
298 // Gets the current number of objects kept alive by this table. | |
299 get count => _registry.length; | |
300 | |
301 // Gets a send port for this table. | |
302 get sendPort => _port.toSendPort(); | |
303 } | |
304 | |
305 // The singleton to manage proxied Dart objects. | |
306 _ProxiedObjectTable _proxiedObjectTable = new _ProxiedObjectTable(); | |
307 | |
308 /// End of proxy implementation. | |
309 | |
310 // Dart serialization support. | |
311 | |
312 _serialize(var message) { | |
313 if (message == null) { | |
314 return null; // Convert undefined to null. | |
315 } else if (message is String || | |
316 message is num || | |
317 message is bool) { | |
318 // Primitives are passed directly through. | |
319 return message; | |
320 } else if (message is SendPortSync) { | |
321 // Non-proxied objects are serialized. | |
322 return message; | |
323 } else if (message is JsFunction) { | |
324 // Remote function proxy. | |
325 return [ 'funcref', message._id, message._port ]; | |
326 } else if (message is JsObject) { | |
327 // Remote object proxy. | |
328 return [ 'objref', message._id, message._port ]; | |
329 } else if (message is Serializable) { | |
330 // use of result of toJs() | |
331 return _serialize(message.toJs()); | |
332 } else if (message is Function) { | |
333 return _serialize(new Callback(message)); | |
334 } else { | |
335 // Local object proxy. | |
336 return [ 'objref', | |
337 _proxiedObjectTable.add(message), | |
338 _proxiedObjectTable.sendPort ]; | |
339 } | |
340 } | |
341 | |
342 _deserialize(var message) { | |
343 deserializeFunction(message) { | |
344 var id = message[1]; | |
345 var port = message[2]; | |
346 if (port == _proxiedObjectTable.sendPort) { | |
347 // Local function. | |
348 return _proxiedObjectTable.get(id); | |
349 } else { | |
350 // Remote function. Forward to its port. | |
351 return new JsFunction._internal(port, id); | |
352 } | |
353 } | |
354 | |
355 deserializeObject(message) { | |
356 var id = message[1]; | |
357 var port = message[2]; | |
358 if (port == _proxiedObjectTable.sendPort) { | |
359 // Local object. | |
360 return _proxiedObjectTable.get(id); | |
361 } else { | |
362 // Remote object. | |
363 return new JsObject._internal(port, id); | |
364 } | |
365 } | |
366 | |
367 if (message == null) { | |
368 return null; // Convert undefined to null. | |
369 } else if (message is String || | |
370 message is num || | |
371 message is bool) { | |
372 // Primitives are passed directly through. | |
373 return message; | |
374 } else if (message is SendPortSync) { | |
375 // Serialized type. | |
376 return message; | |
377 } | |
378 var tag = message[0]; | |
379 switch (tag) { | |
380 case 'funcref': return deserializeFunction(message); | |
381 case 'objref': return deserializeObject(message); | |
382 } | |
383 throw 'Unsupported serialized data: $message'; | |
384 } | |
OLD | NEW |