mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-31 13:19:05 +00:00
LibWeb: Refactor WebAssembly API to use spec-defined promise AOs
This commit is contained in:
parent
3504370281
commit
ba6dcd7521
Notes:
github-actions[bot]
2024-11-01 17:43:26 +00:00
Author: https://github.com/ADKaster
Commit: ba6dcd7521
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1982
6 changed files with 278 additions and 142 deletions
|
@ -19,17 +19,23 @@
|
|||
#include <LibJS/Runtime/TypedArray.h>
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
#include <LibWasm/AbstractMachine/Validator.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||||
#include <LibWeb/WebAssembly/Instance.h>
|
||||
#include <LibWeb/WebAssembly/Memory.h>
|
||||
#include <LibWeb/WebAssembly/Module.h>
|
||||
#include <LibWeb/WebAssembly/Table.h>
|
||||
#include <LibWeb/WebAssembly/WebAssembly.h>
|
||||
#include <LibWeb/WebIDL/AbstractOperations.h>
|
||||
#include <LibWeb/WebIDL/Buffers.h>
|
||||
#include <LibWeb/WebIDL/Promise.h>
|
||||
|
||||
namespace Web::WebAssembly {
|
||||
|
||||
static JS::NonnullGCPtr<WebIDL::Promise> asynchronously_compile_webassembly_module(JS::VM&, ByteBuffer, HTML::Task::Source = HTML::Task::Source::Unspecified);
|
||||
static JS::NonnullGCPtr<WebIDL::Promise> instantiate_promise_of_module(JS::VM&, JS::NonnullGCPtr<WebIDL::Promise>, JS::GCPtr<JS::Object> import_object);
|
||||
static JS::NonnullGCPtr<WebIDL::Promise> asynchronously_instantiate_webassembly_module(JS::VM&, JS::NonnullGCPtr<Module>, JS::GCPtr<JS::Object> import_object);
|
||||
|
||||
namespace Detail {
|
||||
|
||||
HashMap<JS::GCPtr<JS::Object>, WebAssemblyCache> s_caches;
|
||||
|
@ -62,21 +68,19 @@ void finalize(JS::Object& object)
|
|||
bool validate(JS::VM& vm, JS::Handle<WebIDL::BufferSource>& bytes)
|
||||
{
|
||||
// 1. Let stableBytes be a copy of the bytes held by the buffer bytes.
|
||||
// Note: There's no need to copy the bytes here as the buffer data cannot change while we're compiling the module.
|
||||
auto stable_bytes = WebIDL::get_buffer_source_copy(*bytes->raw_object());
|
||||
if (stable_bytes.is_error()) {
|
||||
VERIFY(stable_bytes.error().code() == ENOMEM);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Compile stableBytes as a WebAssembly module and store the results as module.
|
||||
auto module_or_error = Detail::parse_module(vm, bytes->raw_object());
|
||||
auto module_or_error = Detail::compile_a_webassembly_module(vm, stable_bytes.release_value());
|
||||
|
||||
// 3. If module is error, return false.
|
||||
if (module_or_error.is_error())
|
||||
return false;
|
||||
|
||||
// 3 continued - our "compile" step is lazy with validation, explicitly do the validation.
|
||||
auto compiled_module = module_or_error.release_value();
|
||||
auto& cache = Detail::get_cache(*vm.current_realm());
|
||||
if (cache.abstract_machine().validate(compiled_module->module).is_error())
|
||||
return false;
|
||||
|
||||
// 4. Return true.
|
||||
return true;
|
||||
}
|
||||
|
@ -86,87 +90,54 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> compile(JS::VM& vm, JS::H
|
|||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
// FIXME: This shouldn't block!
|
||||
auto compiled_module_or_error = Detail::parse_module(vm, bytes->raw_object());
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
if (compiled_module_or_error.is_error()) {
|
||||
WebIDL::reject_promise(realm, promise, compiled_module_or_error.error_value());
|
||||
} else {
|
||||
auto module_object = vm.heap().allocate<Module>(realm, realm, compiled_module_or_error.release_value());
|
||||
WebIDL::resolve_promise(realm, promise, module_object);
|
||||
// 1. Let stableBytes be a copy of the bytes held by the buffer bytes.
|
||||
auto stable_bytes = WebIDL::get_buffer_source_copy(*bytes->raw_object());
|
||||
if (stable_bytes.is_error()) {
|
||||
VERIFY(stable_bytes.error().code() == ENOMEM);
|
||||
return WebIDL::create_rejected_promise_from_exception(realm, vm.throw_completion<JS::InternalError>(vm.error_message(JS::VM::ErrorMessage::OutOfMemory)));
|
||||
}
|
||||
|
||||
return promise;
|
||||
// 2. Asynchronously compile a WebAssembly module from stableBytes and return the result.
|
||||
return asynchronously_compile_webassembly_module(vm, stable_bytes.release_value());
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/spec/js-api/#dom-webassembly-instantiate
|
||||
WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate(JS::VM& vm, JS::Handle<WebIDL::BufferSource>& bytes, Optional<JS::Handle<JS::Object>>& import_object)
|
||||
WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate(JS::VM& vm, JS::Handle<WebIDL::BufferSource>& bytes, Optional<JS::Handle<JS::Object>>& import_object_handle)
|
||||
{
|
||||
// FIXME: Implement the importObject parameter.
|
||||
(void)import_object;
|
||||
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
// FIXME: This shouldn't block!
|
||||
auto compiled_module_or_error = Detail::parse_module(vm, bytes->raw_object());
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
|
||||
if (compiled_module_or_error.is_error()) {
|
||||
WebIDL::reject_promise(realm, promise, compiled_module_or_error.error_value());
|
||||
return promise;
|
||||
// 1. Let stableBytes be a copy of the bytes held by the buffer bytes.
|
||||
auto stable_bytes = WebIDL::get_buffer_source_copy(*bytes->raw_object());
|
||||
if (stable_bytes.is_error()) {
|
||||
VERIFY(stable_bytes.error().code() == ENOMEM);
|
||||
return WebIDL::create_rejected_promise_from_exception(realm, vm.throw_completion<JS::InternalError>(vm.error_message(JS::VM::ErrorMessage::OutOfMemory)));
|
||||
}
|
||||
|
||||
auto compiled_module = compiled_module_or_error.release_value();
|
||||
auto result = Detail::instantiate_module(vm, compiled_module->module);
|
||||
// 2. Asynchronously compile a WebAssembly module from stableBytes and let promiseOfModule be the result.
|
||||
auto promise_of_module = asynchronously_compile_webassembly_module(vm, stable_bytes.release_value());
|
||||
|
||||
if (result.is_error()) {
|
||||
WebIDL::reject_promise(realm, promise, result.error_value());
|
||||
} else {
|
||||
auto module_object = vm.heap().allocate<Module>(realm, realm, move(compiled_module));
|
||||
auto instance_object = vm.heap().allocate<Instance>(realm, realm, result.release_value());
|
||||
|
||||
auto object = JS::Object::create(realm, nullptr);
|
||||
object->define_direct_property("module", module_object, JS::default_attributes);
|
||||
object->define_direct_property("instance", instance_object, JS::default_attributes);
|
||||
WebIDL::resolve_promise(realm, promise, object);
|
||||
}
|
||||
|
||||
return promise;
|
||||
// 3. Instantiate promiseOfModule with imports importObject and return the result.
|
||||
JS::GCPtr<JS::Object> const import_object = import_object_handle.has_value() ? import_object_handle.value().ptr() : nullptr;
|
||||
return instantiate_promise_of_module(vm, promise_of_module, import_object);
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/spec/js-api/#dom-webassembly-instantiate-moduleobject-importobject
|
||||
WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate(JS::VM& vm, Module const& module_object, Optional<JS::Handle<JS::Object>>& import_object)
|
||||
{
|
||||
// FIXME: Implement the importObject parameter.
|
||||
(void)import_object;
|
||||
|
||||
auto& realm = *vm.current_realm();
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
|
||||
// FIXME: This shouldn't block!
|
||||
auto const& compiled_module = module_object.compiled_module();
|
||||
auto result = Detail::instantiate_module(vm, compiled_module->module);
|
||||
|
||||
if (result.is_error()) {
|
||||
WebIDL::reject_promise(realm, promise, result.error_value());
|
||||
} else {
|
||||
auto instance_object = vm.heap().allocate<Instance>(realm, realm, result.release_value());
|
||||
WebIDL::resolve_promise(realm, promise, instance_object);
|
||||
}
|
||||
|
||||
return promise;
|
||||
// 1. Asynchronously instantiate the WebAssembly module moduleObject importing importObject, and return the result.
|
||||
JS::NonnullGCPtr<Module> module { const_cast<Module&>(module_object) };
|
||||
JS::GCPtr<JS::Object> const imports = import_object.has_value() ? import_object.value().ptr() : nullptr;
|
||||
return asynchronously_instantiate_webassembly_module(vm, module, imports);
|
||||
}
|
||||
|
||||
namespace Detail {
|
||||
|
||||
JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module)
|
||||
JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module, JS::GCPtr<JS::Object> import_object)
|
||||
{
|
||||
Wasm::Linker linker { module };
|
||||
HashMap<Wasm::Linker::Name, Wasm::ExternValue> resolved_imports;
|
||||
auto import_argument = vm.argument(1);
|
||||
auto& cache = get_cache(*vm.current_realm());
|
||||
if (!import_argument.is_undefined()) {
|
||||
auto import_object = TRY(import_argument.to_object(vm));
|
||||
if (import_object) {
|
||||
dbgln_if(LIBWEB_WASM_DEBUG, "Trying to resolve stuff because import object was specified");
|
||||
for (Wasm::Linker::Name const& import_name : linker.unresolved_imports()) {
|
||||
dbgln_if(LIBWEB_WASM_DEBUG, "Trying to resolve {}::{}", import_name.module, import_name.name);
|
||||
|
@ -309,32 +280,10 @@ JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS
|
|||
return instance_result.release_value();
|
||||
}
|
||||
|
||||
JS::ThrowCompletionOr<NonnullRefPtr<CompiledWebAssemblyModule>> parse_module(JS::VM& vm, JS::Object* buffer_object)
|
||||
// // https://webassembly.github.io/spec/js-api/#compile-a-webassembly-module
|
||||
JS::ThrowCompletionOr<NonnullRefPtr<CompiledWebAssemblyModule>> compile_a_webassembly_module(JS::VM& vm, ByteBuffer data)
|
||||
{
|
||||
ReadonlyBytes data;
|
||||
if (is<JS::ArrayBuffer>(buffer_object)) {
|
||||
auto& buffer = static_cast<JS::ArrayBuffer&>(*buffer_object);
|
||||
data = buffer.buffer();
|
||||
} else if (is<JS::TypedArrayBase>(buffer_object)) {
|
||||
auto& buffer = static_cast<JS::TypedArrayBase&>(*buffer_object);
|
||||
|
||||
auto typed_array_record = JS::make_typed_array_with_buffer_witness_record(buffer, JS::ArrayBuffer::Order::SeqCst);
|
||||
if (JS::is_typed_array_out_of_bounds(typed_array_record))
|
||||
return vm.throw_completion<JS::TypeError>(JS::ErrorType::BufferOutOfBounds, "TypedArray"sv);
|
||||
|
||||
data = buffer.viewed_array_buffer()->buffer().span().slice(buffer.byte_offset(), JS::typed_array_byte_length(typed_array_record));
|
||||
} else if (is<JS::DataView>(buffer_object)) {
|
||||
auto& buffer = static_cast<JS::DataView&>(*buffer_object);
|
||||
|
||||
auto view_record = JS::make_data_view_with_buffer_witness_record(buffer, JS::ArrayBuffer::Order::SeqCst);
|
||||
if (JS::is_view_out_of_bounds(view_record))
|
||||
return vm.throw_completion<JS::TypeError>(JS::ErrorType::BufferOutOfBounds, "DataView"sv);
|
||||
|
||||
data = buffer.viewed_array_buffer()->buffer().span().slice(buffer.byte_offset(), JS::get_view_byte_length(view_record));
|
||||
} else {
|
||||
return vm.throw_completion<JS::TypeError>("Not a BufferSource"sv);
|
||||
}
|
||||
FixedMemoryStream stream { data };
|
||||
FixedMemoryStream stream { data.bytes() };
|
||||
auto module_result = Wasm::Module::parse(stream);
|
||||
if (module_result.is_error()) {
|
||||
// FIXME: Throw CompileError instead.
|
||||
|
@ -525,4 +474,154 @@ JS::Value to_js_value(JS::VM& vm, Wasm::Value& wasm_value, Wasm::ValueType type)
|
|||
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/spec/js-api/#asynchronously-compile-a-webassembly-module
|
||||
JS::NonnullGCPtr<WebIDL::Promise> asynchronously_compile_webassembly_module(JS::VM& vm, ByteBuffer bytes, HTML::Task::Source task_source)
|
||||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
// 1. Let promise be a new Promise.
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
|
||||
// 2. Run the following steps in parallel:
|
||||
Platform::EventLoopPlugin::the().deferred_invoke([&vm, bytes = move(bytes), promise, task_source]() mutable {
|
||||
HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*promise->promise()), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
|
||||
// 1. Compile the WebAssembly module bytes and store the result as module.
|
||||
auto module_or_error = Detail::compile_a_webassembly_module(vm, move(bytes));
|
||||
|
||||
// 2. Queue a task to perform the following steps. If taskSource was provided, queue the task on that task source.
|
||||
HTML::queue_a_task(task_source, nullptr, nullptr, JS::create_heap_function(vm.heap(), [&vm, promise, module_or_error = move(module_or_error)]() mutable {
|
||||
HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*promise->promise()), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
|
||||
auto& realm = HTML::relevant_realm(*promise->promise());
|
||||
|
||||
// 1. If module is error, reject promise with a CompileError exception.
|
||||
if (module_or_error.is_error()) {
|
||||
WebIDL::reject_promise(realm, promise, module_or_error.error_value());
|
||||
}
|
||||
|
||||
// 2. Otherwise,
|
||||
else {
|
||||
// 1. Construct a WebAssembly module object from module and bytes, and let moduleObject be the result.
|
||||
// FIXME: Save bytes to the Module instance instead of moving into compile_a_webassembly_module
|
||||
auto module_object = vm.heap().allocate<Module>(realm, realm, module_or_error.release_value());
|
||||
|
||||
// 2. Resolve promise with moduleObject.
|
||||
WebIDL::resolve_promise(*vm.current_realm(), promise, module_object);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
// 3. Return promise.
|
||||
return promise;
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/spec/js-api/#asynchronously-instantiate-a-webassembly-module
|
||||
JS::NonnullGCPtr<WebIDL::Promise> asynchronously_instantiate_webassembly_module(JS::VM& vm, JS::NonnullGCPtr<Module> module_object, JS::GCPtr<JS::Object> import_object)
|
||||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
// 1. Let promise be a new promise.
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
|
||||
// 2. Let module be moduleObject.[[Module]].
|
||||
auto module = module_object->compiled_module();
|
||||
|
||||
// 3. Read the imports of module with imports importObject, and let imports be the result.
|
||||
// If this operation throws an exception, catch it, reject promise with the exception, and return promise.
|
||||
// Note: We do this at the same time as instantiation in instantiate_module.
|
||||
|
||||
// 4. Run the following steps in parallel:
|
||||
// 1. Queue a task to perform the following steps: Note: Implementation-specific work may be performed here.
|
||||
HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, JS::create_heap_function(vm.heap(), [&vm, promise, module, import_object]() {
|
||||
HTML::TemporaryExecutionContext context(HTML::relevant_settings_object(*promise->promise()), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
|
||||
auto& realm = HTML::relevant_realm(*promise->promise());
|
||||
|
||||
// 1. Instantiate the core of a WebAssembly module module with imports, and let instance be the result.
|
||||
// If this throws an exception, catch it, reject promise with the exception, and terminate these substeps.
|
||||
auto result = Detail::instantiate_module(vm, module->module, import_object);
|
||||
if (result.is_error()) {
|
||||
WebIDL::reject_promise(realm, promise, result.error_value());
|
||||
return;
|
||||
}
|
||||
auto instance = result.release_value();
|
||||
|
||||
// 2. Let instanceObject be a new Instance.
|
||||
// 3. Initialize instanceObject from module and instance. If this throws an exception, catch it, reject promise with the exception, and terminate these substeps.
|
||||
// FIXME: Investigate whether we are doing all the proper steps for "initialize an instance object"
|
||||
auto instance_object = vm.heap().allocate<Instance>(realm, realm, move(instance));
|
||||
|
||||
// 4. Resolve promise with instanceObject.
|
||||
WebIDL::resolve_promise(realm, promise, instance_object);
|
||||
}));
|
||||
|
||||
// 5. Return promise.
|
||||
return promise;
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/spec/js-api/#instantiate-a-promise-of-a-module
|
||||
JS::NonnullGCPtr<WebIDL::Promise> instantiate_promise_of_module(JS::VM& vm, JS::NonnullGCPtr<WebIDL::Promise> promise_of_module, JS::GCPtr<JS::Object> import_object)
|
||||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
// 1. Let promise be a new Promise.
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
|
||||
// FIXME: Spec should use react to promise here instead of separate upon fulfillment and upon rejection steps
|
||||
|
||||
// 2. Upon fulfillment of promiseOfModule with value module:
|
||||
auto fulfillment_steps = JS::create_heap_function(vm.heap(), [&vm, promise, import_object](JS::Value module_value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
VERIFY(module_value.is_object() && is<Module>(module_value.as_object()));
|
||||
auto module = JS::NonnullGCPtr { static_cast<Module&>(module_value.as_object()) };
|
||||
|
||||
// 1. Instantiate the WebAssembly module module importing importObject, and let innerPromise be the result.
|
||||
auto inner_promise = asynchronously_instantiate_webassembly_module(vm, module, import_object);
|
||||
|
||||
// 2. Upon fulfillment of innerPromise with value instance.
|
||||
auto instantiate_fulfillment_steps = JS::create_heap_function(vm.heap(), [promise, module](JS::Value instance_value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
auto& realm = HTML::relevant_realm(*promise->promise());
|
||||
|
||||
VERIFY(instance_value.is_object() && is<Instance>(instance_value.as_object()));
|
||||
auto instance = JS::NonnullGCPtr { static_cast<Instance&>(instance_value.as_object()) };
|
||||
|
||||
// 1. Let result be the WebAssemblyInstantiatedSource value «[ "module" → module, "instance" → instance ]».
|
||||
auto result = JS::Object::create(realm, nullptr);
|
||||
result->define_direct_property("module", module, JS::default_attributes);
|
||||
result->define_direct_property("instance", instance, JS::default_attributes);
|
||||
|
||||
// 2. Resolve promise with result.
|
||||
WebIDL::resolve_promise(realm, promise, result);
|
||||
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
// 3. Upon rejection of innerPromise with reason reason.
|
||||
auto instantiate_rejection_steps = JS::create_heap_function(vm.heap(), [promise](JS::Value reason) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
auto& realm = HTML::relevant_realm(*promise->promise());
|
||||
|
||||
// 1. Reject promise with reason.
|
||||
WebIDL::reject_promise(realm, promise, reason);
|
||||
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
WebIDL::react_to_promise(inner_promise, instantiate_fulfillment_steps, instantiate_rejection_steps);
|
||||
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
// 3. Upon rejection of promiseOfModule with reason reason:
|
||||
auto rejection_steps = JS::create_heap_function(vm.heap(), [promise](JS::Value reason) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
auto& realm = HTML::relevant_realm(*promise->promise());
|
||||
|
||||
// 1. Reject promise with reason.
|
||||
WebIDL::reject_promise(realm, promise, reason);
|
||||
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
WebIDL::react_to_promise(promise_of_module, fulfillment_steps, rejection_steps);
|
||||
|
||||
// 4. Return promise.
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue