/* * Copyright (c) 2025, Glenn Skrzypczak * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include namespace Web::WebAssembly { GC_DEFINE_ALLOCATOR(WebAssemblyModule); WebAssemblyModule::WebAssemblyModule(JS::Realm& realm, StringView filename, WebAssembly::Module& module_source, JS::Script::HostDefined* host_defined, Vector 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> 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(realm, module); // 5. Let requestedModules be a set. HashTable 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("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("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("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 module_requests; for (auto const& module_name : requested_modules) { module_requests.append(JS::ModuleRequest { Utf16FlyString::from_utf8(module_name), {} }); } auto module_record = realm.create(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 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 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 WebAssemblyModule::get_exported_names(JS::VM&, HashTable&) { // 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) { // 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 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(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 WebAssemblyModule::execute_module(JS::VM& vm, GC::Ptr 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 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(*resolved_module)) { auto& resolved_webassembly_module = as(*resolved_module); // 1. If resolvedModule.[[Instance]] is ~empty~, throw a {LinkError} exception. if (!resolved_webassembly_module.m_instance) { return vm.throw_completion("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 { if (!externtype.has()) return "Expected memory import"sv; auto other_mem_type = store.get(Wasm::MemoryAddress { externtype.get().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 { if (!externtype.has()) return "Expected table import"sv; auto other_table_type = store.get(Wasm::TableAddress { externtype.get().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 { if (!externtype.has()) return "Expected global import"sv; auto other_global_type = store.get(Wasm::GlobalAddress { externtype.get().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 { if (!externtype.has()) return "Expected function import"sv; auto other_type = store.get(Wasm::FunctionAddress { externtype.get().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 { if (!externtype.has()) return "Expected tag import"sv; auto* other_tag_instance = store.get(Wasm::TagAddress { externtype.get().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 { if (!externtype.has()) return "Expected function import"sv; auto other_type = store.get(Wasm::FunctionAddress { externtype.get().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(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() || entry.description().has()) { 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(JS::ErrorType::NotAFunction, v); auto& function = v.as_function(); // 2. If v has a [[FunctionAddress]] internal slot, and therefore is an Exported Function, Optional funcaddr; if (is(function)) { // 1. Let funcaddr be the value of v’s [[FunctionAddress]] internal slot. auto& exported_function = static_cast(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()) { auto valtype = entry.description().get(); // 1. Let store be the surrounding agent’s associated store. auto& store = cache.abstract_machine().store(); // 2. If v implements Global, Optional globaladdr; if (v.is_object() && is(v.as_object())) { // 1. Let globaladdr be v.[[Global]]. globaladdr = as(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("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("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("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("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("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()) { // 1. If v does not implement Memory, throw a LinkError exception. if (!v.is_object() || !is(v.as_object())) { return vm.throw_completion("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(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()) { // 1. If v does not implement Table, throw a LinkError exception. if (!v.is_object() || !is(v.as_object())) { return vm.throw_completion("Expected an instance of WebAssembly.Table for a table import"sv); } // 2. Let tableaddr be v.[[Table]]. auto tableaddr = static_cast(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(instantiation_error.error); case Wasm::InstantiationErrorSource::StartFunction: return vm.throw_completion(instantiation_error.error); } VERIFY_NOT_REACHED(); } // 7. Set record.[[Instance]] to instance. record->m_instance = vm.heap().allocate(*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()) { // 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().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 {}; } }