Skip to content

Add resizable ArrayBuffers#646

Merged
bnoordhuis merged 5 commits intoquickjs-ng:masterfrom
bnoordhuis:rab
Nov 5, 2024
Merged

Add resizable ArrayBuffers#646
bnoordhuis merged 5 commits intoquickjs-ng:masterfrom
bnoordhuis:rab

Conversation

@bnoordhuis
Copy link
Copy Markdown
Contributor

This commit implements resizable ArrayBuffers - RABs for short - and extends typed arrays (TAs) to support fixed-length and length-tracking modes.

SharedArrayBuffers (SABs) also support the maxByteLength option now but I cheated and allocate all memory upfront because atomically resizing memory allocations is hard and this commit is already big and complex.

The lion's share is updating all the TA prototype methods to deal with RABs resizing underneath them. Method arguments can be arbitrary objects with arbitrary .valueOf methods and arbitrary side effects, like... resizing the RAB we're currently operating on.

This commit implements resizable ArrayBuffers - RABs for short - and
extends typed arrays (TAs) to support fixed-length and length-tracking
modes.

SharedArrayBuffers (SABs) also support the maxByteLength option now but
I cheated and allocate all memory upfront because atomically resizing
memory allocations is hard and this commit is already big and complex.

The lion's share is updating all the TA prototype methods to deal with
RABs resizing underneath them. Method arguments can be arbitrary objects
with arbitrary .valueOf methods and arbitrary side effects, like...
resizing the RAB we're currently operating on.
}
bc_put_u8(s, BC_TAG_ARRAY_BUFFER);
bc_put_leb128(s, abuf->byte_length);
bc_put_leb128(s, abuf->max_byte_length);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach is to define a new tag BC_TAG_ARRAY_BUFFER_RESIZABLE and save a few bytes in the BC_TAG_ARRAY_BUFFER case (which is likely the common case)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is saving a few bytes off the bc really critical?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. I mentioned it for completeness.

Comment on lines +53190 to +53191
if (a_idx >= p->u.array.count || b_idx >= p->u.array.count)
return 0;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, the spec also allows throwing an exception - and only here, in .sort() 🤷

Comment on lines +392 to +394
test262/test/staging/ArrayBuffer/resizable/object-define-property-define-properties.js:55: strict mode: unexpected error: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/staging/ArrayBuffer/resizable/object-define-property-parameter-conversion-grows.js:67: strict mode: unexpected error: TypeError: out-of-bound index in typed array
test262/test/staging/ArrayBuffer/resizable/object-define-property-parameter-conversion-shrinks.js:59: strict mode: unexpected error: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these failing? Honestly, because I didn't feel like fixing them anymore... the non-staging ones were already plenty of work, and these cases are even more deranged than usual.

Copy link
Copy Markdown
Contributor

@saghul saghul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few questions / comments, impressive work!

Just to satisfy my curiosity: is this approach (realloc) better than mmap here? Wouldn't having the pointer be stable simplify things? I suppose platform differences are annoying here?

Also, fun fact: I thought of going for RAB at some point. My plan was to expose the allocation functions like one currently can for SAB, with the limitation that pointers need to be stable, and have a default which allocated everything upfront. Then maybe look at mmap :-P

quickjs.c Outdated
static uint32_t typed_array_get_length(JSContext *ctx, JSObject *p);
static JSValue JS_ThrowTypeErrorDetachedArrayBuffer(JSContext *ctx);
// if you think the current name is lousy,
// I considered naming it JS_ThrowTypeErrorRABOOB
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name looks good, you can drop the comment 😅

if (typed_array_is_oob(p))
if (!(flags & JS_PROP_DEFINE_PROPERTY))
return TRUE; // per spec: no OOB exception
// XXX(bnoordhuis) questionable but generic methods like
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah lol, this conflicts with the PR I just made...

}
bc_put_u8(s, BC_TAG_ARRAY_BUFFER);
bc_put_leb128(s, abuf->byte_length);
bc_put_leb128(s, abuf->max_byte_length);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is saving a few bytes off the bc really critical?

JS_FreeValue(ctx, obj);
if (JS_IsException(val))
return JS_EXCEPTION;
if (JS_IsUndefined(val))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowed, curiously enough, and coerced to zero.

max_len = abuf->max_byte_length;
if (new_len > max_len)
return JS_ThrowTypeError(ctx, "invalid array buffer length");
// TODO(bnoordhuis) support externally managed RABs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By externally managed you mean when a C API where the user can set allocation functions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Buffers allocated by C code, not JS code.

quickjs.c Outdated
return JS_EXCEPTION;
if (abuf->shared) {
if (class_id == JS_CLASS_ARRAY_BUFFER)
return JS_ThrowTypeError(ctx, "resize called on SharedArrayBuffer");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayBuffer.prototype.resize.call(sab, 42) - but now that I think about it, that's already enforced by JS_GetOpaque2. I'll remove this.

quickjs.c Outdated
return JS_ThrowTypeError(ctx, "resize called on SharedArrayBuffer");
} else {
if (class_id == JS_CLASS_SHARED_ARRAY_BUFFER)
return JS_ThrowTypeError(ctx, "grow called on ArrayBuffer");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this happen?

if (abuf->shared) {
if (len < abuf->byte_length)
goto bad_length;
// Note this is off-spec; there's supposed to be a single atomic
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: how is this supposed to work? As in, how would t1 let t0 know that they grew the SAB? Some kind of shared memory region where the length is stored or?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, shared metadata.

args[2] = js_int32(offset);
args[3] = js_int32(count);
// result is length-tracking if source TA is and no explicit count is given
if (ta->track_rab && JS_IsUndefined(argv[1]))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check for isnull here too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No; it mirrors the if (!JS_IsUndefined(argv[1])) guard about 30 lines up.

quickjs.c Outdated
Comment on lines +53238 to +53239
JS_ThrowTypeErrorArrayBufferOOB(ctx);
return JS_EXCEPTION;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
JS_ThrowTypeErrorArrayBufferOOB(ctx);
return JS_EXCEPTION;
return JS_ThrowTypeErrorArrayBufferOOB(ctx);
Copy link
Copy Markdown
Contributor Author

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this approach (realloc) better than mmap here? Wouldn't having the pointer be stable simplify things? I suppose platform differences are annoying here?

mmap's page granularity would be very wasteful for small arraybuffers. Even for larger arraybuffers you still end up wasting, on average, half a page.

Stable pointers could be very useful for SABs but for RABs I don't think it matters. TA methods are agnostic to where the data lives. The hard part is getting the bound checks right.

}
bc_put_u8(s, BC_TAG_ARRAY_BUFFER);
bc_put_leb128(s, abuf->byte_length);
bc_put_leb128(s, abuf->max_byte_length);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. I mentioned it for completeness.

JS_FreeValue(ctx, obj);
if (JS_IsException(val))
return JS_EXCEPTION;
if (JS_IsUndefined(val))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowed, curiously enough, and coerced to zero.

max_len = abuf->max_byte_length;
if (new_len > max_len)
return JS_ThrowTypeError(ctx, "invalid array buffer length");
// TODO(bnoordhuis) support externally managed RABs
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Buffers allocated by C code, not JS code.

quickjs.c Outdated
return JS_EXCEPTION;
if (abuf->shared) {
if (class_id == JS_CLASS_ARRAY_BUFFER)
return JS_ThrowTypeError(ctx, "resize called on SharedArrayBuffer");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayBuffer.prototype.resize.call(sab, 42) - but now that I think about it, that's already enforced by JS_GetOpaque2. I'll remove this.

if (abuf->shared) {
if (len < abuf->byte_length)
goto bad_length;
// Note this is off-spec; there's supposed to be a single atomic
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, shared metadata.

args[2] = js_int32(offset);
args[3] = js_int32(count);
// result is length-tracking if source TA is and no explicit count is given
if (ta->track_rab && JS_IsUndefined(argv[1]))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No; it mirrors the if (!JS_IsUndefined(argv[1])) guard about 30 lines up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants