Implement growable SharedArrayBuffer

This commit is contained in:
Basilisk-Dev 2026-05-15 19:02:03 -04:00 • committed by wuggy
commit 4d11dcb096
5 changed files with 246 additions and 19 deletions

View file

@ -561,6 +561,7 @@ MSG_DEF(JSMSG_SHORT_TYPED_ARRAY_RETURNED, 2, JSEXN_TYPEERR, "expected TypedArray
// Shared array buffer // Shared array buffer
MSG_DEF(JSMSG_SHARED_ARRAY_BAD_LENGTH, 0, JSEXN_RANGEERR, "length argument out of range") MSG_DEF(JSMSG_SHARED_ARRAY_BAD_LENGTH, 0, JSEXN_RANGEERR, "length argument out of range")
MSG_DEF(JSMSG_SHARED_ARRAY_NOT_GROWABLE, 0, JSEXN_TYPEERR, "SharedArrayBuffer is not growable")
MSG_DEF(JSMSG_NON_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected SharedArrayBuffer, but species constructor returned non-SharedArrayBuffer") MSG_DEF(JSMSG_NON_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected SharedArrayBuffer, but species constructor returned non-SharedArrayBuffer")
MSG_DEF(JSMSG_SAME_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different SharedArrayBuffer, but species constructor returned same SharedArrayBuffer") MSG_DEF(JSMSG_SAME_SHARED_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different SharedArrayBuffer, but species constructor returned same SharedArrayBuffer")
MSG_DEF(JSMSG_SHORT_SHARED_ARRAY_BUFFER_RETURNED, 2, JSEXN_TYPEERR, "expected SharedArrayBuffer with at least {0} bytes, but species constructor returns SharedArrayBuffer with {1} bytes") MSG_DEF(JSMSG_SHORT_SHARED_ARRAY_BUFFER_RETURNED, 2, JSEXN_TYPEERR, "expected SharedArrayBuffer with at least {0} bytes, but species constructor returns SharedArrayBuffer with {1} bytes")

View file

@ -0,0 +1,40 @@
// |reftest| skip-if(!this.SharedArrayBuffer)
if (typeof SharedArrayBuffer === "function") {
const fixed = new SharedArrayBuffer(4);
assertEq(fixed.byteLength, 4);
assertEq(fixed.maxByteLength, 4);
assertEq(fixed.growable, false);
assertThrowsInstanceOf(() => fixed.grow(4), TypeError);
assertThrowsInstanceOf(() => new SharedArrayBuffer(-1), RangeError);
assertThrowsInstanceOf(() => new SharedArrayBuffer(8, {maxByteLength: 4}), RangeError);
let optionGetterCalled = false;
const growable = new SharedArrayBuffer(4, {
get maxByteLength() {
optionGetterCalled = true;
return 16;
}
});
assertEq(optionGetterCalled, true);
assertEq(growable.byteLength, 4);
assertEq(growable.maxByteLength, 16);
assertEq(growable.growable, true);
const before = new Uint8Array(growable);
before[0] = 37;
assertEq(growable.grow(12), undefined);
assertEq(growable.byteLength, 12);
assertEq(growable.maxByteLength, 16);
const after = new Uint8Array(growable);
assertEq(after.length, 12);
assertEq(after[0], 37);
assertThrowsInstanceOf(() => growable.grow(11), RangeError);
assertThrowsInstanceOf(() => growable.grow(17), RangeError);
}
if (typeof reportCompare === "function")
reportCompare(true, true);

View file

@ -1245,7 +1245,7 @@ js::WasmArrayBufferMaxSize(const ArrayBufferObjectMaybeShared* buf)
if (buf->is<ArrayBufferObject>()) if (buf->is<ArrayBufferObject>())
return buf->as<ArrayBufferObject>().wasmMaxSize(); return buf->as<ArrayBufferObject>().wasmMaxSize();
return Some(buf->as<SharedArrayBufferObject>().byteLength()); return Some(buf->as<SharedArrayBufferObject>().maxByteLength());
} }
/* static */ bool /* static */ bool

View file

@ -8,6 +8,7 @@
#include "mozilla/Atomics.h" #include "mozilla/Atomics.h"
#include "jsfriendapi.h" #include "jsfriendapi.h"
#include "jsnum.h"
#include "jsprf.h" #include "jsprf.h"
#ifdef XP_WIN #ifdef XP_WIN
@ -104,15 +105,18 @@ SharedArrayAllocSize(uint32_t length)
} }
SharedArrayRawBuffer* SharedArrayRawBuffer*
SharedArrayRawBuffer::New(JSContext* cx, uint32_t length) SharedArrayRawBuffer::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable)
{ {
// The value (uint32_t)-1 is used as a signal in various places, // The value (uint32_t)-1 is used as a signal in various places,
// so guard against it on principle. // so guard against it on principle.
MOZ_ASSERT(length != (uint32_t)-1); MOZ_ASSERT(length != (uint32_t)-1);
MOZ_ASSERT(maxLength != (uint32_t)-1);
MOZ_ASSERT(maxLength >= length);
// Add a page for the header and round to a page boundary. // Add a page for the header and round to a page boundary.
uint32_t allocSize = SharedArrayAllocSize(length); uint32_t allocationLength = growable ? maxLength : length;
if (allocSize <= length) uint32_t allocSize = SharedArrayAllocSize(allocationLength);
if (allocSize <= allocationLength)
return nullptr; return nullptr;
// Test >= to guard against the case where multiple extant runtimes // Test >= to guard against the case where multiple extant runtimes
@ -127,7 +131,8 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length)
} }
} }
bool preparedForAsmJS = jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length); bool preparedForAsmJS =
!growable && jit::JitOptions.asmJSAtomicsEnable && IsValidAsmJSHeapLength(length);
void* p = nullptr; void* p = nullptr;
if (preparedForAsmJS) { if (preparedForAsmJS) {
@ -161,8 +166,9 @@ SharedArrayRawBuffer::New(JSContext* cx, uint32_t length)
uint8_t* buffer = reinterpret_cast<uint8_t*>(p) + gc::SystemPageSize(); uint8_t* buffer = reinterpret_cast<uint8_t*>(p) + gc::SystemPageSize();
uint8_t* base = buffer - sizeof(SharedArrayRawBuffer); uint8_t* base = buffer - sizeof(SharedArrayRawBuffer);
SharedArrayRawBuffer* rawbuf = new (base) SharedArrayRawBuffer(buffer, length, preparedForAsmJS); SharedArrayRawBuffer* rawbuf =
MOZ_ASSERT(rawbuf->length == length); // Deallocation needs this new (base) SharedArrayRawBuffer(buffer, length, maxLength, growable, preparedForAsmJS);
MOZ_ASSERT(rawbuf->allocatedByteLength() == allocationLength); // Deallocation needs this.
return rawbuf; return rawbuf;
} }
@ -201,7 +207,7 @@ SharedArrayRawBuffer::dropReference()
MOZ_ASSERT(p.asValue() % gc::SystemPageSize() == 0); MOZ_ASSERT(p.asValue() % gc::SystemPageSize() == 0);
uint8_t* address = p.unwrap(/*safe - only reference*/); uint8_t* address = p.unwrap(/*safe - only reference*/);
uint32_t allocSize = SharedArrayAllocSize(this->length); uint32_t allocSize = SharedArrayAllocSize(this->allocatedByteLength());
if (this->preparedForAsmJS) { if (this->preparedForAsmJS) {
uint32_t mappedSize = SharedArrayMappedSize(allocSize); uint32_t mappedSize = SharedArrayMappedSize(allocSize);
@ -221,6 +227,23 @@ SharedArrayRawBuffer::dropReference()
numLive--; numLive--;
} }
bool
SharedArrayRawBuffer::growTo(uint32_t newLength)
{
MOZ_ASSERT(growable);
MOZ_ASSERT(newLength <= maxLength);
for (;;) {
uint32_t oldLength = length;
if (newLength < oldLength)
return false;
if (newLength == oldLength)
return true;
if (length.compareExchange(oldLength, newLength))
return true;
}
}
MOZ_ALWAYS_INLINE bool MOZ_ALWAYS_INLINE bool
SharedArrayBufferObject::byteLengthGetterImpl(JSContext* cx, const CallArgs& args) SharedArrayBufferObject::byteLengthGetterImpl(JSContext* cx, const CallArgs& args)
@ -237,6 +260,112 @@ SharedArrayBufferObject::byteLengthGetter(JSContext* cx, unsigned argc, Value* v
return CallNonGenericMethod<IsSharedArrayBuffer, byteLengthGetterImpl>(cx, args); return CallNonGenericMethod<IsSharedArrayBuffer, byteLengthGetterImpl>(cx, args);
} }
MOZ_ALWAYS_INLINE bool
SharedArrayBufferObject::maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args)
{
MOZ_ASSERT(IsSharedArrayBuffer(args.thisv()));
args.rval().setInt32(args.thisv().toObject().as<SharedArrayBufferObject>().maxByteLength());
return true;
}
bool
SharedArrayBufferObject::maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
return CallNonGenericMethod<IsSharedArrayBuffer, maxByteLengthGetterImpl>(cx, args);
}
MOZ_ALWAYS_INLINE bool
SharedArrayBufferObject::growableGetterImpl(JSContext* cx, const CallArgs& args)
{
MOZ_ASSERT(IsSharedArrayBuffer(args.thisv()));
args.rval().setBoolean(args.thisv().toObject().as<SharedArrayBufferObject>().isGrowable());
return true;
}
bool
SharedArrayBufferObject::growableGetter(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
return CallNonGenericMethod<IsSharedArrayBuffer, growableGetterImpl>(cx, args);
}
static bool
ReportSharedArrayBufferNotGrowable(JSContext* cx)
{
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_NOT_GROWABLE);
return false;
}
static bool
ReportSharedArrayBufferLengthOutOfRange(JSContext* cx)
{
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH);
return false;
}
static bool
GetSharedArrayBufferMaxByteLengthOption(JSContext* cx, HandleValue options,
uint32_t byteLength, uint32_t* maxByteLength,
bool* growable)
{
*maxByteLength = byteLength;
*growable = false;
if (!options.isObject())
return true;
RootedObject opts(cx, &options.toObject());
RootedValue maxByteLengthValue(cx);
if (!GetProperty(cx, opts, opts, cx->names().maxByteLength, &maxByteLengthValue))
return false;
if (maxByteLengthValue.isUndefined())
return true;
uint64_t max;
if (!ToIndex(cx, maxByteLengthValue, &max))
return false;
if (max > INT32_MAX || max < byteLength)
return ReportSharedArrayBufferLengthOutOfRange(cx);
*maxByteLength = uint32_t(max);
*growable = true;
return true;
}
MOZ_ALWAYS_INLINE bool
SharedArrayBufferObject::fun_grow_impl(JSContext* cx, const CallArgs& args)
{
MOZ_ASSERT(IsSharedArrayBuffer(args.thisv()));
Rooted<SharedArrayBufferObject*> buffer(cx,
&args.thisv().toObject().as<SharedArrayBufferObject>());
if (!buffer->isGrowable())
return ReportSharedArrayBufferNotGrowable(cx);
uint64_t newByteLength;
if (!ToIndex(cx, args.get(0), &newByteLength))
return false;
if (newByteLength > INT32_MAX)
return ReportSharedArrayBufferLengthOutOfRange(cx);
uint32_t newLength = uint32_t(newByteLength);
if (newLength > buffer->maxByteLength() || !buffer->growTo(newLength))
return ReportSharedArrayBufferLengthOutOfRange(cx);
args.rval().setUndefined();
return true;
}
bool
SharedArrayBufferObject::fun_grow(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
return CallNonGenericMethod<IsSharedArrayBuffer, fun_grow_impl>(cx, args);
}
bool bool
SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp) SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* vp)
{ {
@ -245,11 +374,19 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value*
if (!ThrowIfNotConstructing(cx, args, "SharedArrayBuffer")) if (!ThrowIfNotConstructing(cx, args, "SharedArrayBuffer"))
return false; return false;
uint64_t length64;
if (!ToIndex(cx, args.get(0), &length64))
return false;
// Bugs 1068458, 1161298: Limit length to 2^31-1. // Bugs 1068458, 1161298: Limit length to 2^31-1.
uint32_t length; if (length64 > INT32_MAX)
bool overflow_unused; return ReportSharedArrayBufferLengthOutOfRange(cx);
if (!ToLengthClamped(cx, args.get(0), &length, &overflow_unused) || length > INT32_MAX) { uint32_t length = uint32_t(length64);
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SHARED_ARRAY_BAD_LENGTH);
uint32_t maxByteLength;
bool growable;
if (!GetSharedArrayBufferMaxByteLengthOption(cx, args.get(1), length,
&maxByteLength, &growable))
{
return false; return false;
} }
@ -258,7 +395,7 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value*
if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) if (!GetPrototypeFromConstructor(cx, newTarget, &proto))
return false; return false;
JSObject* bufobj = New(cx, length, proto); JSObject* bufobj = New(cx, length, maxByteLength, growable, proto);
if (!bufobj) if (!bufobj)
return false; return false;
args.rval().setObject(*bufobj); args.rval().setObject(*bufobj);
@ -266,9 +403,10 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value*
} }
SharedArrayBufferObject* SharedArrayBufferObject*
SharedArrayBufferObject::New(JSContext* cx, uint32_t length, HandleObject proto) SharedArrayBufferObject::New(JSContext* cx, uint32_t length, uint32_t maxLength, bool growable,
HandleObject proto)
{ {
SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length); SharedArrayRawBuffer* buffer = SharedArrayRawBuffer::New(cx, length, maxLength, growable);
if (!buffer) if (!buffer)
return nullptr; return nullptr;
@ -341,7 +479,7 @@ SharedArrayBufferObject::addSizeOfExcludingThis(JSObject* obj, mozilla::MallocSi
// just live with the risk. // just live with the risk.
const SharedArrayBufferObject& buf = obj->as<SharedArrayBufferObject>(); const SharedArrayBufferObject& buf = obj->as<SharedArrayBufferObject>();
info->objectsNonHeapElementsShared += info->objectsNonHeapElementsShared +=
buf.byteLength() / buf.rawBufferObject()->refcount(); buf.rawBufferObject()->allocatedByteLength() / buf.rawBufferObject()->refcount();
} }
/* static */ void /* static */ void
@ -409,12 +547,15 @@ static const JSPropertySpec static_properties[] = {
}; };
static const JSFunctionSpec prototype_functions[] = { static const JSFunctionSpec prototype_functions[] = {
JS_FN("grow", SharedArrayBufferObject::fun_grow, 1, 0),
JS_SELF_HOSTED_FN("slice", "SharedArrayBufferSlice", 2, 0), JS_SELF_HOSTED_FN("slice", "SharedArrayBufferSlice", 2, 0),
JS_FS_END JS_FS_END
}; };
static const JSPropertySpec prototype_properties[] = { static const JSPropertySpec prototype_properties[] = {
JS_PSG("byteLength", SharedArrayBufferObject::byteLengthGetter, 0), JS_PSG("byteLength", SharedArrayBufferObject::byteLengthGetter, 0),
JS_PSG("growable", SharedArrayBufferObject::growableGetter, 0),
JS_PSG("maxByteLength", SharedArrayBufferObject::maxByteLengthGetter, 0),
JS_STRING_SYM_PS(toStringTag, "SharedArrayBuffer", JSPROP_READONLY), JS_STRING_SYM_PS(toStringTag, "SharedArrayBuffer", JSPROP_READONLY),
JS_PS_END JS_PS_END
}; };

View file

@ -44,7 +44,9 @@ class SharedArrayRawBuffer
{ {
private: private:
mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire> refcount_; mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire> refcount_;
uint32_t length; mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire> length;
uint32_t maxLength;
bool growable;
bool preparedForAsmJS; bool preparedForAsmJS;
// A list of structures representing tasks waiting on some // A list of structures representing tasks waiting on some
@ -52,9 +54,12 @@ class SharedArrayRawBuffer
FutexWaiter* waiters_; FutexWaiter* waiters_;
protected: protected:
SharedArrayRawBuffer(uint8_t* buffer, uint32_t length, bool preparedForAsmJS) SharedArrayRawBuffer(uint8_t* buffer, uint32_t length, uint32_t maxLength, bool growable,
bool preparedForAsmJS)
: refcount_(1), : refcount_(1),
length(length), length(length),
maxLength(maxLength),
growable(growable),
preparedForAsmJS(preparedForAsmJS), preparedForAsmJS(preparedForAsmJS),
waiters_(nullptr) waiters_(nullptr)
{ {
@ -62,7 +67,11 @@ class SharedArrayRawBuffer
} }
public: public:
static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length); static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length, uint32_t maxLength,
bool growable);
static SharedArrayRawBuffer* New(JSContext* cx, uint32_t length) {
return New(cx, length, length, false);
}
// This may be called from multiple threads. The caller must take // This may be called from multiple threads. The caller must take
// care of mutual exclusion. // care of mutual exclusion.
@ -85,6 +94,20 @@ class SharedArrayRawBuffer
return length; return length;
} }
uint32_t maxByteLength() const {
return growable ? maxLength : byteLength();
}
uint32_t allocatedByteLength() const {
return growable ? maxLength : byteLength();
}
bool isGrowable() const {
return growable;
}
[[nodiscard]] bool growTo(uint32_t newLength);
bool isPreparedForAsmJS() const { bool isPreparedForAsmJS() const {
return preparedForAsmJS; return preparedForAsmJS;
} }
@ -117,6 +140,9 @@ class SharedArrayRawBuffer
class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared
{ {
static bool byteLengthGetterImpl(JSContext* cx, const CallArgs& args); static bool byteLengthGetterImpl(JSContext* cx, const CallArgs& args);
static bool maxByteLengthGetterImpl(JSContext* cx, const CallArgs& args);
static bool growableGetterImpl(JSContext* cx, const CallArgs& args);
static bool fun_grow_impl(JSContext* cx, const CallArgs& args);
public: public:
// RAWBUF_SLOT holds a pointer (as "private" data) to the // RAWBUF_SLOT holds a pointer (as "private" data) to the
@ -128,13 +154,23 @@ class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared
static const Class class_; static const Class class_;
static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp); static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp);
static bool maxByteLengthGetter(JSContext* cx, unsigned argc, Value* vp);
static bool growableGetter(JSContext* cx, unsigned argc, Value* vp);
static bool fun_grow(JSContext* cx, unsigned argc, Value* vp);
static bool class_constructor(JSContext* cx, unsigned argc, Value* vp); static bool class_constructor(JSContext* cx, unsigned argc, Value* vp);
// Create a SharedArrayBufferObject with a new SharedArrayRawBuffer. // Create a SharedArrayBufferObject with a new SharedArrayRawBuffer.
static SharedArrayBufferObject* New(JSContext* cx, static SharedArrayBufferObject* New(JSContext* cx,
uint32_t length, uint32_t length,
uint32_t maxLength,
bool growable,
HandleObject proto = nullptr); HandleObject proto = nullptr);
static SharedArrayBufferObject* New(JSContext* cx,
uint32_t length,
HandleObject proto = nullptr) {
return New(cx, length, length, false, proto);
}
// Create a SharedArrayBufferObject using an existing SharedArrayRawBuffer. // Create a SharedArrayBufferObject using an existing SharedArrayRawBuffer.
static SharedArrayBufferObject* New(JSContext* cx, static SharedArrayBufferObject* New(JSContext* cx,
@ -164,6 +200,15 @@ class SharedArrayBufferObject : public ArrayBufferObjectMaybeShared
uint32_t byteLength() const { uint32_t byteLength() const {
return rawBufferObject()->byteLength(); return rawBufferObject()->byteLength();
} }
uint32_t maxByteLength() const {
return rawBufferObject()->maxByteLength();
}
bool isGrowable() const {
return rawBufferObject()->isGrowable();
}
[[nodiscard]] bool growTo(uint32_t newLength) {
return rawBufferObject()->growTo(newLength);
}
bool isPreparedForAsmJS() const { bool isPreparedForAsmJS() const {
return rawBufferObject()->isPreparedForAsmJS(); return rawBufferObject()->isPreparedForAsmJS();
} }