Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resizable ArrayBuffers #646

Merged
merged 5 commits into from
Nov 5, 2024
Merged

Add resizable ArrayBuffers #646

merged 5 commits into from
Nov 5, 2024

Conversation

bnoordhuis
Copy link
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.
@@ -34116,6 +34130,7 @@ static int JS_WriteArrayBuffer(BCWriterState *s, JSValue obj)
}
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
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
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
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.

quickjs.c Outdated
Comment on lines 53190 to 53191
if (a_idx >= p->u.array.count || b_idx >= p->u.array.count)
return 0;
Copy link
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
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
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

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
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 (!(flags & JS_PROP_DEFINE_PROPERTY))
return TRUE; // per spec: no OOB exception
// XXX(bnoordhuis) questionable but generic methods like
Copy link
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...

@@ -34116,6 +34130,7 @@ static int JS_WriteArrayBuffer(BCWriterState *s, JSValue obj)
}
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
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
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
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
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
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.

return JS_EXCEPTION;
if (abuf->shared) {
if (class_id == JS_CLASS_ARRAY_BUFFER)
return JS_ThrowTypeError(ctx, "resize called on SharedArrayBuffer");
Copy link
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
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.

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
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
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
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.

quickjs.c Outdated
if (JS_IsException(ta_buffer))
goto exception;
args[0] = this_val;
args[1] = ta_buffer;
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
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
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
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
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.

@@ -34116,6 +34130,7 @@ static int JS_WriteArrayBuffer(BCWriterState *s, JSValue obj)
}
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
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
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
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.

return JS_EXCEPTION;
if (abuf->shared) {
if (class_id == JS_CLASS_ARRAY_BUFFER)
return JS_ThrowTypeError(ctx, "resize called on SharedArrayBuffer");
Copy link
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
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.

quickjs.c Outdated
if (JS_IsException(ta_buffer))
goto exception;
args[0] = this_val;
args[1] = ta_buffer;
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
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.

@bnoordhuis bnoordhuis merged commit 37fe427 into quickjs-ng:master Nov 5, 2024
47 checks passed
@bnoordhuis bnoordhuis deleted the rab branch November 5, 2024 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants