Bug 1055472 - Part 14: Make the various TypedArray constructors properly subclassable. (r=Waldo, r=bhackett)
authorEric Faust <efaustbmo@gmail.com>
Fri, 13 Nov 2015 18:22:22 -0800
changeset 275721 1e0c29a6d05886f9001513753aa0875654c13393
parent 275720 dfa71e4ddef2fbda2e09657fa3d0782bbf69e418
child 275722 c2573c84ff61692634696bcf72b7b6403e61d4af
push id29768
push usercbook@mozilla.com
push dateMon, 07 Dec 2015 13:16:29 +0000
treeherdermozilla-central@59bc3c7a83de [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersWaldo, bhackett
bugs1055472
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1055472 - Part 14: Make the various TypedArray constructors properly subclassable. (r=Waldo, r=bhackett)
js/src/jit-test/tests/TypedObject/bug976697.js
js/src/tests/ecma_6/Class/extendBuiltinConstructors.js
js/src/tests/ecma_6/TypedArray/constructor-non-detached.js
js/src/vm/SelfHosting.cpp
js/src/vm/TypedArrayCommon.h
js/src/vm/TypedArrayObject.cpp
deleted file mode 100644
--- a/js/src/jit-test/tests/TypedObject/bug976697.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// Test that instantiating a typed array on top of a neutered buffer
-// doesn't trip any asserts.
-//
-// Any copyright is dedicated to the Public Domain.
-// http://creativecommons.org/licenses/publicdomain/
-
-x = new ArrayBuffer();
-neuter(x, "same-data");
-new Uint32Array(x);
-gc();
-
-x = new ArrayBuffer();
-neuter(x, "change-data");
-new Uint32Array(x);
-gc();
--- a/js/src/tests/ecma_6/Class/extendBuiltinConstructors.js
+++ b/js/src/tests/ecma_6/Class/extendBuiltinConstructors.js
@@ -9,16 +9,34 @@ function testBuiltin(builtin, ...args) {
     }
 
     let instance = new inst(...args);
     assertEq(instance instanceof inst, true);
     assertEq(instance instanceof builtin, true);
     assertEq(instance.called, true);
 }
 
+function testBuiltinTypedArrays() {
+    let typedArrays = [Int8Array,
+                       Uint8Array,
+                       Uint8ClampedArray,
+                       Int16Array,
+                       Uint16Array,
+                       Int32Array,
+                       Uint32Array,
+                       Float32Array,
+                       Float64Array];
+
+    for (let array of typedArrays) {
+        testBuiltin(array);
+        testBuiltin(array, 5);
+        testBuiltin(array, new array());
+        testBuiltin(array, new ArrayBuffer());
+    }
+}
 
 testBuiltin(Function);
 testBuiltin(Object);
 testBuiltin(Boolean);
 testBuiltin(Error);
 testBuiltin(EvalError);
 testBuiltin(RangeError);
 testBuiltin(ReferenceError);
@@ -32,16 +50,17 @@ testBuiltin(Date, 5, 10);
 testBuiltin(RegExp);
 testBuiltin(RegExp, /Regexp Argument/);
 testBuiltin(RegExp, "String Argument");
 testBuiltin(Map);
 testBuiltin(Set);
 testBuiltin(WeakMap);
 testBuiltin(WeakSet);
 testBuiltin(ArrayBuffer);
+testBuiltinTypedArrays();
 
 `;
 
 if (classesEnabled())
     eval(test);
 
 if (typeof reportCompare === 'function')
     reportCompare(0,0,"OK");
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/TypedArray/constructor-non-detached.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!xulRuntime.shell)
+
+const constructors = [
+    Int8Array,
+    Uint8Array,
+    Uint8ClampedArray,
+    Int16Array,
+    Uint16Array,
+    Int32Array,
+    Uint32Array,
+    Float32Array,
+    Float64Array
+];
+
+for (var constructor of constructors) {
+    for (var neuterType of ["change-data", "same-data"]) {
+        var buf = new constructor();
+        neuter(buf.buffer, neuterType);
+        assertThrowsInstanceOf(()=> new constructor(buf), TypeError);
+
+        var buffer = new ArrayBuffer();
+        neuter(buffer, neuterType);
+        assertThrowsInstanceOf(()=> new constructor(buffer), TypeError);
+    }
+}
+
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -1202,20 +1202,31 @@ intrinsic_ConstructorForTypedArray(JSCon
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 1);
     MOZ_ASSERT(args[0].isObject());
     MOZ_ASSERT(IsAnyTypedArray(&args[0].toObject()));
 
     RootedObject object(cx, &args[0].toObject());
     JSProtoKey protoKey = StandardProtoKeyOrNull(object);
     MOZ_ASSERT(protoKey);
-    RootedValue ctor(cx, cx->global()->getConstructor(protoKey));
-    MOZ_ASSERT(ctor.isObject());
 
-    args.rval().set(ctor);
+    // While it may seem like an invariant that in any compartment,
+    // seeing a typed array object implies that the TypedArray constructor
+    // for that type is initialized on the compartment's global, this is not
+    // the case. When we construct a typed array given a cross-compartment
+    // ArrayBuffer, we put the constructed TypedArray in the same compartment
+    // as the ArrayBuffer. Since we use the prototype from the initial
+    // compartment, and never call the constructor in the ArrayBuffer's
+    // compartment from script, we are not guaranteed to have initialized
+    // the constructor.
+    RootedObject ctor(cx);
+    if (!GetBuiltinConstructor(cx, protoKey, &ctor))
+        return false;
+
+    args.rval().setObject(*ctor);
     return true;
 }
 
 static bool
 intrinsic_HostResolveImportedModule(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 2);
--- a/js/src/vm/TypedArrayCommon.h
+++ b/js/src/vm/TypedArrayCommon.h
@@ -107,16 +107,22 @@ AnyTypedArrayBytesPerElement(const JSObj
 
 inline uint32_t
 AnyTypedArrayByteLength(const JSObject* obj)
 {
     return obj->as<TypedArrayObject>().byteLength();
 }
 
 inline bool
+AnyTypedArrayIsDetached(const JSObject* obj)
+{
+    return obj->as<TypedArrayObject>().isNeutered();
+}
+
+inline bool
 IsAnyTypedArrayClass(const Class* clasp)
 {
     return IsTypedArrayClass(clasp);
 }
 
 class SharedOps
 {
   public:
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -175,16 +175,30 @@ js::ClampDoubleToUint8(const double x)
 }
 
 template<typename ElementType>
 static inline JSObject*
 NewArray(JSContext* cx, uint32_t nelements);
 
 namespace {
 
+// We allow nullptr for newTarget for all the creation methods, to allow for
+// JSFriendAPI functions that don't care about subclassing
+static bool
+GetPrototypeForInstance(JSContext* cx, HandleObject newTarget, MutableHandleObject proto)
+{
+    if (newTarget) {
+        if (!GetPrototypeFromConstructor(cx, newTarget, proto))
+            return false;
+    } else {
+        proto.set(nullptr);
+    }
+    return true;
+}
+
 template<typename NativeType>
 class TypedArrayObjectTemplate : public TypedArrayObject
 {
     friend class TypedArrayObject;
 
   public:
     typedef NativeType ElementType;
 
@@ -277,27 +291,18 @@ class TypedArrayObjectTemplate : public 
         }
     }
 
     static TypedArrayObject*
     makeProtoInstance(JSContext* cx, HandleObject proto, AllocKind allocKind)
     {
         MOZ_ASSERT(proto);
 
-        RootedObject obj(cx, NewBuiltinClassInstance(cx, instanceClass(), allocKind));
-        if (!obj)
-            return nullptr;
-
-        ObjectGroup* group = ObjectGroup::defaultNewGroup(cx, obj->getClass(),
-                                                          TaggedProto(proto.get()));
-        if (!group)
-            return nullptr;
-        obj->setGroup(group);
-
-        return &obj->as<TypedArrayObject>();
+        JSObject* obj = NewObjectWithClassProto(cx, instanceClass(), proto, allocKind);
+        return obj ? &obj->as<TypedArrayObject>() : nullptr;
     }
 
     static TypedArrayObject*
     makeTypedInstance(JSContext* cx, uint32_t len, gc::AllocKind allocKind)
     {
         const Class* clasp = instanceClass();
         if (len * sizeof(NativeType) >= TypedArrayObject::SINGLETON_BYTE_LENGTH) {
             JSObject* obj = NewBuiltinClassInstance(cx, clasp, allocKind, SingletonObject);
@@ -329,19 +334,26 @@ class TypedArrayObjectTemplate : public 
                  HandleObject proto)
     {
         MOZ_ASSERT_IF(!buffer, byteOffset == 0);
 
         gc::AllocKind allocKind = buffer
                                   ? GetGCObjectKind(instanceClass())
                                   : AllocKindForLazyBuffer(len * sizeof(NativeType));
 
+        // Subclassing mandates that we hand in the proto every time. Most of
+        // the time, though, that [[Prototype]] will not be interesting. If
+        // it isn't, we can do some more TI optimizations.
+        RootedObject checkProto(cx);
+        if (!GetBuiltinPrototype(cx, JSCLASS_CACHED_PROTO_KEY(instanceClass()), &checkProto))
+            return nullptr;
+
         AutoSetNewObjectMetadata metadata(cx);
         Rooted<TypedArrayObject*> obj(cx);
-        if (proto)
+        if (proto && proto != checkProto)
             obj = makeProtoInstance(cx, proto, allocKind);
         else
             obj = makeTypedInstance(cx, len, allocKind);
         if (!obj)
             return nullptr;
 
         bool isSharedMemory = buffer && IsSharedArrayBuffer(buffer.get());
 
@@ -424,20 +436,23 @@ class TypedArrayObjectTemplate : public 
             return false;
         args.rval().setObject(*obj);
         return true;
     }
 
     static JSObject*
     create(JSContext* cx, const CallArgs& args)
     {
+        MOZ_ASSERT(args.isConstructing());
+        RootedObject newTarget(cx, &args.newTarget().toObject());
+
         /* () or (number) */
         uint32_t len = 0;
         if (args.length() == 0 || ValueIsLength(args[0], &len))
-            return fromLength(cx, len);
+            return fromLength(cx, len, newTarget);
 
         /* (not an object) */
         if (!args[0].isObject()) {
             JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
             return nullptr;
         }
 
         RootedObject dataObj(cx, &args.get(0).toObject());
@@ -448,19 +463,23 @@ class TypedArrayObjectTemplate : public 
          * (type[] array)
          *
          * Otherwise create a new typed array and copy elements 0..len-1
          * properties from the object, treating it as some sort of array.
          * Note that offset and length will be ignored.  Note that a
          * shared array's values are copied here.
          */
         if (!UncheckedUnwrap(dataObj)->is<ArrayBufferObjectMaybeShared>())
-            return fromArray(cx, dataObj);
+            return fromArray(cx, dataObj, newTarget);
 
         /* (ArrayBuffer, [byteOffset, [length]]) */
+        RootedObject proto(cx);
+        if (!GetPrototypeFromConstructor(cx, newTarget, &proto))
+            return nullptr;
+
         int32_t byteOffset = 0;
         int32_t length = -1;
 
         if (args.length() > 1) {
             if (!ToInt32(cx, args[1], &byteOffset))
                 return nullptr;
             if (byteOffset < 0) {
                 JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
@@ -474,17 +493,17 @@ class TypedArrayObjectTemplate : public 
                 if (length < 0) {
                     JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
                                          JSMSG_TYPED_ARRAY_NEGATIVE_ARG, "2");
                     return nullptr;
                 }
             }
         }
 
-        return fromBuffer(cx, dataObj, byteOffset, length);
+        return fromBufferWithProto(cx, dataObj, byteOffset, length, proto);
     }
 
   public:
     static JSObject*
     fromBuffer(JSContext* cx, HandleObject bufobj, uint32_t byteOffset, int32_t lengthInt) {
         return fromBufferWithProto(cx, bufobj, byteOffset, lengthInt, nullptr);
     }
 
@@ -528,46 +547,55 @@ class TypedArrayObjectTemplate : public 
                  *
                  * Rather than hack some crazy solution together, implement
                  * this all using a private helper function, created when
                  * ArrayBufferObject was initialized and cached in the global.
                  * This reuses all the existing cross-compartment crazy so we
                  * don't have to do anything *uniquely* crazy here.
                  */
 
-                Rooted<JSObject*> proto(cx);
-                if (!GetBuiltinPrototype(cx, JSCLASS_CACHED_PROTO_KEY(instanceClass()), &proto))
-                    return nullptr;
+                RootedObject protoRoot(cx, proto);
+                if (!protoRoot) {
+                    if (!GetBuiltinPrototype(cx, JSCLASS_CACHED_PROTO_KEY(instanceClass()), &protoRoot))
+                        return nullptr;
+                }
 
                 InvokeArgs args(cx);
                 if (!args.init(3))
                     return nullptr;
 
                 args.setCallee(cx->compartment()->maybeGlobal()->createArrayFromBuffer<NativeType>());
                 args.setThis(ObjectValue(*bufobj));
                 args[0].setNumber(byteOffset);
                 args[1].setInt32(lengthInt);
-                args[2].setObject(*proto);
+                args[2].setObject(*protoRoot);
 
                 if (!Invoke(cx, args))
                     return nullptr;
                 return &args.rval().toObject();
             }
         }
 
         if (!IsArrayBuffer(bufobj) && !IsSharedArrayBuffer(bufobj)) {
             JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
             return nullptr; // must be arrayBuffer
         }
 
         Rooted<ArrayBufferObjectMaybeShared*> buffer(cx);
-        if (IsArrayBuffer(bufobj))
-            buffer = static_cast<ArrayBufferObjectMaybeShared*>(&AsArrayBuffer(bufobj));
-        else
+        if (IsArrayBuffer(bufobj)) {
+            ArrayBufferObject& buf = AsArrayBuffer(bufobj);
+            if (buf.isNeutered()) {
+                JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED);
+                return nullptr;
+            }
+
+            buffer = static_cast<ArrayBufferObjectMaybeShared*>(&buf);
+        } else {
             buffer = static_cast<ArrayBufferObjectMaybeShared*>(&AsSharedArrayBuffer(bufobj));
+        }
 
         if (byteOffset > buffer->byteLength() || byteOffset % sizeof(NativeType) != 0) {
             JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
             return nullptr; // invalid byteOffset
         }
 
         uint32_t len;
         if (lengthInt == -1) {
@@ -617,26 +645,31 @@ class TypedArrayObjectTemplate : public 
         if (!buf)
             return false;
 
         buffer.set(buf);
         return true;
     }
 
     static JSObject*
-    fromLength(JSContext* cx, uint32_t nelements)
+    fromLength(JSContext* cx, uint32_t nelements, HandleObject newTarget = nullptr)
     {
+        RootedObject proto(cx);
+        if (!GetPrototypeForInstance(cx, newTarget, &proto))
+            return nullptr;
+
         Rooted<ArrayBufferObject*> buffer(cx);
         if (!maybeCreateArrayBuffer(cx, nelements, &buffer))
             return nullptr;
-        return makeInstance(cx, buffer, 0, nelements);
+
+        return makeInstance(cx, buffer, 0, nelements, proto);
     }
 
     static JSObject*
-    fromArray(JSContext* cx, HandleObject other);
+    fromArray(JSContext* cx, HandleObject other, HandleObject newTarget = nullptr);
 
     static const NativeType
     getIndex(JSObject* obj, uint32_t index)
     {
         TypedArrayObject& tarray = obj->as<TypedArrayObject>();
         MOZ_ASSERT(index < tarray.length());
         return jit::AtomicOperations::loadSafeWhenRacy(tarray.viewDataEither().cast<NativeType*>() + index);
     }
@@ -666,30 +699,45 @@ typedef TypedArrayObjectTemplate<uint8_c
 template<typename T>
 struct TypedArrayObject::OfType
 {
     typedef TypedArrayObjectTemplate<T> Type;
 };
 
 template<typename T>
 /* static */ JSObject*
-TypedArrayObjectTemplate<T>::fromArray(JSContext* cx, HandleObject other)
+TypedArrayObjectTemplate<T>::fromArray(JSContext* cx, HandleObject other,
+                                       HandleObject newTarget /* = nullptr */)
 {
+    // Allow nullptr newTarget for FriendAPI methods, which don't care about
+    // subclassing.
+    RootedObject proto(cx);
+
     uint32_t len;
     if (IsAnyTypedArray(other)) {
+        if (!GetPrototypeForInstance(cx, newTarget, &proto))
+            return nullptr;
+
+        if (AnyTypedArrayIsDetached(other)) {
+            JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED);
+            return nullptr;
+        }
         len = AnyTypedArrayLength(other);
-    } else if (!GetLengthProperty(cx, other, &len)) {
-        return nullptr;
+    } else {
+        if (!GetLengthProperty(cx, other, &len))
+            return nullptr;
+        if (!GetPrototypeForInstance(cx, newTarget, &proto))
+            return nullptr;
     }
 
     Rooted<ArrayBufferObject*> buffer(cx);
     if (!maybeCreateArrayBuffer(cx, len, &buffer))
         return nullptr;
 
-    Rooted<TypedArrayObject*> obj(cx, makeInstance(cx, buffer, 0, len));
+    Rooted<TypedArrayObject*> obj(cx, makeInstance(cx, buffer, 0, len, proto));
     if (!obj || !TypedArrayMethods<TypedArrayObject>::setFromArrayLike(cx, obj, other, len))
         return nullptr;
     return obj;
 }
 
 bool
 TypedArrayConstructor(JSContext* cx, unsigned argc, Value* vp)
 {
@@ -1703,47 +1751,47 @@ TypedArrayObject::setElement(TypedArrayO
  *** JS impl
  ***/
 
 /*
  * TypedArrayObject boilerplate
  */
 
 #define IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS(Name,NativeType)                                    \
-  JS_FRIEND_API(JSObject*) JS_New ## Name ## Array(JSContext* cx, uint32_t nelements)          \
+  JS_FRIEND_API(JSObject*) JS_New ## Name ## Array(JSContext* cx, uint32_t nelements)           \
   {                                                                                             \
       return TypedArrayObjectTemplate<NativeType>::fromLength(cx, nelements);                   \
   }                                                                                             \
-  JS_FRIEND_API(JSObject*) JS_New ## Name ## ArrayFromArray(JSContext* cx, HandleObject other) \
+  JS_FRIEND_API(JSObject*) JS_New ## Name ## ArrayFromArray(JSContext* cx, HandleObject other)  \
   {                                                                                             \
       return TypedArrayObjectTemplate<NativeType>::fromArray(cx, other);                        \
   }                                                                                             \
-  JS_FRIEND_API(JSObject*) JS_New ## Name ## ArrayWithBuffer(JSContext* cx,                    \
+  JS_FRIEND_API(JSObject*) JS_New ## Name ## ArrayWithBuffer(JSContext* cx,                     \
                                HandleObject arrayBuffer, uint32_t byteOffset, int32_t length)   \
   {                                                                                             \
       return TypedArrayObjectTemplate<NativeType>::fromBuffer(cx, arrayBuffer, byteOffset,      \
                                                               length);                          \
   }                                                                                             \
   JS_FRIEND_API(bool) JS_Is ## Name ## Array(JSObject* obj)                                     \
   {                                                                                             \
       if (!(obj = CheckedUnwrap(obj)))                                                          \
           return false;                                                                         \
       const Class* clasp = obj->getClass();                                                     \
       return clasp == TypedArrayObjectTemplate<NativeType>::instanceClass();                    \
-  } \
-  JS_FRIEND_API(JSObject*) js::Unwrap ## Name ## Array(JSObject* obj)                          \
+  }                                                                                             \
+  JS_FRIEND_API(JSObject*) js::Unwrap ## Name ## Array(JSObject* obj)                           \
   {                                                                                             \
       obj = CheckedUnwrap(obj);                                                                 \
       if (!obj)                                                                                 \
           return nullptr;                                                                       \
       const Class* clasp = obj->getClass();                                                     \
       if (clasp == TypedArrayObjectTemplate<NativeType>::instanceClass())                       \
           return obj;                                                                           \
       return nullptr;                                                                           \
-  } \
+  }                                                                                             \
   const js::Class* const js::detail::Name ## ArrayClassPtr =                                    \
       &js::TypedArrayObject::classes[TypedArrayObjectTemplate<NativeType>::ArrayTypeID()];
 
 IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS(Int8, int8_t)
 IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS(Uint8, uint8_t)
 IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS(Uint8Clamped, uint8_clamped)
 IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS(Int16, int16_t)
 IMPL_TYPED_ARRAY_JSAPI_CONSTRUCTORS(Uint16, uint16_t)