LibWeb: Implement resizable ArrayBuffers for Wasm memories

This commit adds the toResizableBuffer() and toFixedLengthBuffer()
methods to WebAssembly.Memory. This includes the necessary hook to
HostResizeArrayBuffer. Some modifications to function signatures in
LibWeb/WebAssembly/Memory.h were also made (changing the return type
from WebIDL::ExceptionOr to JS::ThrowCompletionOr) to allow the use of
some code in the aforementioned hook.

Note: the hook for HostGrowSharedArrayBuffer isn't implemented, since
LibJS doesn't seem to have complete support for growable
SharedArrayBuffers; the relevant methods/getters don't even exist on
the prototype, let alone HostGrowSharedArrayBuffer!

This should help pass the WebAssembly.Memory WPT tests included in
Interop 2025, except those pertaining to growable SharedArrayBuffers.
This commit is contained in:
CountBleck 2025-08-17 14:14:24 -07:00 committed by Ali Mohammad Pur
commit d0d5bffb2d
Notes: github-actions[bot] 2025-08-23 06:27:40 +00:00
6 changed files with 246 additions and 6 deletions

View file

@ -50,6 +50,7 @@
#include <LibWeb/HTML/WorkletGlobalScope.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/ServiceWorker/ServiceWorkerGlobalScope.h>
#include <LibWeb/WebAssembly/WebAssembly.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
namespace Web::Bindings {
@ -690,6 +691,14 @@ void initialize_main_thread_vm(AgentType type)
s_main_thread_vm->host_unrecognized_date_string = [](StringView date) {
dbgln("Unable to parse date string: \"{}\"", date);
};
s_main_thread_vm->host_resize_array_buffer = [default_host_resize_array_buffer = move(s_main_thread_vm->host_resize_array_buffer)](JS::ArrayBuffer& buffer, size_t new_byte_length) -> JS::ThrowCompletionOr<JS::HandledByHost> {
auto wasm_handled = TRY(WebAssembly::Detail::host_resize_array_buffer(*s_main_thread_vm, buffer, new_byte_length));
if (wasm_handled == JS::HandledByHost::Handled)
return JS::HandledByHost::Handled;
return default_host_resize_array_buffer(buffer, new_byte_length);
};
}
JS::VM& main_thread_vm()

View file

@ -89,7 +89,7 @@ void Memory::visit_edges(Visitor& visitor)
}
// https://webassembly.github.io/spec/js-api/#dom-memory-grow
WebIDL::ExceptionOr<u32> Memory::grow(u32 delta)
JS::ThrowCompletionOr<u32> Memory::grow(u32 delta)
{
auto& vm = this->vm();
@ -106,6 +106,91 @@ WebIDL::ExceptionOr<u32> Memory::grow(u32 delta)
return previous_size;
}
// https://webassembly.github.io/threads/js-api/index.html#dom-memory-tofixedlengthbuffer
WebIDL::ExceptionOr<GC::Ref<JS::ArrayBuffer>> Memory::to_fixed_length_buffer()
{
auto& vm = this->vm();
// 1. Let buffer be this.[[BufferObject]].
// 2. Let memaddr be this.[[Memory]].
// 3. If IsSharedArrayBuffer(buffer) is false,
if (m_shared == Shared::No) {
// 1. If IsFixedLengthArrayBuffer(buffer) is true, return buffer.
if (m_buffer->is_fixed_length())
return GC::Ref(*m_buffer);
// 2. Otherwise,
// 1. Let fixedBuffer be the result of creating a fixed length memory buffer from memaddr.
auto fixed_buffer = create_a_fixed_length_memory_buffer(vm, realm(), m_address, m_shared);
// 2. Perform ! DetachArrayBuffer(buffer, "WebAssembly.Memory").
MUST(JS::detach_array_buffer(vm, *m_buffer, JS::PrimitiveString::create(vm, "WebAssembly.Memory"_string)));
// 3. Set this.[[BufferObject]] to fixedBuffer.
m_buffer = fixed_buffer;
// 4. Return fixedBuffer.
return fixed_buffer;
}
// 4. Otherwise,
// 1. Let map be the surrounding agent's associated Memory object cache.
auto& cache = Detail::get_cache(realm());
// 2. Assert: map[memaddr] exists.
// 3. Let newMemory be map[memaddr].
auto new_memory = cache.get_memory_instance(m_address);
VERIFY(new_memory.has_value());
// 4. Let newBufferObject be newMemory.[[BufferObject]].
auto new_buffer_object = new_memory.value()->m_buffer;
// 5. Set this.[[BufferObject]] to newBufferObject.
m_buffer = new_buffer_object;
// 6. Return newBufferObject.
return GC::Ref(*new_buffer_object);
}
// https://webassembly.github.io/spec/js-api/#dom-memory-toresizablebuffer
WebIDL::ExceptionOr<GC::Ref<JS::ArrayBuffer>> Memory::to_resizable_buffer()
{
auto& vm = this->vm();
// 1. Let buffer be this.[[BufferObject]].
// 2. If IsFixedLengthArrayBuffer(buffer) is false, return buffer.
if (!m_buffer->is_fixed_length())
return GC::Ref(*m_buffer);
// 3. Let memaddr be this.[[Memory]].
// 4. Let store be the surrounding agents associated store.
auto& store = Detail::get_cache(realm()).abstract_machine().store();
// 5. Let memtype be mem_type(store, memaddr).
auto mem_type = store.get(m_address)->type();
// 6. If memtype has a max,
// 1. Let maxsize be the max value in memtype.
// 7. Otherwise,
// 1. Let maxsize be 65536 × 65536.
size_t max_size = mem_type.limits().max().value_or(65536) * Wasm::Constants::page_size;
// 8. Let resizableBuffer be the result of creating a resizable memory buffer from memaddr and maxsize.
auto resizable_buffer = TRY(create_a_resizable_memory_buffer(vm, realm(), m_address, m_shared, max_size));
// https://webassembly.github.io/threads/js-api/index.html#dom-memory-toresizablebuffer
// 5. If IsSharedArrayBuffer(buffer) is false,
// 9. Perform ! DetachArrayBuffer(buffer, "WebAssembly.Memory").
if (!m_buffer->is_shared_array_buffer())
MUST(JS::detach_array_buffer(vm, *m_buffer, JS::PrimitiveString::create(vm, "WebAssembly.Memory"_string)));
// 10. Set this.[[BufferObject]] to resizableBuffer.
m_buffer = resizable_buffer;
// 11. Return resizeableBuffer.
return resizable_buffer;
}
// https://webassembly.github.io/spec/js-api/#refresh-the-memory-buffer
void Memory::refresh_the_memory_buffer(JS::VM& vm, JS::Realm& realm, Wasm::MemoryAddress address)
{
@ -130,15 +215,45 @@ void Memory::refresh_the_memory_buffer(JS::VM& vm, JS::Realm& realm, Wasm::Memor
// 2. Let newBuffer be the result of creating a fixed length memory buffer from memaddr.
// 3. Set memory.[[BufferObject]] to newBuffer.
}
buffer = create_a_fixed_length_memory_buffer(vm, realm, address, memory.value()->m_shared);
} else {
// 1. Let block be a Data Block which is identified with the underlying memory of memaddr.
auto& bytes = cache.abstract_machine().store().get(address)->data();
buffer = create_a_fixed_length_memory_buffer(vm, realm, address, memory.value()->m_shared);
// 2. Set buffer.[[ArrayBufferData]] to block.
// 3. Set buffer.[[ArrayBufferByteLength]] to the length of block.
buffer->set_data_block({ JS::DataBlock::UnownedFixedLengthByteBuffer(&bytes) });
}
}
// https://webassembly.github.io/spec/js-api/#dom-memory-buffer
// https://webassembly.github.io/threads/js-api/#dom-memory-buffer
WebIDL::ExceptionOr<GC::Ref<JS::ArrayBuffer>> Memory::buffer() const
{
return GC::Ref(*m_buffer);
// 1. Let memaddr be this.[[Memory]].
// 2. Let block be a Data Block which is identified with the underlying memory of memaddr.
// 3. If block is a Shared Data Block,
if (m_shared == Shared::Yes) {
// 1. Let map be the surrounding agent's associated Memory object cache.
// 2. Assert: map[memaddr] exists.
// 3. Let newMemory be map[memaddr].
auto& cache = Detail::get_cache(realm());
auto new_memory = cache.get_memory_instance(m_address);
VERIFY(new_memory.has_value());
// 4. Let newBufferObject be newMemory.[[BufferObject]].
auto new_buffer_object = new_memory.value()->m_buffer;
// 5. Set this.[[BufferObject]] to newBufferObject.
m_buffer = new_buffer_object;
// 6. Return newBufferObject.
return GC::Ref(*new_buffer_object);
}
// 4. Otherwise,
else {
// 1. Return this.[[BufferObject]].
return GC::Ref(*m_buffer);
}
}
// https://webassembly.github.io/spec/js-api/#create-a-fixed-length-memory-buffer
@ -173,4 +288,62 @@ GC::Ref<JS::ArrayBuffer> Memory::create_a_fixed_length_memory_buffer(JS::VM& vm,
return GC::Ref(*array_buffer);
}
// https://webassembly.github.io/spec/js-api/#create-a-resizable-memory-buffer
JS::ThrowCompletionOr<GC::Ref<JS::ArrayBuffer>> Memory::create_a_resizable_memory_buffer(JS::VM& vm, JS::Realm& realm, Wasm::MemoryAddress address, Shared shared, size_t max_size)
{
auto& context = Detail::get_cache(realm);
auto* memory = context.abstract_machine().store().get(address);
VERIFY(memory);
// 3. If maxsize > (65536 × 65536),
if (max_size > (65536 * Wasm::Constants::page_size)) {
// 1. Throw a RangeError exception.
return vm.throw_completion<JS::RangeError>("Maximum memory length exceeds 65536 * 65536 bytes"sv);
}
// https://webassembly.github.io/threads/js-api/index.html#create-a-resizable-memory-buffer
// 5. If share is shared,
if (shared == Shared::Yes) {
// 1. Let block be a Shared Data Block which is identified with the underlying memory of memaddr.
// 2. Let buffer be a new SharedArrayBuffer with the internal slots [[ArrayBufferData]], [[ArrayBufferByteLength]], and [[ArrayBufferMaxByteLength]].
// 3. Set buffer.[[ArrayBufferData]] to block.
auto buffer = JS::ArrayBuffer::create(realm, &memory->data());
// AD-HOC: The threads proposal uses the memory type's minimum for both shared and
// non-shared memories, but the upstream spec uses the memory instance's current
// size. We assume the upstream spec is correct for both cases.
// 4. Set buffer.[[ArrayBufferByteLength]] to min.
VERIFY(buffer->byte_length() == memory->size());
// 5. Set buffer.[[ArrayBufferMaxByteLength]] to maxsize.
buffer->set_max_byte_length(max_size);
// 6. Perform ! SetIntegrityLevel(buffer, "frozen").
MUST(buffer->set_integrity_level(IntegrityLevel::Frozen));
// 7. Return buffer.
return buffer;
}
// 6. Otherwise,
else {
// 1. Let block be a Data Block which is identified with the underlying memory of memaddr.
// 4. Let buffer be a new ArrayBuffer with the internal slots [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferMaxByteLength]], and [[ArrayBufferDetachKey]].
// 5. Set buffer.[[ArrayBufferData]] to block.
auto buffer = JS::ArrayBuffer::create(realm, &memory->data());
// 2. Let length be the length of block.
// 6. Set buffer.[[ArrayBufferByteLength]] to length.
VERIFY(buffer->byte_length() == memory->size());
// 7. Set buffer.[[ArrayBufferMaxByteLength]] to maxsize.
buffer->set_max_byte_length(max_size);
// 8. Set buffer.[[ArrayBufferDetachKey]] to "WebAssembly.Memory".
buffer->set_detach_key(JS::PrimitiveString::create(vm, "WebAssembly.Memory"_string));
// 9. Return buffer.
return buffer;
}
}
}

View file

@ -35,10 +35,14 @@ class Memory : public Bindings::PlatformObject {
public:
static WebIDL::ExceptionOr<GC::Ref<Memory>> construct_impl(JS::Realm&, MemoryDescriptor& descriptor);
WebIDL::ExceptionOr<u32> grow(u32 delta);
JS::ThrowCompletionOr<u32> grow(u32 delta);
WebIDL::ExceptionOr<GC::Ref<JS::ArrayBuffer>> to_fixed_length_buffer();
WebIDL::ExceptionOr<GC::Ref<JS::ArrayBuffer>> to_resizable_buffer();
WebIDL::ExceptionOr<GC::Ref<JS::ArrayBuffer>> buffer() const;
Wasm::MemoryAddress address() const { return m_address; }
GC::Ptr<JS::ArrayBuffer> buffer_object() const { return m_buffer; }
private:
Memory(JS::Realm&, Wasm::MemoryAddress, Shared shared);
@ -48,6 +52,7 @@ private:
static void refresh_the_memory_buffer(JS::VM&, JS::Realm&, Wasm::MemoryAddress);
static GC::Ref<JS::ArrayBuffer> create_a_fixed_length_memory_buffer(JS::VM&, JS::Realm&, Wasm::MemoryAddress, Shared shared);
static JS::ThrowCompletionOr<GC::Ref<JS::ArrayBuffer>> create_a_resizable_memory_buffer(JS::VM&, JS::Realm&, Wasm::MemoryAddress, Shared shared, size_t max_size);
Wasm::MemoryAddress m_address;
Shared m_shared { Shared::No };

View file

@ -12,5 +12,7 @@ interface Memory {
unsigned long grow([EnforceRange] unsigned long delta);
ArrayBuffer toFixedLengthBuffer();
ArrayBuffer toResizableBuffer();
readonly attribute ArrayBuffer buffer;
};

View file

@ -438,6 +438,56 @@ JS::ThrowCompletionOr<NonnullRefPtr<CompiledWebAssemblyModule>> compile_a_webass
return compiled_module;
}
JS::ThrowCompletionOr<JS::HandledByHost> host_resize_array_buffer(JS::VM& vm, JS::ArrayBuffer& buffer, size_t new_length)
{
// 1. If buffer.[[ArrayBufferDetachKey]] is "WebAssembly.Memory",
auto detach_key = buffer.detach_key();
if (detach_key.is_string() && detach_key.as_string() == JS::PrimitiveString::create(vm, "WebAssembly.Memory"_string)) {
// 1. Let map be the surrounding agent's associated Memory object cache.
auto const& map = get_cache(*vm.current_realm()).memory_instances();
// 3. For each memaddr → mem in map,
bool seen = false;
for (auto [address, memory] : map) {
auto buffer_object = memory->buffer_object();
// 1. If SameValue(mem.[[BufferObject]], buffer) is true,
if (buffer_object.ptr() == &buffer) {
// 2. Assert: buffer is the [[BufferObject]] of exactly one value in map.
VERIFY(!seen);
seen = true;
// 1. Assert: buffer.[[ArrayBufferByteLength]] modulo 65536 is 0.
VERIFY(buffer.byte_length() % Wasm::Constants::page_size == 0);
// 2. Let lengthDelta be newLength - buffer.[[ArrayBufferByteLength]].
auto length_delta = new_length - buffer.byte_length();
// 3. If lengthDelta < 0 or lengthDelta modulo 65536 is not 0,
if (new_length < buffer.byte_length() || length_delta % Wasm::Constants::page_size != 0) {
// 1. Throw a RangeError exception.
return vm.throw_completion<JS::RangeError>("WebAssembly.Memory buffers must be resized by a multiple of the page size"sv);
}
// 4. Let delta be lengthDelta ÷ 65536.
auto delta = length_delta / Wasm::Constants::page_size;
// 5. Grow the memory buffer associated with memaddr by delta.
// FIXME: "Grow the memory buffer" is a separate algorithm from the Memory#grow() method.
TRY(memory->grow(delta));
}
}
// 2. Assert: buffer is the [[BufferObject]] of exactly one value in map.
VERIFY(seen);
// 4. Return handled.
return JS::HandledByHost::Handled;
}
// 2. Otherwise, return unhandled.
return JS::HandledByHost::Unhandled;
}
GC_DEFINE_ALLOCATOR(ExportedWasmFunction);
GC::Ref<ExportedWasmFunction> ExportedWasmFunction::create(JS::Realm& realm, Utf16FlyString name, Function<JS::ThrowCompletionOr<JS::Value>(JS::VM&)> behavior, Wasm::FunctionAddress exported_address)

View file

@ -107,6 +107,7 @@ JS::ThrowCompletionOr<Wasm::Value> to_webassembly_value(JS::VM&, JS::Value value
Wasm::Value default_webassembly_value(JS::VM&, Wasm::ValueType type);
JS::Value to_js_value(JS::VM&, Wasm::Value& wasm_value, Wasm::ValueType type);
JS::ThrowCompletionOr<void> host_ensure_can_compile_wasm_bytes(JS::VM&);
JS::ThrowCompletionOr<JS::HandledByHost> host_resize_array_buffer(JS::VM&, JS::ArrayBuffer&, size_t);
extern HashMap<GC::Ptr<JS::Object>, WebAssemblyCache> s_caches;