| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library _interceptors; |
| |
| import 'dart:_js_embedded_names' |
| show DISPATCH_PROPERTY_NAME, TYPE_TO_INTERCEPTOR_MAP; |
| |
| import 'dart:collection' hide LinkedList, LinkedListEntry; |
| import 'dart:_internal' hide Symbol; |
| import "dart:_internal" as _symbol_dev show Symbol; |
| import 'dart:_js_helper' |
| show |
| allMatchesInStringUnchecked, |
| JSSyntaxRegExp, |
| Primitives, |
| argumentErrorValue, |
| checkBool, |
| checkInt, |
| checkNull, |
| checkNum, |
| checkString, |
| defineProperty, |
| diagnoseIndexError, |
| getIsolateAffinityTag, |
| initNativeDispatch, |
| initNativeDispatchFlag, |
| regExpGetNative, |
| regExpCaptureCount, |
| stringContainsUnchecked, |
| stringIndexOfStringUnchecked, |
| stringLastIndexOfUnchecked, |
| stringReplaceAllFuncUnchecked, |
| stringReplaceAllUnchecked, |
| stringReplaceFirstUnchecked, |
| stringReplaceFirstMappedUnchecked, |
| stringReplaceRangeUnchecked, |
| stringSplitUnchecked, |
| throwConcurrentModificationError, |
| lookupAndCacheInterceptor, |
| StringMatch, |
| firstMatchAfter; |
| |
| import 'dart:_foreign_helper' |
| show |
| getInterceptor, |
| JS, |
| JS_EFFECT, |
| JS_EMBEDDED_GLOBAL, |
| JS_INTERCEPTOR_CONSTANT, |
| JS_STRING_CONCAT; |
| |
| import 'dart:_rti' show getRuntimeType; |
| |
| import 'dart:math' show Random, ln2; |
| |
| part 'js_array.dart'; |
| part 'js_number.dart'; |
| part 'js_string.dart'; |
| |
| final String DART_CLOSURE_PROPERTY_NAME = |
| getIsolateAffinityTag(r'_$dart_dartClosure'); |
| |
| getDispatchProperty(object) { |
| return JS( |
| '', '#[#]', object, JS_EMBEDDED_GLOBAL('String', DISPATCH_PROPERTY_NAME)); |
| } |
| |
| setDispatchProperty(object, value) { |
| defineProperty( |
| object, JS_EMBEDDED_GLOBAL('String', DISPATCH_PROPERTY_NAME), value); |
| } |
| |
| // Avoid inlining this method because inlining gives us multiple allocation |
| // points for records which is bad because it leads to polymorphic access. |
| @pragma('dart2js:noInline') |
| makeDispatchRecord(interceptor, proto, extension, indexability) { |
| // Dispatch records are stored in the prototype chain, and in some cases, on |
| // instances. |
| // |
| // The record layout and field usage is designed to minimize the number of |
| // operations on the common paths. |
| // |
| // [interceptor] is the interceptor - a holder of methods for the object, |
| // i.e. the prototype of the interceptor class. |
| // |
| // [proto] is usually the prototype, used to check that the dispatch record |
| // matches the object and is not the dispatch record of a superclass. Other |
| // values: |
| // - `false` for leaf classes that need no check. |
| // - `true` for Dart classes where the object is its own interceptor (unused) |
| // - a function used to continue matching. |
| // |
| // [extension] is used for irregular cases. |
| // |
| // [indexability] is used to cache whether or not the object |
| // implements JavaScriptIndexingBehavior. |
| // |
| // proto interceptor extension action |
| // ----- ----------- --------- ------ |
| // false I use interceptor I |
| // true - use object |
| // P I if object's prototype is P, use I |
| // F - P if object's prototype is P, call F |
| |
| return JS('', '{i: #, p: #, e: #, x: #}', interceptor, proto, extension, |
| indexability); |
| } |
| |
| dispatchRecordInterceptor(record) => JS('', '#.i', record); |
| dispatchRecordProto(record) => JS('', '#.p', record); |
| dispatchRecordExtension(record) => JS('', '#.e', record); |
| bool? dispatchRecordIndexability(record) => JS('bool|Null', '#.x', record); |
| |
| /// Returns the interceptor for a native class instance. Used by |
| /// [getInterceptor]. |
| getNativeInterceptor(object) { |
| var record = getDispatchProperty(object); |
| |
| if (record == null) { |
| if (initNativeDispatchFlag == null) { |
| initNativeDispatch(); |
| record = getDispatchProperty(object); |
| } |
| } |
| |
| if (record != null) { |
| var proto = dispatchRecordProto(record); |
| if (false == proto) return dispatchRecordInterceptor(record); |
| if (true == proto) return object; |
| var objectProto = JS('', 'Object.getPrototypeOf(#)', object); |
| if (JS('bool', '# === #', proto, objectProto)) { |
| return dispatchRecordInterceptor(record); |
| } |
| |
| var extension = dispatchRecordExtension(record); |
| if (JS('bool', '# === #', extension, objectProto)) { |
| // TODO(sra): The discriminator returns a tag. The tag is an uncached or |
| // instance-cached tag, defaulting to instance-cached if caching |
| // unspecified. |
| var discriminatedTag = JS('', '(#)(#, #)', proto, object, record); |
| throw new UnimplementedError('Return interceptor for $discriminatedTag'); |
| } |
| } |
| |
| // Check for cached UnknownJavaScriptObject. This avoids doing the slow |
| // dispatch-record based lookup for repeated js-interop classes. |
| var constructor = JS('', '#.constructor', object); |
| var interceptor = lookupInterceptorByConstructor(constructor); |
| if (interceptor != null) return interceptor; |
| |
| // This takes care of dispatch-record based caching, but not constructor based |
| // caching of [UnknownJavaScriptObject]s. |
| interceptor = lookupAndCacheInterceptor(object); |
| if (interceptor != null) return interceptor; |
| |
| // JavaScript Objects created via object literals and `Object.create(null)` |
| // are 'plain' Objects. This test could be simplified and the dispatch path |
| // be faster if Object.prototype was pre-patched with a non-leaf dispatch |
| // record. |
| if (JS('bool', 'typeof # == "function"', object)) { |
| interceptor = JS_INTERCEPTOR_CONSTANT(JavaScriptFunction); |
| // TODO(sra): Investigate caching on `Function`. It might be impossible if |
| // some HTML embedded objects on some browsers are (still) JS functions. |
| return interceptor; |
| } |
| var proto = JS('', 'Object.getPrototypeOf(#)', object); |
| if (JS('bool', '# == null', proto)) { |
| // Nowhere to cache output. |
| return JS_INTERCEPTOR_CONSTANT(PlainJavaScriptObject); |
| } |
| interceptor = JS_INTERCEPTOR_CONSTANT(UnknownJavaScriptObject); |
| if (JS('bool', '# === Object.prototype', proto)) { |
| interceptor = JS_INTERCEPTOR_CONSTANT(PlainJavaScriptObject); |
| // TODO(sra): Investigate caching on 'Object'. It might be impossible if |
| // some native class is plain Object (e.g. like Firefox's ImageData). |
| return interceptor; |
| } |
| if (JS('bool', 'typeof # == "function"', constructor)) { |
| cacheInterceptorOnConstructor(constructor, interceptor); |
| return interceptor; |
| } |
| return JS_INTERCEPTOR_CONSTANT(UnknownJavaScriptObject); |
| } |
| |
| // A JS String or Symbol. |
| dynamic _JS_INTEROP_INTERCEPTOR_TAG = null; |
| get JS_INTEROP_INTERCEPTOR_TAG { |
| return _JS_INTEROP_INTERCEPTOR_TAG ??= getIsolateAffinityTag(r'_$dart_js'); |
| } |
| |
| lookupInterceptorByConstructor(constructor) { |
| return constructor == null |
| ? null |
| : JS('', '#[#]', constructor, JS_INTEROP_INTERCEPTOR_TAG); |
| } |
| |
| void cacheInterceptorOnConstructor(constructor, interceptor) { |
| defineProperty(constructor, JS_INTEROP_INTERCEPTOR_TAG, interceptor); |
| } |
| |
| var constructorToInterceptor = |
| JS('', 'typeof(self.WeakMap) == "undefined" ? new Map() : new WeakMap()'); |
| |
| XlookupInterceptorByConstructor(constructor) { |
| return JS('', '#.get(#)', constructorToInterceptor, constructor); |
| } |
| |
| void XcacheInterceptorOnConstructor(constructor, interceptor) { |
| JS('', '#.set(#, #)', constructorToInterceptor, constructor, interceptor); |
| } |
| |
| /// Data structure used to map a [Type] to the [Interceptor] and constructors |
| /// for that type. It is JavaScript array of 3N entries of adjacent slots |
| /// containing a [Type], followed by an [Interceptor] class for the type, |
| /// followed by a JavaScript object map for the constructors. |
| /// |
| /// The value of this variable is set by the compiler and contains only types |
| /// that are user extensions of native classes where the type occurs as a |
| /// constant in the program. |
| /// |
| /// The compiler, in CustomElementsAnalysis, assumes that [typeToInterceptorMap] |
| /// is accessed only by code that also calls [findIndexForWebComponentType]. If |
| /// this assumption is invalidated, the compiler will have to be updated. |
| get typeToInterceptorMap { |
| return JS_EMBEDDED_GLOBAL('', TYPE_TO_INTERCEPTOR_MAP); |
| } |
| |
| int? findIndexForNativeSubclassType(Type? type) { |
| if (JS('bool', '# == null', typeToInterceptorMap)) return null; |
| List map = JS('JSFixedArray', '#', typeToInterceptorMap); |
| for (int i = 0; i + 1 < map.length; i += 3) { |
| if (type == map[i]) { |
| return i; |
| } |
| } |
| return null; |
| } |
| |
| findInterceptorConstructorForType(Type? type) { |
| var index = findIndexForNativeSubclassType(type); |
| if (index == null) return null; |
| List map = JS('JSFixedArray', '#', typeToInterceptorMap); |
| return map[index + 1]; |
| } |
| |
| /// Returns a JavaScript function that runs the constructor on its argument, or |
| /// `null` if there is no such constructor. |
| /// |
| /// The returned function takes one argument, the web component object. |
| findConstructorForNativeSubclassType(Type? type, String name) { |
| var index = findIndexForNativeSubclassType(type); |
| if (index == null) return null; |
| List map = JS('JSFixedArray', '#', typeToInterceptorMap); |
| var constructorMap = map[index + 2]; |
| var constructorFn = JS('', '#[#]', constructorMap, name); |
| return constructorFn; |
| } |
| |
| findInterceptorForType(Type? type) { |
| var constructor = findInterceptorConstructorForType(type); |
| if (constructor == null) return null; |
| return JS('', '#.prototype', constructor); |
| } |
| |
| /// The base interceptor class. |
| /// |
| /// The code `r.foo(a)` is compiled to `getInterceptor(r).foo$1(r, a)`. The |
| /// value returned by [getInterceptor] holds the methods separately from the |
| /// state of the instance. The compiler converts the methods on an interceptor |
| /// to take the Dart `this` argument as an explicit `receiver` argument. The |
| /// JavaScript `this` parameter is bound to the interceptor. |
| /// |
| /// In order to have uniform call sites, if a method is defined on an |
| /// interceptor, methods of that name on plain unintercepted classes also use |
| /// the interceptor calling convention. The plain classes are |
| /// _self-interceptors_, and for them, `getInterceptor(r)` returns `r`. Methods |
| /// on plain unintercepted classes have a redundant `receiver` argument and, to |
| /// enable some optimizations, must ignore `receiver` in favour of `this`. |
| /// |
| /// In the case of mixins, a method may be placed on both an intercepted class |
| /// and an unintercepted class. In this case, the method must use the |
| /// `receiver` parameter. |
| /// |
| /// |
| /// There are various optimizations of the general call pattern. |
| /// |
| /// When the interceptor can be statically determined, it can be used directly: |
| /// |
| /// CONSTANT_INTERCEPTOR.foo$1(r, a) |
| /// |
| /// If there are only a few classes, [getInterceptor] can be specialized with a |
| /// more efficient dispatch: |
| /// |
| /// getInterceptor$specialized(r).foo$1(r, a) |
| /// |
| /// If it can be determined that the receiver is an unintercepted class, it can |
| /// be called directly: |
| /// |
| /// r.foo$1(r, a) |
| /// |
| /// If, further, it is known that the call site cannot call a foo that is |
| /// mixed-in to a native class, then it is known that the explicit receiver is |
| /// ignored, and space-saving dummy value can be passed instead: |
| /// |
| /// r.foo$1(0, a) |
| /// |
| /// This class defines implementations of *all* methods on [Object] so no |
| /// interceptor inherits an implementation from [Object]. This enables the |
| /// implementations on Object to ignore the explicit receiver argument, which |
| /// allows dummy receiver optimization. |
| abstract class Interceptor { |
| const Interceptor(); |
| |
| bool operator ==(other) => identical(this, other); |
| |
| int get hashCode => Primitives.objectHashCode(this); |
| |
| String toString() => Primitives.objectToHumanReadableString(this); |
| |
| // [Interceptor.noSuchMethod] is identical to [Object.noSuchMethod]. However, |
| // each copy is compiled differently. The presence of the method on an |
| // Interceptor class forces [noSuchMethod] to use interceptor calling |
| // convention. In the [Interceptor] version, `this` is the explicit receiver |
| // argument. In the [Object] version, as Object is not an intercepted class, |
| // `this` is the JavaScript receiver, and the explicit receiver is ignored. |
| // The noSuchMethod stubs for selectors that use the interceptor calling |
| // convention do not know the calling convention and forward `this` and |
| // `receiver` to one of these noSuchMethod implementations which selects the |
| // correct Dart receiver. |
| // |
| // We don't allow [noSuchMethod] on intercepted classes (that would force all |
| // calls to use interceptor calling convention). If we did allow it, the |
| // interceptor context would select the correct `this`. |
| dynamic noSuchMethod(Invocation invocation) { |
| throw new NoSuchMethodError(this, invocation.memberName, |
| invocation.positionalArguments, invocation.namedArguments); |
| } |
| |
| Type get runtimeType => getRuntimeType(this); |
| } |
| |
| /// The interceptor class for [bool]. |
| class JSBool extends Interceptor implements bool { |
| const JSBool(); |
| |
| // Note: if you change this, also change the function [S]. |
| String toString() => JS('String', r'String(#)', this); |
| |
| bool operator &(bool other) => JS('bool', "# && #", checkBool(other), this); |
| |
| bool operator |(bool other) => JS('bool', "# || #", checkBool(other), this); |
| |
| bool operator ^(bool other) => !identical(this, checkBool(other)); |
| |
| // The values here are SMIs, co-prime and differ about half of the bit |
| // positions, including the low bit, so they are different mod 2^k. |
| int get hashCode => this ? (2 * 3 * 23 * 3761) : (269 * 811); |
| |
| Type get runtimeType => bool; |
| } |
| |
| /// The interceptor class for [Null]. |
| /// |
| /// This class defines implementations for *all* methods on [Object] since |
| /// the methods on Object assume the receiver is non-null. This means that |
| /// JSNull will always be in the interceptor set for methods defined on Object. |
| class JSNull extends Interceptor implements Null { |
| const JSNull(); |
| |
| bool operator ==(other) => identical(null, other); |
| |
| // Note: if you change this, also change the function [S]. |
| String toString() => 'null'; |
| |
| int get hashCode => 0; |
| |
| // The spec guarantees that `null` is the singleton instance of the `Null` |
| // class. In the mirrors library we also have to patch the `type` getter to |
| // special case `null`. |
| Type get runtimeType => Null; |
| |
| dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); |
| } |
| |
| /// The supertype for JSString and JSArray. Used by the backend as to |
| /// have a type mask that contains the objects that we can use the |
| /// native JS [] operator and length on. |
| abstract class JSIndexable<E> { |
| int get length; |
| E operator [](int index); |
| } |
| |
| /// The supertype for JSMutableArray and |
| /// JavaScriptIndexingBehavior. Used by the backend to have a type mask |
| /// that contains the objects we can use the JS []= operator on. |
| abstract class JSMutableIndexable<E> extends JSIndexable<E> { |
| operator []=(int index, E value); |
| } |
| |
| /// The interface implemented by JavaScript objects. |
| /// |
| /// These are methods in addition to the regular Dart Object methods like |
| /// [Object.hashCode]. This is the type that should be exported by a JavaScript |
| /// interop library. |
| abstract class JSObject {} |
| |
| /// Superclass of all interop objects and native types defined in the web |
| /// libraries. |
| /// |
| /// This is the class static interop classes erase to and the class interop |
| /// extension types should use as the on-type. |
| class JavaScriptObject extends Interceptor { |
| const JavaScriptObject(); |
| } |
| |
| /// Interceptor base class for JavaScript objects not recognized as some more |
| /// specific native type. |
| /// |
| /// Note that this used to be `JavaScriptObject`. |
| class LegacyJavaScriptObject extends JavaScriptObject implements JSObject { |
| const LegacyJavaScriptObject(); |
| |
| // It would be impolite to stash a property on the object. |
| int get hashCode => 0; |
| |
| Type get runtimeType => JSObject; |
| |
| /// Returns the result of the JavaScript objects `toString` method. |
| String toString() => JS('String', 'String(#)', this); |
| } |
| |
| /// Interceptor for plain JavaScript objects created as JavaScript object |
| /// literals or `new Object()`. |
| class PlainJavaScriptObject extends LegacyJavaScriptObject { |
| const PlainJavaScriptObject(); |
| } |
| |
| /// Interceptor for unclassified JavaScript objects, typically objects with a |
| /// non-trivial prototype chain. |
| /// |
| /// This class also serves as a fallback for unknown JavaScript exceptions. |
| class UnknownJavaScriptObject extends LegacyJavaScriptObject { |
| const UnknownJavaScriptObject(); |
| } |
| |
| /// Interceptor for JavaScript function objects and Dart functions that have |
| /// been converted to JavaScript functions. |
| /// These interceptor methods are not always used as the JavaScript function |
| /// object has also been mangled to support Dart function calling conventions. |
| class JavaScriptFunction extends LegacyJavaScriptObject implements Function { |
| const JavaScriptFunction(); |
| |
| String toString() { |
| var dartClosure = JS('', '#.#', this, DART_CLOSURE_PROPERTY_NAME); |
| if (dartClosure == null) return super.toString(); |
| return 'JavaScript function for ${dartClosure.toString()}'; |
| } |
| } |