Description
Many important Web API's use 'array buffers' (either ArrayBuffer or SharedArrayBuffer) or 'typed array views' (Int8Array, etc.) to represent bulk data. With externref
wasm can hold on to references to these objects, however wasm still needs to call out to imported JS code to actually access the contents of these buffers and this can be expensive.
Engines can optimize this pattern, but similarly to js-string-builtins it's nicer if critical operations have consistent performance and don't rely on heuristics.
In addition to the above, once there is a native type for a 'view of bytes' we could add extensions to wasm to create views of GC arrays or even linear memory.
Basic Proposal
Introduce a new type (slice share? mut? addrtype)
. A shorthand of sliceref
is given for (slice mut i32)
. A slice
is a view into some other buffer of bytes.
A slice differs from a wasm array by being an object that points into another buffer, as opposed to being just a buffer. A slice is expected to have slightly higher overhead than an array because it must always have an extra indirection to get the referenced buffer, while a wasm array can theoretically be implemented as just a pointer to the data it owns.
Open question: I'm not sure if a slice makes more sense as a heap type or a defined type (that would go in the type section).
JS-API conversions
With the JS-API, a wasm slice
is interchangeable with the JS array buffer types and typed array views. ToWebAssemblyValue(slice)
will coerce any typed array view down to it's underlying array buffer type, then pass it into wasm as a sliceref
. This is equivalent to the WebIDL 'BufferSource' behavior. ToJSValue(slice)
will return the array buffer type.
Open question: Do we need the coercion behavior from a typed array view down to the underlying buffer. This seems nice from an ergonomic perspective, but does break round tripping strict equality.
Sharing
JS array buffers can be shared using SharedArrayBuffer. A shared wasm slice is interchangeable with a SharedArrayBuffer, and an unshared wasm slice is interchangeable with a plain ArrayBuffer.
Address types
JS array buffers can have lengths larger than 32-bits. Following the memory64
proposal, slices are parameterized by their address type (either i32 or i64). A dynamic length check is performed when a sliceable value is passed into wasm to ensure the length fits within the slice type's limit. For growable array buffer types, this check must also apply to the maximum value.
Access instructions
We would define instructions for loading and storing to the slice, along with accessing the length.
- Get the length of a slice in bytes
slice.length $typeidx
$typeidx refers to a defined slice type
[(ref $t)] => $addrtype
- Load from the slice
slice.$numtype.load $typeidx
Defined for the numtypes {i32, i64, f32, f64}
$typeidx refers to a defined slice type
[(ref $t) addrtype] => [$numtype]
slice.$packedtype.load_s/u $typeidx
Defined for the packedtypes {i8, i16}
$typeidx refers to a defined slice type
[(ref $t) addrtype] => [$numtype]
- Store to the slice
slice.$storagetype.store $typeidx
Defined for the storagetypes {i8, i16, i32, i64, f32, f64}
$typeidx refers to a defined slice type
The slice must be mutable
[(ref $t) addrtype $storagetype] => []
Creation instructions (optional)
We do not need to define instructions for creating slices, and could rely on wasm getting access to slices from the host. However, it could be useful to have instructions for creating slices that refer to core wasm types.
- Creating a slice from a wasm GC array
slice.of_array $typeidx
$typeidx must refer to a wasm array
[(ref $typeidx) i32 i32] => [(slice mut? i32)]
- Creating a slice from a wasm memory
slice.of_memory $memidx
[(ref $t) addrtype addrtype] => [(slice mut addrtype)]
Bulk operations (optional)
We optionally could have bulk instructions for copying/filling slices. It's a bit annoying to add another column to the types that we want efficient copying between, but I'm not sure of another alternative. In JS engines, if there is no native instruction for this users could call out to JS and perform the equivalent operation.
Detaching and resizing
JS array buffer types can be detached via Web API's or through a transfer
method. This operation semantically changes the length of the buffer to zero. In addition, ArrayBuffers can be resizable and shrink in size. To handle this, slices are required to shrink in size if their underlying buffer source shrinks such that the slice's view is no longer valid. This matches the behavior of the JS typed array views.
This behavior will need to be permitted by the core wasm semantics, but we do not need to expose it through a core wasm instruction. This will permit other kinds of hosts of wasm that do not have such a behavior to optimize their slices a bit more.
Alternatives
The same approach taken by js-string-builtins could be used here. We could define a wasm:typed-array
builtin collection which exposes all the critical operations of JS typed arrays. This would be perfectly fine by me.
However, in this particular case I believe the semantics of JS array buffers and typed arrays are simple and general enough that we could define a core wasm extension that has broad applicability beyond just JS environments.
Naming
slice
and sliceref
seem like a fine name for this concept. It has been pointed out to me that slice
typically is used in programming languages for references to a range inside an array of arbitrary type, not just bytes. One alternative name would be buffer
and bufferref
.