-
Notifications
You must be signed in to change notification settings - Fork 105
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
Conversation
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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
if (a_idx >= p->u.array.count || b_idx >= p->u.array.count) | ||
return 0; |
There was a problem hiding this comment.
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() 🤷
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about null?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When can this happen?
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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])) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
JS_ThrowTypeErrorArrayBufferOOB(ctx); | ||
return JS_EXCEPTION; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JS_ThrowTypeErrorArrayBufferOOB(ctx); | |
return JS_EXCEPTION; | |
return JS_ThrowTypeErrorArrayBufferOOB(ctx); |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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])) |
There was a problem hiding this comment.
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.
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.