LibWeb: Support WASM modules
This adds support for importing WASM modules in JavaScript and vice versa.
This commit is contained in:
parent
7392d2a2f4
commit
e5dab9e1c7
30 changed files with 759 additions and 77 deletions
|
|
@ -324,7 +324,7 @@ ThrowCompletionOr<u32> CyclicModule::inner_module_linking(VM& vm, Vector<Module*
|
|||
}
|
||||
|
||||
// 16.2.1.5.3 Evaluate ( ), https://tc39.es/ecma262/#sec-moduleevaluation
|
||||
ThrowCompletionOr<GC::Ref<Promise>> CyclicModule::evaluate(VM& vm)
|
||||
ThrowCompletionOr<GC::Ref<PromiseCapability>> CyclicModule::evaluate(VM& vm)
|
||||
{
|
||||
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] evaluate[{}](vm)", this);
|
||||
// 1. Assert: This call to Evaluate is not happening at the same time as another call to Evaluate within the surrounding agent.
|
||||
|
|
@ -351,7 +351,7 @@ ThrowCompletionOr<GC::Ref<Promise>> CyclicModule::evaluate(VM& vm)
|
|||
// 4. If module.[[TopLevelCapability]] is not empty, then
|
||||
if (m_top_level_capability != nullptr) {
|
||||
// a. Return module.[[TopLevelCapability]].[[Promise]].
|
||||
return as<Promise>(*m_top_level_capability->promise());
|
||||
return GC::Ref<PromiseCapability>(*m_top_level_capability);
|
||||
}
|
||||
|
||||
// 5. Let stack be a new empty List.
|
||||
|
|
@ -417,7 +417,8 @@ ThrowCompletionOr<GC::Ref<Promise>> CyclicModule::evaluate(VM& vm)
|
|||
}
|
||||
|
||||
// 11. Return capability.[[Promise]].
|
||||
return as<Promise>(*m_top_level_capability->promise());
|
||||
// AD-HOC: Return the promise capability and let the caller unwrap the promise
|
||||
return GC::Ref<PromiseCapability>(*m_top_level_capability);
|
||||
}
|
||||
|
||||
// 16.2.1.5.2.1 InnerModuleEvaluation ( module, stack, index ), https://tc39.es/ecma262/#sec-innermoduleevaluation
|
||||
|
|
@ -909,7 +910,7 @@ void continue_dynamic_import(GC::Ref<PromiseCapability> promise_capability, Thro
|
|||
auto on_fulfilled = NativeFunction::create(*vm.current_realm(), move(fulfilled_closure), 0);
|
||||
|
||||
// f. Perform PerformPromiseThen(evaluatePromise, onFulfilled, onRejected).
|
||||
evaluate_promise.value()->perform_then(on_fulfilled, on_rejected, {});
|
||||
static_cast<JS::Promise&>(*evaluate_promise.value()->promise()).perform_then(on_fulfilled, on_rejected, {});
|
||||
|
||||
// g. Return unused.
|
||||
return js_undefined();
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public:
|
|||
// Note: Do not call these methods directly unless you are HostResolveImportedModule.
|
||||
// Badges cannot be used because other hosts must be able to call this (and it is called recursively)
|
||||
virtual ThrowCompletionOr<void> link(VM& vm) override final;
|
||||
virtual ThrowCompletionOr<GC::Ref<Promise>> evaluate(VM& vm) override final;
|
||||
virtual ThrowCompletionOr<GC::Ref<PromiseCapability>> evaluate(VM& vm) override final;
|
||||
|
||||
virtual PromiseCapability& load_requested_modules(GC::Ptr<GraphLoadingState::HostDefined>) override;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <LibJS/Runtime/ModuleNamespaceObject.h>
|
||||
#include <LibJS/Runtime/ModuleRequest.h>
|
||||
#include <LibJS/Runtime/Promise.h>
|
||||
#include <LibJS/Runtime/PromiseCapability.h>
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
|
||||
namespace JS {
|
||||
|
|
@ -45,22 +46,22 @@ ThrowCompletionOr<void> Module::evaluate_module_sync(VM& vm)
|
|||
{
|
||||
// 1. Assert: module is not a Cyclic Module Record.
|
||||
// 2. Let promise be module.Evaluate().
|
||||
auto promise = TRY(evaluate(vm));
|
||||
auto& promise = static_cast<JS::Promise&>(*TRY(evaluate(vm))->promise());
|
||||
|
||||
// 3. Assert: promise.[[PromiseState]] is either FULFILLED or REJECTED.
|
||||
VERIFY(first_is_one_of(promise->state(), Promise::State::Fulfilled, Promise::State::Rejected));
|
||||
VERIFY(first_is_one_of(promise.state(), Promise::State::Fulfilled, Promise::State::Rejected));
|
||||
|
||||
// 4. If promise.[[PromiseState]] is REJECTED, then
|
||||
if (promise->state() == Promise::State::Rejected) {
|
||||
if (promise.state() == Promise::State::Rejected) {
|
||||
// a. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
|
||||
if (!promise->is_handled())
|
||||
if (!promise.is_handled())
|
||||
vm.host_promise_rejection_tracker(promise, Promise::RejectionOperation::Handle);
|
||||
|
||||
// b. Set promise.[[PromiseIsHandled]] to true.
|
||||
promise->set_is_handled();
|
||||
promise.set_is_handled();
|
||||
|
||||
// c. Return ThrowCompletion(promise.[[PromiseResult]]).
|
||||
return throw_completion(promise->result());
|
||||
return throw_completion(promise.result());
|
||||
}
|
||||
|
||||
// 5. Return UNUSED.
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ public:
|
|||
GC::Ref<Object> get_module_namespace(VM& vm);
|
||||
|
||||
virtual ThrowCompletionOr<void> link(VM& vm) = 0;
|
||||
virtual ThrowCompletionOr<GC::Ref<Promise>> evaluate(VM& vm) = 0;
|
||||
virtual ThrowCompletionOr<GC::Ref<PromiseCapability>> evaluate(VM& vm) = 0;
|
||||
|
||||
Vector<Utf16FlyString> get_exported_names(VM& vm);
|
||||
virtual Vector<Utf16FlyString> get_exported_names(VM& vm, HashTable<Module const*>& export_star_set) = 0;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
namespace JS {
|
||||
|
||||
// 9.1.1.5 Module Environment Records, https://tc39.es/ecma262/#sec-module-environment-records
|
||||
class ModuleEnvironment final : public DeclarativeEnvironment {
|
||||
class JS_API ModuleEnvironment final : public DeclarativeEnvironment {
|
||||
JS_ENVIRONMENT(ModuleEnvironment, DeclarativeEnvironment);
|
||||
GC_DECLARE_ALLOCATOR(ModuleEnvironment);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ struct LoadedModuleRequest {
|
|||
};
|
||||
|
||||
// https://tc39.es/ecma262/#modulerequest-record
|
||||
struct ModuleRequest {
|
||||
struct JS_API ModuleRequest {
|
||||
ModuleRequest() = default;
|
||||
|
||||
explicit ModuleRequest(Utf16FlyString specifier)
|
||||
|
|
|
|||
|
|
@ -619,16 +619,16 @@ ThrowCompletionOr<void> VM::link_and_eval_module(CyclicModule& module)
|
|||
if (evaluated_or_error.is_error())
|
||||
return evaluated_or_error.throw_completion();
|
||||
|
||||
auto evaluated_value = evaluated_or_error.value();
|
||||
auto const& evaluated_value = static_cast<Promise&>(*evaluated_or_error.value()->promise());
|
||||
|
||||
run_queued_promise_jobs();
|
||||
VERIFY(m_promise_jobs.is_empty());
|
||||
|
||||
// FIXME: This will break if we start doing promises actually asynchronously.
|
||||
VERIFY(evaluated_value->state() != Promise::State::Pending);
|
||||
VERIFY(evaluated_value.state() != Promise::State::Pending);
|
||||
|
||||
if (evaluated_value->state() == Promise::State::Rejected)
|
||||
return JS::throw_completion(evaluated_value->result());
|
||||
if (evaluated_value.state() == Promise::State::Rejected)
|
||||
return JS::throw_completion(evaluated_value.result());
|
||||
|
||||
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] Evaluating passed for module {}", module.filename());
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ ThrowCompletionOr<void> SyntheticModule::link(VM& vm)
|
|||
}
|
||||
|
||||
// 16.2.1.8.4.5 Evaluate ( ), https://tc39.es/ecma262/#sec-smr-Evaluate
|
||||
ThrowCompletionOr<GC::Ref<Promise>> SyntheticModule::evaluate(VM& vm)
|
||||
ThrowCompletionOr<GC::Ref<PromiseCapability>> SyntheticModule::evaluate(VM& vm)
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
|
||||
|
|
@ -191,7 +191,8 @@ ThrowCompletionOr<GC::Ref<Promise>> SyntheticModule::evaluate(VM& vm)
|
|||
MUST(call(vm, *promise_capability->resolve(), js_undefined(), js_undefined()));
|
||||
|
||||
// 16. Return pc.[[Promise]].
|
||||
return static_cast<Promise&>(*promise_capability->promise());
|
||||
// AD-HOC: Return the promise capability and let the caller unwrap the promise
|
||||
return promise_capability;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public:
|
|||
virtual Vector<Utf16FlyString> get_exported_names(VM& vm, HashTable<Module const*>& export_star_set) override;
|
||||
virtual ResolvedBinding resolve_export(VM& vm, Utf16FlyString const& export_name, Vector<ResolvedBinding> resolve_set) override;
|
||||
virtual ThrowCompletionOr<void> link(VM& vm) override;
|
||||
virtual ThrowCompletionOr<GC::Ref<Promise>> evaluate(VM& vm) override;
|
||||
virtual ThrowCompletionOr<GC::Ref<PromiseCapability>> evaluate(VM& vm) override;
|
||||
|
||||
private:
|
||||
SyntheticModule(Realm& realm, Vector<Utf16FlyString> export_names, EvaluationFunction evaluation_steps, ByteString filename);
|
||||
|
|
|
|||
|
|
@ -1065,6 +1065,7 @@ set(SOURCES
|
|||
WebAssembly/Module.cpp
|
||||
WebAssembly/Table.cpp
|
||||
WebAssembly/WebAssembly.cpp
|
||||
WebAssembly/WebAssemblyModule.cpp
|
||||
WebAudio/AnalyserNode.cpp
|
||||
WebAudio/AudioBuffer.cpp
|
||||
WebAudio/AudioBufferSourceNode.cpp
|
||||
|
|
|
|||
|
|
@ -1299,6 +1299,7 @@ class Instance;
|
|||
class Memory;
|
||||
class Module;
|
||||
class Table;
|
||||
class WebAssemblyModule;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -773,12 +773,16 @@ void fetch_single_module_script(JS::Realm& realm,
|
|||
// FIXME: 4. Let referrerPolicy be the result of parsing the `Referrer-Policy` header given response. [REFERRERPOLICY]
|
||||
// FIXME: 5. If referrerPolicy is not the empty string, set options's referrer policy to referrerPolicy.
|
||||
|
||||
// FIXME: 6. If mimeType's essence is "application/wasm" and moduleType is "javascript-or-wasm", then set moduleScript
|
||||
// to the result of creating a WebAssembly module script given bodyBytes, settingsObject, response's URL, and
|
||||
// options.
|
||||
// 6. If mimeType's essence is "application/wasm" and moduleType is "javascript-or-wasm", then set moduleScript
|
||||
// to the result of creating a WebAssembly module script given bodyBytes, moduleMapRealm, response's URL, and
|
||||
// options.
|
||||
// FIXME: Pass options.
|
||||
if (mime_type.has_value() && mime_type->essence() == "application/wasm"sv && module_type == "javascript-or-wasm") {
|
||||
module_script = ModuleScript::create_a_webassembly_module_script(url.to_byte_string(), body_bytes.get<ByteBuffer>(), module_map_realm, response->url().value_or({})).release_value_but_fixme_should_propagate_errors();
|
||||
}
|
||||
|
||||
// 7. Otherwise
|
||||
{
|
||||
else {
|
||||
// 1. Let sourceText be the result of UTF-8 decoding bodyBytes.
|
||||
auto decoder = TextCodec::decoder_for("UTF-8"sv);
|
||||
VERIFY(decoder.has_value());
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
#include <LibWeb/HTML/Scripting/Fetching.h>
|
||||
#include <LibWeb/HTML/Scripting/ModuleScript.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
|
||||
#include <LibWeb/WebIDL/DOMException.h>
|
||||
#include <LibWeb/WebIDL/ExceptionOr.h>
|
||||
#include <LibWeb/WebIDL/QuotaExceededError.h>
|
||||
|
|
@ -148,18 +150,59 @@ WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> ModuleScript::create_a_json_module_sc
|
|||
return script;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-webassembly-module-script
|
||||
// https://whatpr.org/html/9893/webappapis.html#creating-a-webassembly-module-script
|
||||
WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> ModuleScript::create_a_webassembly_module_script(ByteString const& filename, ByteBuffer body_bytes, JS::Realm& realm, URL::URL base_url)
|
||||
{
|
||||
// 1. If scripting is disabled for realm, then set bodyBytes to the byte sequence 0x00 0x61 0x73 0x6d 0x01 0x00 0x00 0x00.
|
||||
// NOTE: This byte sequence corresponds to an empty WebAssembly module with only the magic bytes and version number provided.
|
||||
if (HTML::is_scripting_disabled(realm)) {
|
||||
auto byte_sequence = "\x00\x61\x73\x6d\x01\x00\x00\x00"sv.bytes();
|
||||
body_bytes = MUST(ByteBuffer::create_uninitialized(byte_sequence.size()));
|
||||
byte_sequence.copy_to(body_bytes);
|
||||
}
|
||||
|
||||
// 2. Let script be a new module script that this algorithm will subsequently initialize.
|
||||
// 3. Set script's realm to realm.
|
||||
// 4. Set script's base URL to baseURL.
|
||||
// FIXME: 5. Set script's fetch options to options.
|
||||
auto script = realm.create<ModuleScript>(base_url, filename, realm);
|
||||
|
||||
// 6. Set script's parse error and error to rethrow to null.
|
||||
script->set_parse_error(JS::js_null());
|
||||
script->set_error_to_rethrow(JS::js_null());
|
||||
|
||||
// 7. Let result be the result of parsing a web assembly module given bodyBytes, realm, and script.
|
||||
// NOTE: Passing script as the last parameter here ensures result.[[HostDefined]] will be script.
|
||||
TemporaryExecutionContext execution_context { realm };
|
||||
auto result = WebAssembly::WebAssemblyModule::parse(body_bytes, realm, filename, script);
|
||||
|
||||
// 8. If the previous step threw an error error, then:
|
||||
if (result.is_error()) {
|
||||
// 1. Set script's parse error to error.
|
||||
script->set_parse_error(result.error().value());
|
||||
|
||||
// 2. Return script.
|
||||
return script;
|
||||
}
|
||||
|
||||
// 9. Set script's record to result.
|
||||
script->m_record = result.value();
|
||||
|
||||
// 10. Return script.
|
||||
return script;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#run-a-module-script
|
||||
// https://whatpr.org/html/9893/webappapis.html#run-a-module-script
|
||||
JS::Promise* ModuleScript::run(PreventErrorReporting)
|
||||
WebIDL::Promise* ModuleScript::run(PreventErrorReporting prevent_error_reporting)
|
||||
{
|
||||
// 1. Let realm be the realm of script.
|
||||
auto& realm = this->realm();
|
||||
|
||||
// 2. Check if we can run script with realm. If this returns "do not run", then return a promise resolved with undefined.
|
||||
if (can_run_script(realm) == RunScriptDecision::DoNotRun) {
|
||||
auto promise = JS::Promise::create(realm);
|
||||
promise->fulfill(JS::js_undefined());
|
||||
return promise;
|
||||
return WebIDL::create_resolved_promise(realm, JS::js_undefined());
|
||||
}
|
||||
|
||||
// FIXME: 3. Record module script execution start time given script.
|
||||
|
|
@ -168,12 +211,11 @@ JS::Promise* ModuleScript::run(PreventErrorReporting)
|
|||
prepare_to_run_script(realm);
|
||||
|
||||
// 5. Let evaluationPromise be null.
|
||||
JS::Promise* evaluation_promise = nullptr;
|
||||
GC::Ptr<WebIDL::Promise> evaluation_promise = nullptr;
|
||||
|
||||
// 6. If script's error to rethrow is not null, then set evaluationPromise to a promise rejected with script's error to rethrow.
|
||||
if (!error_to_rethrow().is_null()) {
|
||||
evaluation_promise = JS::Promise::create(realm);
|
||||
evaluation_promise->reject(error_to_rethrow());
|
||||
evaluation_promise = WebIDL::create_rejected_promise(realm, error_to_rethrow());
|
||||
}
|
||||
// 7. Otherwise:
|
||||
else {
|
||||
|
|
@ -198,10 +240,7 @@ JS::Promise* ModuleScript::run(PreventErrorReporting)
|
|||
// If Evaluate fails to complete as a result of the user agent aborting the running script,
|
||||
// then set evaluationPromise to a promise rejected with a new "QuotaExceededError" DOMException.
|
||||
if (elevation_promise_or_error.is_error()) {
|
||||
auto promise = JS::Promise::create(realm);
|
||||
promise->reject(WebIDL::QuotaExceededError::create(realm, "Failed to evaluate module script"_utf16));
|
||||
|
||||
evaluation_promise = promise;
|
||||
evaluation_promise = WebIDL::create_rejected_promise(realm, WebIDL::QuotaExceededError::create(realm, "Failed to evaluate module script"_utf16).ptr());
|
||||
} else {
|
||||
evaluation_promise = elevation_promise_or_error.value();
|
||||
}
|
||||
|
|
@ -211,7 +250,15 @@ JS::Promise* ModuleScript::run(PreventErrorReporting)
|
|||
stack.deallocate(stack_mark);
|
||||
}
|
||||
|
||||
// FIXME: 8. If preventErrorReporting is false, then upon rejection of evaluationPromise with reason, report the exception given by reason for script.
|
||||
// 8. If preventErrorReporting is false, then upon rejection of evaluationPromise with reason, report the exception given by reason for script.
|
||||
if (prevent_error_reporting == PreventErrorReporting::No) {
|
||||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||||
evaluation_promise = WebIDL::upon_rejection(*evaluation_promise, GC::create_function(realm.heap(), [&realm](JS::Value reason) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
auto& window_or_worker = as<WindowOrWorkerGlobalScopeMixin>(realm.global_object());
|
||||
window_or_worker.report_an_exception(reason);
|
||||
return throw_completion(reason);
|
||||
}));
|
||||
}
|
||||
|
||||
// 9. Clean up after running script with realm.
|
||||
clean_up_after_running_script(realm);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <LibJS/SyntheticModule.h>
|
||||
#include <LibWeb/Export.h>
|
||||
#include <LibWeb/HTML/Scripting/Script.h>
|
||||
#include <LibWeb/WebAssembly/WebAssemblyModule.h>
|
||||
|
||||
namespace JS::FFI {
|
||||
|
||||
|
|
@ -19,8 +20,7 @@ struct ParsedProgram;
|
|||
|
||||
namespace Web::HTML {
|
||||
|
||||
// FIXME: Support WebAssembly Module Record
|
||||
using ModuleScriptRecord = Variant<Empty, GC::Ref<JS::SourceTextModule>, GC::Ref<JS::SyntheticModule>>;
|
||||
using ModuleScriptRecord = Variant<Empty, GC::Ref<JS::SourceTextModule>, GC::Ref<JS::SyntheticModule>, GC::Ref<WebAssembly::WebAssemblyModule>>;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#module-script
|
||||
class WEB_API ModuleScript : public Script {
|
||||
|
|
@ -35,13 +35,14 @@ public:
|
|||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_a_javascript_module_script(ByteString const& filename, StringView source, JS::Realm&, URL::URL base_url);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_a_css_module_script(ByteString const& filename, StringView source, JS::Realm&);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_a_json_module_script(ByteString const& filename, StringView source, JS::Realm&);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_a_webassembly_module_script(ByteString const& filename, ByteBuffer body_bytes, JS::Realm&, URL::URL base_url);
|
||||
|
||||
enum class PreventErrorReporting {
|
||||
Yes,
|
||||
No
|
||||
};
|
||||
|
||||
JS::Promise* run(PreventErrorReporting = PreventErrorReporting::No);
|
||||
WebIDL::Promise* run(PreventErrorReporting = PreventErrorReporting::No);
|
||||
|
||||
ModuleScriptRecord record() const { return m_record; }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public:
|
|||
static WebIDL::ExceptionOr<GC::Ref<Instance>> construct_impl(JS::Realm&, Module& module, Optional<GC::Root<JS::Object>>& import_object);
|
||||
|
||||
Object const* exports() const { return m_exports.ptr(); }
|
||||
Wasm::ModuleInstance const* module_instance() const { return m_module_instance.ptr(); }
|
||||
|
||||
private:
|
||||
Instance(JS::Realm&, NonnullOwnPtr<Wasm::ModuleInstance>);
|
||||
|
|
|
|||
|
|
@ -215,6 +215,47 @@ namespace Detail {
|
|||
_temporary_result.release_value(); \
|
||||
})
|
||||
|
||||
Wasm::HostFunction create_host_function(JS::VM& vm, JS::FunctionObject& function, Wasm::FunctionType const& type, ByteString const& name)
|
||||
{
|
||||
return Wasm::HostFunction {
|
||||
[&](auto&, auto arguments) -> Wasm::Result {
|
||||
GC::RootVector<JS::Value> argument_values { vm.heap() };
|
||||
size_t index = 0;
|
||||
for (auto& entry : arguments) {
|
||||
argument_values.append(to_js_value(vm, entry, type.parameters()[index]));
|
||||
++index;
|
||||
}
|
||||
|
||||
auto result = TRY_OR_RETURN_TRAP(JS::call(vm, function, JS::js_undefined(), argument_values.span()));
|
||||
if (type.results().is_empty())
|
||||
return Wasm::Result { Vector<Wasm::Value> {} };
|
||||
|
||||
if (type.results().size() == 1)
|
||||
return Wasm::Result { Vector<Wasm::Value> { TRY_OR_RETURN_TRAP(to_webassembly_value(vm, result, type.results().first())) } };
|
||||
|
||||
auto method = TRY_OR_RETURN_TRAP(result.get_method(vm, vm.names.iterator));
|
||||
if (!method)
|
||||
return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, result.to_string_without_side_effects()));
|
||||
|
||||
auto values = TRY_OR_RETURN_TRAP(JS::iterator_to_list(vm, TRY_OR_RETURN_TRAP(JS::get_iterator_from_method(vm, result, *method))));
|
||||
|
||||
if (values.size() != type.results().size())
|
||||
return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(ByteString::formatted("Invalid number of return values for multi-value wasm return of {} objects", type.results().size())));
|
||||
|
||||
Vector<Wasm::Value> wasm_values;
|
||||
TRY_OR_RETURN_OOM_TRAP(vm, wasm_values.try_ensure_capacity(values.size()));
|
||||
|
||||
size_t i = 0;
|
||||
for (auto& value : values)
|
||||
wasm_values.append(TRY_OR_RETURN_TRAP(to_webassembly_value(vm, value, type.results()[i++])));
|
||||
|
||||
return Wasm::Result { move(wasm_values) };
|
||||
},
|
||||
type,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module, GC::Ptr<JS::Object> import_object)
|
||||
{
|
||||
Wasm::Linker linker { module };
|
||||
|
|
@ -268,43 +309,7 @@ JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS
|
|||
else {
|
||||
// 3.4.3.1. Create a host function from v and functype, and let funcaddr be the result.
|
||||
cache.add_imported_object(function);
|
||||
Wasm::HostFunction host_function {
|
||||
[&](auto&, auto arguments) -> Wasm::Result {
|
||||
GC::RootVector<JS::Value> argument_values { vm.heap() };
|
||||
size_t index = 0;
|
||||
for (auto& entry : arguments) {
|
||||
argument_values.append(to_js_value(vm, entry, function_type.parameters()[index]));
|
||||
++index;
|
||||
}
|
||||
|
||||
auto result = TRY_OR_RETURN_TRAP(JS::call(vm, function, JS::js_undefined(), argument_values.span()));
|
||||
if (function_type.results().is_empty())
|
||||
return Wasm::Result { Vector<Wasm::Value> {} };
|
||||
|
||||
if (function_type.results().size() == 1)
|
||||
return Wasm::Result { Vector<Wasm::Value> { TRY_OR_RETURN_TRAP(to_webassembly_value(vm, result, function_type.results().first())) } };
|
||||
|
||||
auto method = TRY_OR_RETURN_TRAP(result.get_method(vm, vm.names.iterator));
|
||||
if (!method)
|
||||
return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, result));
|
||||
|
||||
auto values = TRY_OR_RETURN_TRAP(JS::iterator_to_list(vm, TRY_OR_RETURN_TRAP(JS::get_iterator_from_method(vm, result, *method))));
|
||||
|
||||
if (values.size() != function_type.results().size())
|
||||
return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(ByteString::formatted("Invalid number of return values for multi-value wasm return of {} objects", function_type.results().size())));
|
||||
|
||||
Vector<Wasm::Value> wasm_values;
|
||||
TRY_OR_RETURN_OOM_TRAP(vm, wasm_values.try_ensure_capacity(values.size()));
|
||||
|
||||
size_t i = 0;
|
||||
for (auto& value : values)
|
||||
wasm_values.append(TRY_OR_RETURN_TRAP(to_webassembly_value(vm, value, function_type.results()[i++])));
|
||||
|
||||
return Wasm::Result { move(wasm_values) };
|
||||
},
|
||||
function_type,
|
||||
ByteString::formatted("func{}", resolved_imports.size()),
|
||||
};
|
||||
auto host_function = create_host_function(vm, function, function_type, ByteString::formatted("func{}", resolved_imports.size()));
|
||||
address = cache.abstract_machine().store().allocate(move(host_function));
|
||||
// FIXME: 3.4.3.2. Let index be the number of external functions in imports. This value index is known as the index of the host function funcaddr.
|
||||
// 'index' doesn't seem to be used anywhere?
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ WEB_API WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> instantiate_streaming(JS::
|
|||
|
||||
namespace Detail {
|
||||
|
||||
Wasm::HostFunction create_host_function(JS::VM& vm, JS::FunctionObject& function, Wasm::FunctionType const& type, ByteString const& name);
|
||||
|
||||
struct CompiledWebAssemblyModule : public RefCounted<CompiledWebAssemblyModule> {
|
||||
explicit CompiledWebAssemblyModule(NonnullRefPtr<Wasm::Module> module)
|
||||
: module(move(module))
|
||||
|
|
|
|||
525
Libraries/LibWeb/WebAssembly/WebAssemblyModule.cpp
Normal file
525
Libraries/LibWeb/WebAssembly/WebAssemblyModule.cpp
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Glenn Skrzypczak <glenn.skrzypczak@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibJS/Runtime/ModuleEnvironment.h>
|
||||
#include <LibJS/Runtime/ModuleRequest.h>
|
||||
#include <LibWasm/AbstractMachine/AbstractMachine.h>
|
||||
#include <LibWasm/AbstractMachine/Validator.h>
|
||||
#include <LibWeb/WebAssembly/Global.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/WebAssembly/WebAssemblyModule.h>
|
||||
|
||||
namespace Web::WebAssembly {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(WebAssemblyModule);
|
||||
|
||||
WebAssemblyModule::WebAssemblyModule(JS::Realm& realm, StringView filename, WebAssembly::Module& module_source,
|
||||
JS::Script::HostDefined* host_defined, Vector<JS::ModuleRequest> requested_modules)
|
||||
: CyclicModule(realm, filename, false, move(requested_modules), host_defined)
|
||||
, m_module_source(module_source)
|
||||
{
|
||||
}
|
||||
|
||||
WebAssemblyModule::~WebAssemblyModule() = default;
|
||||
|
||||
void WebAssemblyModule::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_instance);
|
||||
visitor.visit(m_module_source);
|
||||
visitor.visit(m_module_record);
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module
|
||||
JS::ThrowCompletionOr<GC::Ref<WebAssemblyModule>> WebAssemblyModule::parse(ByteBuffer bytes, JS::Realm& realm, StringView filename, JS::Script::HostDefined* host_defined)
|
||||
{
|
||||
auto& vm = realm.vm();
|
||||
|
||||
// 1. Let stableBytes be a copy of the bytes held by the buffer bytes.
|
||||
auto stable_bytes
|
||||
= MUST(ByteBuffer::create_uninitialized(bytes.size()));
|
||||
bytes.bytes().copy_to(stable_bytes);
|
||||
|
||||
// 2. Compile the WebAssembly module stableBytes and store the result as module.
|
||||
// 3. If module is error, throw a CompileError exception.
|
||||
// NOTE: When integrating with the JS String Builtins proposal, builtinSetNames should be passed in the following
|
||||
// step as « "js-string" » and importedStringModule as null.
|
||||
auto module = TRY(Detail::compile_a_webassembly_module(vm, stable_bytes));
|
||||
|
||||
// 4. Construct a WebAssembly module object from module and bytes, and let module be the result.
|
||||
auto module_object = realm.create<WebAssembly::Module>(realm, module);
|
||||
|
||||
// 5. Let requestedModules be a set.
|
||||
HashTable<ByteString> requested_modules;
|
||||
|
||||
// 6. For each (moduleName, name, type) in module_imports(module.[[Module]]),
|
||||
auto const& imports = module_object->compiled_module()->module->import_section().imports();
|
||||
for (auto const& entry : imports) {
|
||||
// 1. If moduleName starts with the prefix "wasm-js:",
|
||||
if (entry.module().starts_with("wasm-js:"sv)) {
|
||||
// 1. Throw a LinkError exception.
|
||||
return vm.throw_completion<LinkError>("Import with invalid module name"sv);
|
||||
}
|
||||
|
||||
// 2. If name starts with the prefix "wasm:" or "wasm-js:",
|
||||
if (entry.name().starts_with("wasm:"sv) || entry.name().starts_with("wasm-js:"sv)) {
|
||||
// 1. Throw a LinkError exception.
|
||||
return vm.throw_completion<LinkError>("Import with invalid name"sv);
|
||||
}
|
||||
|
||||
// NOTE: The following step only applies when integrating with the JS String Builtins proposal.
|
||||
// FIXME: 3. If Find a builtin with (moduleName, name, type) and builtins module.[[BuiltinSets]] is not null,
|
||||
// then continue.
|
||||
|
||||
// 4. Append moduleName to requestedModules.
|
||||
requested_modules.set(entry.module());
|
||||
}
|
||||
|
||||
// 7. For each (name, type) in module_exports(module.[[Module]])
|
||||
auto const& exports = module_object->compiled_module()->module->export_section().entries();
|
||||
for (auto const& entry : exports) {
|
||||
// 1. If name starts with the prefix "wasm:" or "wasm-js:",
|
||||
if (entry.name().starts_with("wasm:"sv) || entry.name().starts_with("wasm-js:"sv)) {
|
||||
// 1. Throw a LinkError exception.
|
||||
return vm.throw_completion<LinkError>("Export with invalid name"sv);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Let moduleRecord be { [[Instance]]: ~empty~, [[Realm]]: realm, [[Environment]]: ~empty~,
|
||||
// [[Namespace]]: ~empty~, [[ModuleSource]]: module, [[HostDefined]]: hostDefined,
|
||||
// [[Status]]: "new", [[EvaluationError]]: undefined, [[DFSIndex]]: undefined,
|
||||
// [[DFSAncestorIndex]]: undefined, [[RequestedModules]]: requestedModules,
|
||||
// [[LoadedModules]]: « », [[CycleRoot]]: ~empty~, [[HasTLA]]: false,
|
||||
// [[AsyncEvaluation]]: false, [[TopLevelCapability]]: ~empty~ [[AsyncParentModules]]: « »,
|
||||
// [[PendingAsyncDependencies]]: ~empty~, }.
|
||||
AK::Vector<JS::ModuleRequest> module_requests;
|
||||
for (auto const& module_name : requested_modules) {
|
||||
module_requests.append(JS::ModuleRequest { Utf16FlyString::from_utf8(module_name), {} });
|
||||
}
|
||||
auto module_record = realm.create<WebAssemblyModule>(realm, filename, module_object, host_defined, module_requests);
|
||||
|
||||
// 9. Set module.[[ModuleRecord]] to moduleRecord.
|
||||
module_record->m_module_record = module_record;
|
||||
|
||||
// 10. Return moduleRecord.
|
||||
return module_record;
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/esm-integration/js-api/index.html#export-name-list
|
||||
Vector<Utf16FlyString> WebAssemblyModule::export_name_list()
|
||||
{
|
||||
// AD-HOC: Return cached export name list if available
|
||||
if (m_cached_export_name_list.has_value())
|
||||
return m_cached_export_name_list.value();
|
||||
|
||||
// 1. Let module be record’s [[ModuleSource]] internal slot.
|
||||
auto module = m_module_source;
|
||||
|
||||
// 2. Let exports be an empty list.
|
||||
Vector<Utf16FlyString> exports;
|
||||
|
||||
// 3. For each(name, type) in module_exports(module.[[Module]])
|
||||
auto module_exports = module->compiled_module()->module->export_section().entries();
|
||||
for (auto const& entry : module_exports) {
|
||||
// 1. Append name to the end of exports.
|
||||
exports.append(Utf16FlyString::from_utf8(entry.name()));
|
||||
}
|
||||
|
||||
// AD-HOC: Cache exports
|
||||
m_cached_export_name_list = exports;
|
||||
|
||||
// 4. Return exports.
|
||||
return exports;
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/esm-integration/js-api/index.html#get-exported-names
|
||||
Vector<Utf16FlyString> WebAssemblyModule::get_exported_names(JS::VM&, HashTable<Module const*>&)
|
||||
{
|
||||
// 1. Let record be this WebAssembly Module Record.
|
||||
auto* record = this;
|
||||
|
||||
// 2. Return the export name list of record.
|
||||
return record->export_name_list();
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/esm-integration/js-api/index.html#resolve-export
|
||||
JS::ResolvedBinding WebAssemblyModule::resolve_export(JS::VM&, Utf16FlyString const& export_name, Vector<JS::ResolvedBinding>)
|
||||
{
|
||||
// 1. Let record be this WebAssembly Module Record.
|
||||
auto* record = this;
|
||||
|
||||
// 2. If the export name list of record contains exportName, return { [[Module]]: record, [[BindingName]]: exportName }.
|
||||
if (export_name_list().contains_slow(export_name)) {
|
||||
return JS::ResolvedBinding { JS::ResolvedBinding::Type::BindingName, record, export_name };
|
||||
}
|
||||
|
||||
// 3. Otherwise, return null.
|
||||
return JS::ResolvedBinding::null();
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/esm-integration/js-api/index.html#module-declaration-environment-setup
|
||||
JS::ThrowCompletionOr<void> WebAssemblyModule::initialize_environment(JS::VM& vm)
|
||||
{
|
||||
// 1. Let record be this WebAssembly Module Record.
|
||||
auto* record = this;
|
||||
|
||||
// 2. Let env be NewModuleEnvironment(null).
|
||||
auto env = vm.heap().allocate<JS::ModuleEnvironment>(nullptr);
|
||||
|
||||
// 3. Set record.[[Environment]] to env.
|
||||
record->set_environment(env);
|
||||
|
||||
// 4. For each name in the export name list of record,
|
||||
for (auto const& name : export_name_list()) {
|
||||
// 1. Perform !env.CreateImmutableBinding(name, true).
|
||||
MUST(env->create_immutable_binding(vm, name, true));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/esm-integration/js-api/index.html#module-execution
|
||||
JS::ThrowCompletionOr<void> WebAssemblyModule::execute_module(JS::VM& vm, GC::Ptr<JS::PromiseCapability> capability)
|
||||
{
|
||||
auto& cache = Detail::get_cache(*vm.current_realm());
|
||||
|
||||
// 1. Assert: promiseCapability was not provided.
|
||||
VERIFY(!capability);
|
||||
|
||||
// 2. Let record be this WebAssembly Module Record.
|
||||
auto* record = this;
|
||||
|
||||
// 3. Let module be record.[[ModuleSource]].[[Module]].
|
||||
auto module = record->m_module_source->compiled_module();
|
||||
|
||||
// 4. Let imports be « ».
|
||||
Vector<Wasm::ExternValue> imports;
|
||||
|
||||
// 5. For each (importedModuleName, name, importtype) in module_imports(module),
|
||||
for (auto const& entry : module->module->import_section().imports()) {
|
||||
// NOTE: The following step only applies when integrating with the JS String Builtins proposal.
|
||||
// FIXME: 1. If Find a builtin with (importedModuleName, name) and builtins module.[[BuiltinSets]] is not null, then continue.
|
||||
|
||||
// 2. Let importedModule be GetImportedModule(record, importedModuleName).
|
||||
auto imported_module = record->get_imported_module(JS::ModuleRequest { Utf16FlyString::from_utf8(entry.module()) });
|
||||
|
||||
// 3. Let resolution be importedModule.ResolveExport(name).
|
||||
auto resolution = imported_module->resolve_export(vm, Utf16FlyString::from_utf8(entry.name()));
|
||||
|
||||
// 4. Assert: resolution is a ResolvedBinding Record, as validated during environment initialization.
|
||||
VERIFY(resolution.is_valid());
|
||||
|
||||
// 5. Let resolvedModule be resolution.\[[Module]].
|
||||
auto resolved_module = resolution.module;
|
||||
|
||||
// 6. Let resolvedName be resolution.[[BindingName]].
|
||||
auto resolved_name = resolution.export_name;
|
||||
|
||||
// 7. If resolvedModule is a WebAssembly Module Record,
|
||||
if (is<WebAssemblyModule>(*resolved_module)) {
|
||||
auto& resolved_webassembly_module = as<WebAssemblyModule>(*resolved_module);
|
||||
|
||||
// 1. If resolvedModule.[[Instance]] is ~empty~, throw a {LinkError} exception.
|
||||
if (!resolved_webassembly_module.m_instance) {
|
||||
return vm.throw_completion<LinkError>("Module has not been instantiated"sv);
|
||||
}
|
||||
|
||||
// 2. Assert: resolvedModule.[[Instance]] is a WebAssembly Instance object.
|
||||
// 3. Assert: resolvedModule.[[ModuleSource]] is a WebAssembly Module object.
|
||||
// 4. Let module be resolvedModule.[[ModuleSource]].[[Module]].
|
||||
auto module = resolved_webassembly_module.m_module_source->compiled_module();
|
||||
|
||||
// 5. Let externval be instance_export(resolvedModule.[[Instance]], resolvedName).
|
||||
// https://webassembly.github.io/spec/core/appendix/embedding.html#embed-instance-export
|
||||
auto externval = resolved_webassembly_module.m_instance->module_instance()->exports().first_matching([resolved_name](auto const& export_instance) { return export_instance.name() == resolved_name; });
|
||||
|
||||
// 6. Assert: externval is not error.
|
||||
VERIFY(externval.has_value());
|
||||
|
||||
// 7. Assert: module_exports(module) contains an element (resolvedName, type).
|
||||
auto module_export = module->module->export_section().entries().first_matching([resolved_name](auto& element) { return element.name() == resolved_name; });
|
||||
VERIFY(module_export.has_value());
|
||||
|
||||
// 8. Let externtype be the value of type for the element (resolvedName, type) in module_exports(module).
|
||||
auto externtype = module_export->description();
|
||||
|
||||
// 9. If importtype is not an extern subtype of externtype, throw a LinkError exception.
|
||||
// https://webassembly.github.io/spec/core/valid/types.html#match-externtype
|
||||
auto& store = cache.abstract_machine().store();
|
||||
auto invalid = entry.description().visit(
|
||||
[&](Wasm::MemoryType const& mem_type) -> Optional<ByteString> {
|
||||
if (!externtype.has<Wasm::MemoryIndex>())
|
||||
return "Expected memory import"sv;
|
||||
auto other_mem_type = store.get(Wasm::MemoryAddress { externtype.get<Wasm::MemoryIndex>().value() })->type();
|
||||
if (other_mem_type.limits().is_subset_of(mem_type.limits()))
|
||||
return {};
|
||||
return ByteString::formatted("Memory import and extern do not match: {}-{} vs {}-{}", mem_type.limits().min(), mem_type.limits().max(), other_mem_type.limits().min(), other_mem_type.limits().max());
|
||||
},
|
||||
[&](Wasm::TableType const& table_type) -> Optional<ByteString> {
|
||||
if (!externtype.has<Wasm::TableIndex>())
|
||||
return "Expected table import"sv;
|
||||
auto other_table_type = store.get(Wasm::TableAddress { externtype.get<Wasm::TableIndex>().value() })->type();
|
||||
if (table_type.element_type() == other_table_type.element_type()
|
||||
&& other_table_type.limits().is_subset_of(table_type.limits()))
|
||||
return {};
|
||||
|
||||
return ByteString::formatted("Table import and extern do not match: {}-{} vs {}-{}", table_type.limits().min(), table_type.limits().max(), other_table_type.limits().min(), other_table_type.limits().max());
|
||||
},
|
||||
[&](Wasm::GlobalType const& global_type) -> Optional<ByteString> {
|
||||
if (!externtype.has<Wasm::GlobalIndex>())
|
||||
return "Expected global import"sv;
|
||||
auto other_global_type = store.get(Wasm::GlobalAddress { externtype.get<Wasm::GlobalIndex>().value() })->type();
|
||||
if (global_type.type() == other_global_type.type()
|
||||
&& global_type.is_mutable() == other_global_type.is_mutable())
|
||||
return {};
|
||||
return "Global import and extern do not match"sv;
|
||||
},
|
||||
[&](Wasm::FunctionType const& type) -> Optional<ByteString> {
|
||||
if (!externtype.has<Wasm::FunctionIndex>())
|
||||
return "Expected function import"sv;
|
||||
auto other_type = store.get(Wasm::FunctionAddress { externtype.get<Wasm::FunctionIndex>().value() })->visit([&](Wasm::WasmFunction const& wasm_func) { return wasm_func.type(); }, [&](Wasm::HostFunction const& host_func) { return host_func.type(); });
|
||||
if (type.results() != other_type.results())
|
||||
return ByteString::formatted("Function import and extern do not match, results: {} vs {}", type.results(), other_type.results());
|
||||
if (type.parameters() != other_type.parameters())
|
||||
return ByteString::formatted("Function import and extern do not match, parameters: {} vs {}", type.parameters(), other_type.parameters());
|
||||
return {};
|
||||
},
|
||||
[&](Wasm::TagType const& type) -> Optional<ByteString> {
|
||||
if (!externtype.has<Wasm::TagIndex>())
|
||||
return "Expected tag import"sv;
|
||||
auto* other_tag_instance = store.get(Wasm::TagAddress { externtype.get<Wasm::TagIndex>().value() });
|
||||
if (other_tag_instance->flags() != type.flags())
|
||||
return "Tag import and extern do not match"sv;
|
||||
|
||||
auto const& this_type = module->module->type_section().types()[type.type().value()];
|
||||
|
||||
if (other_tag_instance->type().parameters() != this_type.function().parameters())
|
||||
return "Tag import and extern do not match"sv;
|
||||
return {};
|
||||
},
|
||||
[&](Wasm::TypeIndex type_index) -> Optional<ByteString> {
|
||||
if (!externtype.has<Wasm::FunctionIndex>())
|
||||
return "Expected function import"sv;
|
||||
auto other_type = store.get(Wasm::FunctionAddress { externtype.get<Wasm::FunctionIndex>().value() })->visit([&](Wasm::WasmFunction const& wasm_func) { return wasm_func.type(); }, [&](Wasm::HostFunction const& host_func) { return host_func.type(); });
|
||||
auto const& type = module->module->type_section().types()[type_index.value()].function();
|
||||
if (type.results() != other_type.results())
|
||||
return ByteString::formatted("Function import and extern do not match, results: {} vs {}", type.results(), other_type.results());
|
||||
if (type.parameters() != other_type.parameters())
|
||||
return ByteString::formatted("Function import and extern do not match, parameters: {} vs {}", type.parameters(), other_type.parameters());
|
||||
return {};
|
||||
});
|
||||
if (invalid.has_value())
|
||||
return vm.throw_completion<LinkError>(ByteString::formatted("{}::{}: {}", entry.module(), entry.name(), invalid.release_value()));
|
||||
|
||||
// 10. Append externval to imports.
|
||||
imports.append(externval.value().value());
|
||||
}
|
||||
|
||||
// 8. Otherwise,
|
||||
else {
|
||||
// 1. Let env be resolvedModule.[[Environment]].
|
||||
auto env = resolved_module->environment();
|
||||
|
||||
// 2. Let v be ?env.GetBindingValue(resolvedName, true).
|
||||
auto v = TRY(env->get_binding_value(vm, resolved_name, true));
|
||||
|
||||
// 3. If importtype is of the form func functype,
|
||||
// AD-HOC: Resolve type index
|
||||
if (entry.description().has<Wasm::FunctionType>() || entry.description().has<Wasm::TypeIndex>()) {
|
||||
auto functype = entry.description().visit(
|
||||
[](Wasm::FunctionType function_type) { return function_type; },
|
||||
[&module](Wasm::TypeIndex type_index) { return module->module->type_section().types()[type_index.value()].function(); },
|
||||
[](auto) -> Wasm::FunctionType { VERIFY_NOT_REACHED(); });
|
||||
|
||||
// 1. If IsCallable(v) is false, throw a LinkError exception.
|
||||
if (!v.is_function())
|
||||
return vm.throw_completion<LinkError>(JS::ErrorType::NotAFunction, v);
|
||||
auto& function = v.as_function();
|
||||
|
||||
// 2. If v has a [[FunctionAddress]] internal slot, and therefore is an Exported Function,
|
||||
Optional<Wasm::FunctionAddress> funcaddr;
|
||||
if (is<Detail::ExportedWasmFunction>(function)) {
|
||||
// 1. Let funcaddr be the value of v’s [[FunctionAddress]] internal slot.
|
||||
auto& exported_function = static_cast<Detail::ExportedWasmFunction&>(function);
|
||||
funcaddr = exported_function.exported_address();
|
||||
}
|
||||
|
||||
// 3. Otherwise,
|
||||
else {
|
||||
// 1. Create a host function from v and functype, and let funcaddr be the result.
|
||||
cache.add_imported_object(function);
|
||||
auto host_function = Detail::create_host_function(vm, function, functype, ByteString::formatted("func{}", imports.size()));
|
||||
funcaddr = cache.abstract_machine().store().allocate(move(host_function));
|
||||
|
||||
// FIXME: 2. Let index be the number of external functions in imports, defining the index of the host function funcaddr.
|
||||
}
|
||||
|
||||
// 4. Let externfunc be the external value func funcaddr.
|
||||
Wasm::ExternValue externfunc { Wasm::FunctionAddress { *funcaddr } };
|
||||
|
||||
// 5. Append externfunc to imports.
|
||||
imports.append(externfunc);
|
||||
}
|
||||
|
||||
// 4. If importtype is of the form global mut valtype,
|
||||
if (entry.description().has<Wasm::GlobalType>()) {
|
||||
auto valtype = entry.description().get<Wasm::GlobalType>();
|
||||
|
||||
// 1. Let store be the surrounding agent’s associated store.
|
||||
auto& store = cache.abstract_machine().store();
|
||||
|
||||
// 2. If v implements Global,
|
||||
Optional<Wasm::GlobalAddress> globaladdr;
|
||||
if (v.is_object() && is<Global>(v.as_object())) {
|
||||
// 1. Let globaladdr be v.[[Global]].
|
||||
globaladdr = as<Global>(v.as_object()).address();
|
||||
|
||||
// 2. Let targetmut valuetype be global_type(store, globaladdr).
|
||||
auto* valuetype = store.get(*globaladdr);
|
||||
|
||||
// 3. If mut is const and targetmut is var, throw a LinkError exception.
|
||||
if (!valtype.is_mutable() && valuetype->is_mutable()) {
|
||||
return vm.throw_completion<LinkError>("Mutable globals are not supported for immutable imports"sv);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Otherwise,
|
||||
else {
|
||||
// AD-HOC: If valtype is i64 and v is a Number, throw a LinkError exception.
|
||||
if (valtype.type().kind() == Wasm::ValueType::I64 && v.is_number()) {
|
||||
return vm.throw_completion<LinkError>("Import resolution attempted to cast a Number to a BigInteger"sv);
|
||||
}
|
||||
|
||||
// AD-HOC: If valtype is not i64 and v is a BigInt, throw a LinkError exception.
|
||||
if (valtype.type().kind() != Wasm::ValueType::I64 && v.is_bigint()) {
|
||||
return vm.throw_completion<LinkError>("Import resolution attempted to cast a BigInteger to a Number"sv);
|
||||
}
|
||||
|
||||
// 1. If valtype is v128, throw a LinkError exception.
|
||||
if (valtype.type().kind() == Wasm::ValueType::V128) {
|
||||
return vm.throw_completion<LinkError>("V128 is not supported as a global value type"sv);
|
||||
}
|
||||
|
||||
// 2. If mut is var, throw a LinkError exception.
|
||||
if (valtype.is_mutable()) {
|
||||
return vm.throw_completion<LinkError>("Variable global value types are not supported"sv);
|
||||
}
|
||||
|
||||
// 3. Let value be ?ToWebAssemblyValue(v, valtype).
|
||||
auto value = TRY(Detail::to_webassembly_value(vm, v, valtype.type()));
|
||||
|
||||
// 4. Let(store, globaladdr) be global_alloc(store, mut valtype, value).
|
||||
// 5. Set the surrounding agent’s associated store to store.
|
||||
globaladdr = cache.abstract_machine().store().allocate(valtype, value);
|
||||
}
|
||||
|
||||
// 4. Let externglobal be global globaladdr.
|
||||
Wasm::ExternValue externglobal { Wasm::GlobalAddress { *globaladdr } };
|
||||
|
||||
// 5. Append externglobal to imports.
|
||||
imports.append(externglobal);
|
||||
}
|
||||
|
||||
// 5. If importtype is of the form mem memtype,
|
||||
if (entry.description().has<Wasm::MemoryType>()) {
|
||||
// 1. If v does not implement Memory, throw a LinkError exception.
|
||||
if (!v.is_object() || !is<WebAssembly::Memory>(v.as_object())) {
|
||||
return vm.throw_completion<LinkError>("Expected an instance of WebAssembly.Memory for a memory import"sv);
|
||||
}
|
||||
|
||||
// 2. Let externmem be the external value mem v.[[Memory]].
|
||||
auto externmem = static_cast<WebAssembly::Memory const&>(v.as_object()).address();
|
||||
|
||||
// 3. Append externmem to imports.
|
||||
imports.append(externmem);
|
||||
}
|
||||
|
||||
// 6. If importtype is of the form table tabletype,
|
||||
if (entry.description().has<Wasm::TableType>()) {
|
||||
// 1. If v does not implement Table, throw a LinkError exception.
|
||||
if (!v.is_object() || !is<WebAssembly::Table>(v.as_object())) {
|
||||
return vm.throw_completion<LinkError>("Expected an instance of WebAssembly.Table for a table import"sv);
|
||||
}
|
||||
|
||||
// 2. Let tableaddr be v.[[Table]].
|
||||
auto tableaddr = static_cast<WebAssembly::Table const&>(v.as_object()).address();
|
||||
|
||||
// 3. Let externtable be the external value table tableaddr.
|
||||
Wasm::ExternValue externtable { tableaddr };
|
||||
|
||||
// 4. Append externtable to imports.
|
||||
imports.append(externtable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Instantiate the core of a WebAssembly module module with imports, and let instance be the result.
|
||||
// https://webassembly.github.io/spec/js-api/index.html#instantiate-the-core-of-a-webassembly-module
|
||||
auto instantiation_result = cache.abstract_machine().instantiate(module->module, imports);
|
||||
if (instantiation_result.is_error()) {
|
||||
auto instantiation_error = instantiation_result.release_error();
|
||||
switch (instantiation_error.source) {
|
||||
case Wasm::InstantiationErrorSource::Linking:
|
||||
return vm.throw_completion<LinkError>(instantiation_error.error);
|
||||
case Wasm::InstantiationErrorSource::StartFunction:
|
||||
return vm.throw_completion<RuntimeError>(instantiation_error.error);
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// 7. Set record.[[Instance]] to instance.
|
||||
record->m_instance = vm.heap().allocate<Instance>(*vm.current_realm(), instantiation_result.release_value());
|
||||
|
||||
// 8. For each (name, externtype) of module_exports(module),
|
||||
for (auto const& entry : module->module->export_section().entries()) {
|
||||
// 1. If externtype is of the form global mut globaltype,
|
||||
if (entry.description().has<Wasm::GlobalIndex>()) {
|
||||
// 1. Assert: externval is of the form global globaladdr.
|
||||
// 2. Let global globaladdr be externval.
|
||||
// 3. Let global_value be global_read(store, globaladdr).
|
||||
auto globaladdr = Wasm::GlobalAddress { entry.description().get<Wasm::GlobalIndex>().value() };
|
||||
auto* global_value = cache.abstract_machine().store().get(globaladdr);
|
||||
VERIFY(global_value);
|
||||
|
||||
// 4. If globaltype is not v128,
|
||||
auto type = global_value->type();
|
||||
if (type.type().kind() != Wasm::ValueType::Kind::V128) {
|
||||
// NOTE: The condition above leaves unsupported JS values as uninitialized in TDZ and therefore as a
|
||||
// reference error on access. When integrating with shared globals, they may be excluded here
|
||||
// similarly to v128 above.
|
||||
|
||||
// 1. Perform !record.[[Environment]].InitializeBinding(name, ToJSValue(global_value)).
|
||||
auto value = global_value->value();
|
||||
MUST(record->environment()->initialize_binding(vm, Utf16FlyString::from_utf8(entry.name()), Detail::to_js_value(vm, value, type.type()), JS::Environment::InitializeBindingHint::Normal));
|
||||
|
||||
// FIXME: 2. If mut is var, then associate all future mutations of globaladdr with the ECMA-262 binding record
|
||||
// for name in record.[[Environment]], such that record.[[Environment]].GetBindingValue(resolution.[[BindingName]], true)
|
||||
// always returns ToJSValue(global_read(store, globaladdr)) for the current surrounding agent’s associated store store.
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Otherwise,
|
||||
else {
|
||||
// 1. Perform !record.[[Environment]].InitializeBinding(name, !Get(instance.[[Exports]], name)).
|
||||
auto name = Utf16FlyString::from_utf8(entry.name());
|
||||
MUST(record->environment()->initialize_binding(vm, name, MUST(record->m_instance->get(JS::PropertyKey { name })), JS::Environment::InitializeBindingHint::Normal));
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The linking semantics here for Wasm to Wasm modules are identical to the WebAssembly JS API semantics as if
|
||||
// passing the the exports object as the imports object in instantiation. When linking Wasm module imports to
|
||||
// JS module exports, the JS API semantics are exactly followed as well. It is only in the case of importing
|
||||
// Wasm from JS that WebAssembly.Global unwrapping is observable on the WebAssembly Module Record Environment
|
||||
// Record.
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
47
Libraries/LibWeb/WebAssembly/WebAssemblyModule.h
Normal file
47
Libraries/LibWeb/WebAssembly/WebAssemblyModule.h
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Glenn Skrzypczak <glenn.skrzypczak@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibJS/CyclicModule.h>
|
||||
#include <LibJS/Export.h>
|
||||
#include <LibWasm/AbstractMachine/AbstractMachine.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace Web::WebAssembly {
|
||||
|
||||
// 16.2.1.6 Source Text Module Records, https://tc39.es/ecma262/#sec-source-text-module-records
|
||||
class WebAssemblyModule final : public JS::CyclicModule {
|
||||
GC_CELL(WebAssemblyModule, JS::CyclicModule);
|
||||
GC_DECLARE_ALLOCATOR(WebAssemblyModule);
|
||||
|
||||
public:
|
||||
virtual ~WebAssemblyModule() override;
|
||||
|
||||
static JS::ThrowCompletionOr<GC::Ref<WebAssemblyModule>> parse(ByteBuffer bytes, JS::Realm&, StringView filename = {}, JS::Script::HostDefined* host_defined = nullptr);
|
||||
|
||||
Vector<Utf16FlyString> export_name_list();
|
||||
|
||||
virtual Vector<Utf16FlyString> get_exported_names(JS::VM& vm, HashTable<Module const*>& export_star_set) override;
|
||||
virtual JS::ResolvedBinding resolve_export(JS::VM& vm, Utf16FlyString const& export_name, Vector<JS::ResolvedBinding> resolve_set = {}) override;
|
||||
|
||||
protected:
|
||||
virtual JS::ThrowCompletionOr<void> initialize_environment(JS::VM& vm) override;
|
||||
virtual JS::ThrowCompletionOr<void> execute_module(JS::VM& vm, GC::Ptr<JS::PromiseCapability> capability) override;
|
||||
|
||||
private:
|
||||
WebAssemblyModule(JS::Realm&, StringView filename, WebAssembly::Module& module_source, JS::Script::HostDefined* host_defined, Vector<JS::ModuleRequest> requested_modules);
|
||||
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
GC::Ptr<Instance> m_instance; // [[Instance]]
|
||||
GC::Ref<WebAssembly::Module> m_module_source; // [[ModuleSource]]
|
||||
GC::Ptr<WebAssemblyModule> m_module_record; // [[ModuleRecord]]
|
||||
|
||||
Optional<Vector<Utf16FlyString>> m_cached_export_name_list;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ Text/input/wpt-import/html/semantics/forms/the-input-element/show-picker-disable
|
|||
Text/input/wpt-import/html/semantics/scripting-1/the-script-element/css-module/import-css-module-dynamic.html
|
||||
Text/input/wpt-import/html/semantics/scripting-1/the-script-element/css-module/import-css-module-basic.html
|
||||
Text/input/wpt-import/html/semantics/scripting-1/the-script-element/json-module/module.html
|
||||
Text/input/wpt-import/wasm/webapi/esm-integration/wasm-import.tentative.html
|
||||
|
||||
; Unable to fetch the test resources JSON with CORS error.
|
||||
; Fails in other browsers too when loaded from file://.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 1 tests
|
||||
|
||||
1 Pass
|
||||
Pass Invalid imports for WebAssembly modules should error.
|
||||
|
|
@ -0,0 +1 @@
|
|||
export let f = 5;
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
export let g = 5;
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
export let m = 5;
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
export let t = 5;
|
||||
Binary file not shown.
|
|
@ -0,0 +1,34 @@
|
|||
<!doctype html>
|
||||
<title>Errors for imports of WebAssembly modules</title>
|
||||
|
||||
<script src="../../../resources/testharness.js"></script>
|
||||
<script src="../../../resources/testharnessreport.js"></script>
|
||||
<script>
|
||||
setup({allow_uncaught_exception: true});
|
||||
|
||||
const test_load = async_test(
|
||||
"Invalid imports for WebAssembly modules should error.");
|
||||
|
||||
window.log = [];
|
||||
window.addEventListener("error", ev => {
|
||||
test_load.step(() => assert_equals(ev.error.constructor, WebAssembly.LinkError));
|
||||
log.push(ev.message);
|
||||
});
|
||||
|
||||
window.addEventListener("load", test_load.step_func_done(ev => {
|
||||
assert_equals(log[1], 1);
|
||||
assert_equals(log[3], 2);
|
||||
assert_equals(log[5], 3);
|
||||
assert_equals(log[7], 4);
|
||||
}));
|
||||
|
||||
function unreachable() { log.push("unexpected"); }
|
||||
</script>
|
||||
<script type="module" src="./resources/wasm-import-func.wasm"
|
||||
onerror="unreachable()" onload="log.push(1)"></script>
|
||||
<script type="module" src="./resources/wasm-import-memory.wasm"
|
||||
onerror="unreachable()" onload="log.push(2)"></script>
|
||||
<script type="module" src="./resources/wasm-import-table.wasm"
|
||||
onerror="unreachable()" onload="log.push(3)"></script>
|
||||
<script type="module" src="./resources/wasm-import-global.wasm"
|
||||
onerror="unreachable()" onload="log.push(4)"></script>
|
||||
Loading…
Reference in a new issue