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.
| } | ||
| 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.
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.
Is saving a few bytes off the bc really critical?
There was a problem hiding this comment.
I don't think so. I mentioned it for completeness.
| if (a_idx >= p->u.array.count || b_idx >= p->u.array.count) | ||
| return 0; |
There was a problem hiding this comment.
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.
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.
saghul
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
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.
By externally managed you mean when a C API where the user can set allocation functions?
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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"); |
| 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.
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.
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])) |
There was a problem hiding this comment.
Should we check for isnull here too?
There was a problem hiding this comment.
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.
| JS_ThrowTypeErrorArrayBufferOOB(ctx); | |
| return JS_EXCEPTION; | |
| return JS_ThrowTypeErrorArrayBufferOOB(ctx); |
bnoordhuis
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
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.
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"); |
There was a problem hiding this comment.
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.
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])) |
There was a problem hiding this comment.
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.