/* * Copyright (c) 2026-present, Ali Mohammad Pur * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(AK_OS_WINDOWS) # include #else # include # include # include #endif #if defined(AK_OS_MACOS) # include # include #endif using namespace Wasm; using namespace Cranelift; namespace { struct InputHeader { u32 function_count; u32 helpers_offset; u64 outcome_return; u64 code_region_start; u64 reloc_region_start; u64 total_size; }; struct InputFunctionEntry { u32 insn_offset; u32 insn_count; u32 result_arity; }; struct OutputFunctionEntry { u64 code_offset; u32 code_size; u32 compiled; // Offset (relative to the start of the reloc region) and count of `HelperReloc` // entries describing the absolute helper addresses baked into this function's code. // On cache install we walk these and rewrite the 8 bytes at code+code_offset+offset // with the live address of helper N for the current process. u64 reloc_offset; u32 reloc_count; u64 trap_offset; u32 trap_count; u32 _pad; }; struct CodeMapping { void* mapping; size_t size; Vector traps; }; static constexpr size_t oop_code_region_min_size = 256 * KiB; static constexpr size_t oop_code_bytes_per_insn = 256; static constexpr size_t oop_reloc_region_min_size = 64 * KiB; static constexpr size_t oop_reloc_bytes_per_insn = 128; static size_t align_up(size_t value, size_t alignment) { VERIFY(alignment > 0); auto remainder = value % alignment; return remainder == 0 ? value : value + (alignment - remainder); } struct BatchInput { Vector insns; u32 result_arity; u32 function_index; CompiledInstructions* target; }; // Disk-cache blob format. Stable: cached files name format_version + layout_hash so // any rebuild that changes those will simply miss the cache rather than try to // execute incompatible bytes. constexpr u64 cache_blob_magic = 0x4354494A4D534157ULL; // "WASMJITC" little-endian constexpr u32 cache_blob_format_version = 3; struct CacheBlobHeader { u64 magic; u32 format_version; u32 helper_count; u64 layout_hash; u8 wasm_hash[32]; u32 function_count; u32 _pad; }; static_assert(sizeof(CacheBlobHeader) == 64); struct CacheBlobFunctionEntry { u32 function_index; u32 code_size; u32 reloc_count; u32 trap_count; }; static_assert(sizeof(CacheBlobFunctionEntry) == 16); struct CacheRecord { u32 function_index; ByteBuffer unpatched_code; Vector relocs; Vector traps; }; // On a cache miss we capture every successful compile so we can hand the blob to a // store callback after validation finishes. On a cache hit we populate the install // map up front; the per-function lookup happens inside try_cranelift_compile when // the dispatch table for that function has just been built and is ready to receive // a handler_ptr. struct CacheCaptureState { bool capturing { false }; Vector records; }; struct PendingInstallState { bool active { false }; HashMap records; }; struct CacheState { CacheCaptureState cache_capture; PendingInstallState pending_install; Vector pending_batch; }; static thread_local u32 s_active_function_index = NumericLimits::max(); static CacheState& cranelift_cache_state() { static thread_local auto* state = new CacheState; return *state; } static u64 compute_layout_hash(RuntimeHelpers const& h) { auto fnv1a = [](u64 hash, u64 value) { for (int i = 0; i < 8; ++i) { hash ^= (value >> (i * 8)) & 0xff; hash *= 0x100000001b3ULL; } return hash; }; u64 hash = 0xcbf29ce484222325ULL; hash = fnv1a(hash, h.regs_offset); hash = fnv1a(hash, h.value_size); hash = fnv1a(hash, h.locals_base_offset); hash = fnv1a(hash, h.default_memory_base_offset); hash = fnv1a(hash, h.compiled_call_result_scratch_offset); return hash; } // `HelperId` values are assigned in lockstep with the field order of `RuntimeHelpers`, // so the helper address for id N is simply the N-th `size_t` field of the struct. static_assert(offsetof(RuntimeHelpers, call_function) == 0); static_assert(offsetof(RuntimeHelpers, memory_fill) == sizeof(size_t) * 30); static_assert(HELPER_COUNT == 31); static bool apply_helper_relocs(u8* code_bytes, size_t code_size, HelperReloc const* relocs, size_t reloc_count, RuntimeHelpers const& helpers) { auto const* helper_table = reinterpret_cast(&helpers); for (size_t i = 0; i < reloc_count; ++i) { auto const& r = relocs[i]; if (r.helper_id >= HELPER_COUNT) return false; if (static_cast(r.code_offset) + sizeof(u64) > code_size) return false; u64 addr = static_cast(helper_table[r.helper_id]) + static_cast(r.addend); __builtin_memcpy(code_bytes + r.code_offset, &addr, sizeof(addr)); } return true; } // Allocate an RX-able page, copy the (still unpatched) machine code into it, apply the // helper-address patches, and install the resulting function pointer into `target`. // Used by both the fresh-compile path (bytes come from the subprocess shm) and the // cache-install path (bytes come from a `.wasmjit` blob). static bool install_compiled_function(CompiledInstructions& target, ReadonlyBytes code_bytes, HelperReloc const* relocs, size_t reloc_count, ReadonlySpan traps, RuntimeHelpers const& helpers) { if (target.dispatches.is_empty()) return false; auto const code_size = code_bytes.size(); if (code_size == 0) return false; #if defined(AK_OS_WINDOWS) SYSTEM_INFO si; GetSystemInfo(&si); auto const page_size = static_cast(si.dwPageSize); auto const rx_aligned_size = (code_size + page_size - 1) & ~(page_size - 1); auto* jit_mem = VirtualAlloc(nullptr, rx_aligned_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!jit_mem) return false; __builtin_memcpy(jit_mem, code_bytes.data(), code_size); if (!apply_helper_relocs(static_cast(jit_mem), code_size, relocs, reloc_count, helpers)) { VirtualFree(jit_mem, 0, MEM_RELEASE); return false; } DWORD old_protect; VirtualProtect(jit_mem, rx_aligned_size, PAGE_EXECUTE_READ, &old_protect); FlushInstructionCache(GetCurrentProcess(), jit_mem, code_size); auto* func_ptr = static_cast(jit_mem); auto* handle = new CodeMapping { jit_mem, rx_aligned_size, {} }; #elif defined(AK_OS_MACOS) auto const page_size = static_cast(sysconf(_SC_PAGESIZE)); auto const rx_aligned_size = (code_size + page_size - 1) & ~(page_size - 1); auto* jit_mapping = mmap(nullptr, rx_aligned_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON | MAP_JIT, -1, 0); if (jit_mapping == MAP_FAILED) return false; pthread_jit_write_protect_np(0); __builtin_memcpy(jit_mapping, code_bytes.data(), code_size); if (!apply_helper_relocs(static_cast(jit_mapping), code_size, relocs, reloc_count, helpers)) { munmap(jit_mapping, rx_aligned_size); return false; } pthread_jit_write_protect_np(1); sys_icache_invalidate(jit_mapping, code_size); auto* func_ptr = static_cast(jit_mapping); auto* handle = new CodeMapping { jit_mapping, rx_aligned_size, {} }; #else auto const page_size = static_cast(sysconf(_SC_PAGESIZE)); auto const rx_aligned_size = (code_size + page_size - 1) & ~(page_size - 1); auto* rw_mapping = mmap(nullptr, rx_aligned_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (rw_mapping == MAP_FAILED) return false; __builtin_memcpy(rw_mapping, code_bytes.data(), code_size); if (!apply_helper_relocs(static_cast(rw_mapping), code_size, relocs, reloc_count, helpers) || mprotect(rw_mapping, rx_aligned_size, PROT_READ | PROT_EXEC) != 0) { munmap(rw_mapping, rx_aligned_size); return false; } __builtin___clear_cache(static_cast(rw_mapping), static_cast(rw_mapping) + code_size); auto* func_ptr = static_cast(rw_mapping); auto* handle = new CodeMapping { rw_mapping, rx_aligned_size, {} }; #endif handle->traps.ensure_capacity(traps.size()); for (auto const& trap : traps) handle->traps.unchecked_append(trap); target.cranelift_code_handle = handle; target.cranelift_code_size = code_size; target.cranelift_traps = handle->traps.data(); target.cranelift_trap_count = handle->traps.size(); target.cranelift_compiled = true; publish_cranelift_entry(target, bit_cast(func_ptr)); return true; } } extern "C" { static ALWAYS_INLINE i32 wasm_cl_finish_call(BytecodeInterpreter& interpreter, Configuration& config, FunctionAddress address, Vector& args) { if (interpreter.trap_if_insufficient_native_stack_space()) return 1; auto* instance = config.store().unsafe_get(address); if (auto* wasm_function = instance->get_pointer(); wasm_function && !config.should_limit_instruction_count() && cranelift_entry_acquire(wasm_function->code().func().body().compiled_instructions) != 0) { // Fast compiled-to-compiled call: stack-allocate locals + non-owning frame. auto& func = wasm_function->code().func(); auto arg_count = args.size(); auto local_count = func.total_local_count(); auto total = arg_count + local_count; auto callee_arity = wasm_function->type().results().size(); Value callee_buf[64]; Value* callee_locals = callee_buf; Vector heap_fallback; if (total > 64) [[unlikely]] { heap_fallback.ensure_capacity(total); heap_fallback.resize_and_keep_capacity(total); callee_locals = heap_fallback.data(); } for (size_t i = 0; i < arg_count; i++) callee_locals[i] = args[i]; __builtin_memset(callee_locals + arg_count, 0, local_count * sizeof(Value)); auto& ci = func.body().compiled_instructions; { BytecodeInterpreter::CallFrameHandle handle { interpreter, config }; config.set_frame_lightweight(wasm_function->module(), callee_locals, func.body(), callee_arity); config.setup_call_record_for_current_frame(); config.ip() = 0; interpreter.clear_trap(); auto const* cc = ci.dispatches.data(); auto const* addrs = ci.src_dst_mappings.data(); using HandlerFn = Outcome (*)(BytecodeInterpreter&, Configuration&, Instruction const*, u32, Dispatch const*, SourcesAndDestination const*); auto const handler = bit_cast(cranelift_entry_acquire(ci)); auto outcome = handler(interpreter, config, cc[0].instruction, 0, cc, addrs); if (outcome != Outcome::Return) { interpreter.set_trap("Compiled function returned unexpectedly"sv); return 1; } if (interpreter.did_trap()) return 1; if (callee_arity == 1) config.compiled_call_result_scratch() = config.value_stack().unsafe_take_last(); // No label pop: set_frame_lightweight doesn't push labels. } return 0; } // direct-threaded interpreter path: // CallFrameHandle saves/zeros/restores the direct call counter automatically. if (auto* wasm_function = instance->get_pointer(); wasm_function && !config.should_limit_instruction_count() && wasm_function->code().func().body().compiled_instructions.direct) { BytecodeInterpreter::CallFrameHandle handle { interpreter, config }; if (auto prepare_result = config.prepare_wasm_call(*wasm_function, args); prepare_result.is_error()) { interpreter.set_trap(prepare_result.release_error()); return 1; } config.ip() = 0; auto outcome = interpreter.run_compiled_function_direct(config); if (outcome != Outcome::Return) { interpreter.set_trap("Compiled function returned unexpectedly"sv); return 1; } if (interpreter.did_trap()) return 1; if (config.frame().arity() == 1) config.compiled_call_result_scratch() = config.value_stack().unsafe_take_last(); if (!config.label_stack().is_empty()) config.label_stack().take_last(); return 0; } // non-compiled call (interpreter or host function) Wasm::Result result { Vector {} }; if (instance->has()) { BytecodeInterpreter::CallFrameHandle handle { interpreter, config }; result = config.call(interpreter, address, args); } else { result = config.call(interpreter, address, args); config.release_arguments_allocation(args); } if (result.is_trap()) { interpreter.set_trap(move(result.trap())); return 1; } if (!result.values().is_empty()) config.compiled_call_result_scratch() = result.values().first(); return 0; } i32 wasm_cl_call_function(void* interp_ptr, void* config_ptr, i32 func_index); i32 wasm_cl_call_function(void* interp_ptr, void* config_ptr, i32 func_index) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto const& functions = module.functions(); if (static_cast(func_index) >= functions.size()) return 1; auto address = functions[func_index]; SourcesAndDestination addrs {}; addrs.sources[0] = Dispatch::RegisterOrStack::Stack; addrs.sources[1] = Dispatch::RegisterOrStack::Stack; addrs.sources[2] = Dispatch::RegisterOrStack::Stack; addrs.destination = Dispatch::RegisterOrStack::Stack; auto outcome = interpreter.call_address(config, address, addrs, BytecodeInterpreter::CallAddressSource::DirectCall, BytecodeInterpreter::CallType::UsingStack); return outcome == Outcome::Return && interpreter.did_trap() ? 1 : 0; } void wasm_cl_set_trap(void* interp_ptr, u8 const* msg, i32 len); void wasm_cl_set_trap(void* interp_ptr, u8 const* msg, i32 len) { auto& interpreter = *static_cast(interp_ptr); interpreter.set_trap(StringView(reinterpret_cast(msg), len)); } static inline MemoryInstance* wasm_cl_get_memory(void* config_ptr, i32 mem_idx) { auto& config = *static_cast(config_ptr); if (mem_idx == 0) [[likely]] { if (auto* memory = config.default_memory()) return memory; } auto const& module = config.frame().module(); auto const& mem_address = module.memories().data()[mem_idx]; return config.store().unsafe_get(mem_address); } static inline u8 const* wasm_cl_memory_data_if_in_bounds(MemoryInstance* memory, u64 instance_addr, size_t size) { if (instance_addr > memory->size() || size > memory->size() - instance_addr) return nullptr; return memory->data().offset_pointer(instance_addr); } i32 wasm_cl_memory_load8_s(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out); i32 wasm_cl_memory_load8_s(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto const* data = wasm_cl_memory_data_if_in_bounds(memory, static_cast(addr), 1); if (!data) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return 1; } *out = static_cast(static_cast(data[0])); return 0; } i32 wasm_cl_memory_load8_u(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out); i32 wasm_cl_memory_load8_u(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto const* data = wasm_cl_memory_data_if_in_bounds(memory, static_cast(addr), 1); if (!data) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return 1; } *out = static_cast(data[0]); return 0; } i32 wasm_cl_memory_load16_s(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out); i32 wasm_cl_memory_load16_s(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto const* data = wasm_cl_memory_data_if_in_bounds(memory, static_cast(addr), 2); if (!data) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return 1; } u16 val; __builtin_memcpy(&val, data, sizeof(val)); *out = static_cast(static_cast(val)); return 0; } i32 wasm_cl_memory_load16_u(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out); i32 wasm_cl_memory_load16_u(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto const* data = wasm_cl_memory_data_if_in_bounds(memory, static_cast(addr), 2); if (!data) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return 1; } u16 val; __builtin_memcpy(&val, data, sizeof(val)); *out = static_cast(val); return 0; } i32 wasm_cl_memory_load32_s(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out); i32 wasm_cl_memory_load32_s(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto const* data = wasm_cl_memory_data_if_in_bounds(memory, static_cast(addr), 4); if (!data) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return 1; } u32 val; __builtin_memcpy(&val, data, sizeof(val)); *out = static_cast(static_cast(val)); return 0; } i32 wasm_cl_memory_load32_u(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out); i32 wasm_cl_memory_load32_u(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto const* data = wasm_cl_memory_data_if_in_bounds(memory, static_cast(addr), 4); if (!data) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return 1; } u32 val; __builtin_memcpy(&val, data, sizeof(val)); *out = static_cast(val); return 0; } i32 wasm_cl_memory_load64(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out); i32 wasm_cl_memory_load64(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64* out) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto const* data = wasm_cl_memory_data_if_in_bounds(memory, static_cast(addr), 8); if (!data) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return 1; } u64 val; __builtin_memcpy(&val, data, sizeof(val)); *out = static_cast(val); return 0; } static inline bool wasm_cl_memory_store_in_bounds(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, size_t size, u8*& data) { auto* memory = wasm_cl_get_memory(config_ptr, mem_idx); auto instance_addr = static_cast(addr); if (instance_addr > memory->size() || size > memory->size() - instance_addr) { static_cast(interp_ptr)->set_trap("Memory access out of bounds"sv); return false; } data = memory->data().offset_pointer(instance_addr); return true; } i32 wasm_cl_memory_store8(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value); i32 wasm_cl_memory_store8(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value) { u8* data; if (!wasm_cl_memory_store_in_bounds(interp_ptr, config_ptr, mem_idx, addr, 1, data)) return 1; // OOB trap data[0] = static_cast(value); return 0; } i32 wasm_cl_memory_store16(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value); i32 wasm_cl_memory_store16(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value) { u8* data; if (!wasm_cl_memory_store_in_bounds(interp_ptr, config_ptr, mem_idx, addr, 2, data)) return 1; u16 val = static_cast(value); __builtin_memcpy(data, &val, sizeof(val)); return 0; } i32 wasm_cl_memory_store32(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value); i32 wasm_cl_memory_store32(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value) { u8* data; if (!wasm_cl_memory_store_in_bounds(interp_ptr, config_ptr, mem_idx, addr, 4, data)) return 1; u32 val = static_cast(value); __builtin_memcpy(data, &val, sizeof(val)); return 0; } i32 wasm_cl_memory_store64(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value); i32 wasm_cl_memory_store64(void* interp_ptr, void* config_ptr, i32 mem_idx, i64 addr, i64 value) { u8* data; if (!wasm_cl_memory_store_in_bounds(interp_ptr, config_ptr, mem_idx, addr, 8, data)) return 1; u64 val = static_cast(value); __builtin_memcpy(data, &val, sizeof(val)); return 0; } i64 wasm_cl_memory_size(void* config_ptr, i32 mem_idx); i64 wasm_cl_memory_size(void* config_ptr, i32 mem_idx) { auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto const& mem_address = module.memories().data()[mem_idx]; auto* memory = config.store().unsafe_get(mem_address); return static_cast(memory->size() / Constants::page_size); } i32 wasm_cl_memory_grow(void* config_ptr, i32 mem_idx, i32 pages); i32 wasm_cl_memory_grow(void* config_ptr, i32 mem_idx, i32 pages) { auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto const& mem_address = module.memories().data()[mem_idx]; auto* memory = config.store().unsafe_get(mem_address); auto old_pages = memory->size() / Constants::page_size; if (!memory->grow(pages * Constants::page_size)) return -1; if (mem_idx == 0) config.refresh_default_memory_base(); return static_cast(old_pages); } i64 wasm_cl_read_global(void* config_ptr, i32 index); i64 wasm_cl_read_global(void* config_ptr, i32 index) { auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto global_address = module.globals().data()[index]; auto* global = config.store().get(global_address); return global->value().to(); } void wasm_cl_write_global(void* config_ptr, i32 index, i64 value); void wasm_cl_write_global(void* config_ptr, i32 index, i64 value) { auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto global_address = module.globals().data()[index]; auto* global = config.store().get(global_address); global->set_value(Value(value)); } i32 wasm_cl_call_indirect(void* interp_ptr, void* config_ptr, i32 table_idx, i32 type_idx, i32 element_index); i32 wasm_cl_call_indirect(void* interp_ptr, void* config_ptr, i32 table_idx, i32 type_idx, i32 element_index) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto table_address = module.tables()[table_idx]; auto* table_instance = config.store().get(table_address); if (!table_instance || element_index < 0 || static_cast(element_index) >= table_instance->elements().size()) return interpreter.set_trap(Trap::from_string("Table index out of bounds")); auto& element = table_instance->elements()[element_index]; if (!element.ref().has()) return interpreter.set_trap(Trap::from_string("Table element is not a function reference")); auto address = element.ref().get().address; auto* function = config.store().get(address); if (!function) return interpreter.set_trap(Trap::from_string("Indirect call to freed function")); // https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-control-mathsf-call-indirect-x-y // call_indirect's runtime check is a defined-type match (a downcast), not structural equality. auto const* type_actual = function->visit([](auto& f) { return f.defined_type(); }); auto const* type_expected = module.canonical_types()[type_idx]; if (!type_actual || !matches_defined_type(*type_actual, *type_expected)) return interpreter.set_trap(Trap::from_string("Indirect call type mismatch")); SourcesAndDestination addrs {}; addrs.sources[0] = Dispatch::RegisterOrStack::Stack; addrs.sources[1] = Dispatch::RegisterOrStack::Stack; addrs.sources[2] = Dispatch::RegisterOrStack::Stack; addrs.destination = Dispatch::RegisterOrStack::Stack; auto outcome = interpreter.call_address(config, address, addrs, BytecodeInterpreter::CallAddressSource::IndirectCall, BytecodeInterpreter::CallType::UsingStack); return outcome == Outcome::Return && interpreter.did_trap() ? 1 : 0; } i32 wasm_cl_memory_copy(void* interp_ptr, void* config_ptr, i32 dst_mem, i32 src_mem, i32 dst_offset, i32 src_offset, i32 count); i32 wasm_cl_memory_copy(void* interp_ptr, void* config_ptr, i32 dst_mem, i32 src_mem, i32 dst_offset, i32 src_offset, i32 count) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto* src_instance = config.store().unsafe_get(module.memories().data()[src_mem]); auto* dst_instance = config.store().unsafe_get(module.memories().data()[dst_mem]); auto src_end = static_cast(static_cast(src_offset)) + static_cast(count); auto dst_end = static_cast(static_cast(dst_offset)) + static_cast(count); if (src_end > src_instance->size() || dst_end > dst_instance->size()) return interpreter.set_trap(Trap::from_string("Memory access out of bounds")); if (count > 0) __builtin_memmove(dst_instance->data().data() + static_cast(dst_offset), src_instance->data().data() + static_cast(src_offset), static_cast(count)); return 0; } i32 wasm_cl_memory_fill(void* interp_ptr, void* config_ptr, i32 mem_idx, i32 offset, i32 value, i32 count); i32 wasm_cl_memory_fill(void* interp_ptr, void* config_ptr, i32 mem_idx, i32 offset, i32 value, i32 count) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto* instance = config.store().unsafe_get(module.memories().data()[mem_idx]); auto end = static_cast(static_cast(offset)) + static_cast(count); if (end > instance->size()) return interpreter.set_trap(Trap::from_string("Memory access out of bounds")); if (count > 0) __builtin_memset(instance->data().data() + static_cast(offset), static_cast(value), static_cast(count)); return 0; } void wasm_cl_stack_push(void* config_ptr, i64 value); void wasm_cl_stack_push(void* config_ptr, i64 value) { auto& config = *static_cast(config_ptr); config.value_stack().append(Value(value)); } i64 wasm_cl_stack_pop(void* config_ptr); i64 wasm_cl_stack_pop(void* config_ptr) { auto& config = *static_cast(config_ptr); return config.value_stack().unsafe_take_last().to(); } i64 wasm_cl_stack_size(void* config_ptr); i64 wasm_cl_stack_size(void* config_ptr) { auto& config = *static_cast(config_ptr); return static_cast(config.value_stack().size()); } void wasm_cl_stack_cleanup(void* config_ptr, i64 initial_size, i32 result_arity); void wasm_cl_stack_cleanup(void* config_ptr, i64 initial_size, i32 result_arity) { auto& config = *static_cast(config_ptr); auto& stack = config.value_stack(); auto expected = static_cast(initial_size) + static_cast(result_arity); if (stack.size() == expected) return; if (stack.size() < expected) { // Under-push (e.g. trap path), fill with zeros so the caller has something to pop. while (stack.size() < expected) stack.append(Value(static_cast(0))); return; } // Take results, trim stack, and put them back. Value saved[8]; auto n = min(static_cast(result_arity), size_t(8)); for (size_t i = 0; i < n; i++) saved[i] = stack.unsafe_take_last(); while (stack.size() > static_cast(initial_size)) stack.unsafe_take_last(); for (size_t i = n; i > 0; i--) stack.append(saved[i - 1]); } i64 wasm_cl_callrec_read(void* config_ptr, i32 index); i64 wasm_cl_callrec_read(void* config_ptr, i32 index) { auto& config = *static_cast(config_ptr); return config.call_record_entry(index).to(); } void wasm_cl_callrec_write(void* config_ptr, i32 index, i64 value); void wasm_cl_callrec_write(void* config_ptr, i32 index, i64 value) { auto& config = *static_cast(config_ptr); if (!config.call_record_base()) [[unlikely]] { // No call record yet, allocate now. auto size = config.frame().expression().compiled_instructions.max_call_rec_size; if (size == 0) size = static_cast(index) + 1; config.setup_call_record(size); } config.call_record_entry(index) = Value(value); } i32 wasm_cl_call_with_record(void* interp_ptr, void* config_ptr, i32 func_index); i32 wasm_cl_call_with_record(void* interp_ptr, void* config_ptr, i32 func_index) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); auto const& module = config.frame().module(); auto const& functions = module.functions(); if (static_cast(func_index) >= functions.size()) return 1; auto address = functions[func_index]; auto* instance = config.store().get(address); if (!instance) { interpreter.set_trap("Attempt to call nonexistent function by address"sv); return 1; } FunctionType const* type { nullptr }; instance->visit([&](auto const& function) { type = &function.type(); }); Vector args; config.take_call_record(args); args.shrink(type->parameters().size(), true); return wasm_cl_finish_call(interpreter, config, address, args); } // Direct compiled-to-compiled call. Falls back to wasm_cl_finish_call for non-compiled targets. static ALWAYS_INLINE i32 wasm_cl_direct_call_impl(BytecodeInterpreter& interpreter, Configuration& config, i32 func_index, Value* args, size_t arg_count) { auto const* table = config.frame().compiled_fn_table(); auto index = static_cast(func_index); if (!table || index >= table->size() || !(*table)[index].module) [[unlikely]] { // Not Cranelift-compiled, fall back to full path. Vector args_vec; args_vec.ensure_capacity(arg_count); for (size_t i = 0; i < arg_count; i++) args_vec.unchecked_append(args[i]); return wasm_cl_finish_call(interpreter, config, config.frame().module().functions()[func_index], args_vec); } auto const& entry = (*table)[index]; if (config.depth() > 500) [[unlikely]] { interpreter.set_trap(Constants::stack_exhaustion_message); return 1; } // Stack-allocate callee locals: args + zero-initialized locals. auto total = arg_count + entry.total_local_count; Value callee_locals_buf[64]; Value* callee_locals = callee_locals_buf; Vector heap_buf; if (total > 64) [[unlikely]] { heap_buf.ensure_capacity(total); heap_buf.resize_and_keep_capacity(total); callee_locals = heap_buf.data(); } for (size_t i = 0; i < arg_count; i++) callee_locals[i] = args[i]; __builtin_memset(callee_locals + arg_count, 0, entry.total_local_count * sizeof(Value)); // Lightweight non-owning frame push + direct handler call. BytecodeInterpreter::CallFrameHandle handle { interpreter, config }; config.set_frame_lightweight(*entry.module, callee_locals, *entry.expression, entry.arity); config.setup_call_record_for_current_frame(); config.ip() = 0; interpreter.clear_trap(); using HandlerFn = Outcome (*)(BytecodeInterpreter&, Configuration&, Instruction const*, u32, Dispatch const*, SourcesAndDestination const*); auto const handler = bit_cast(entry.handler_ptr); auto outcome = handler(interpreter, config, entry.first_insn, 0, bit_cast(entry.dispatches_ptr), bit_cast(entry.src_dst_ptr)); if (outcome != Outcome::Return) { interpreter.set_trap("Compiled function returned unexpectedly"sv); return 1; } if (interpreter.did_trap()) return 1; if (entry.arity == 1) config.compiled_call_result_scratch() = config.value_stack().unsafe_take_last(); // No label pop: set_frame_lightweight doesn't push labels. return 0; } i32 wasm_cl_direct_call_0(void* interp_ptr, void* config_ptr, i32 func_index); i32 wasm_cl_direct_call_0(void* interp_ptr, void* config_ptr, i32 func_index) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); return wasm_cl_direct_call_impl(interpreter, config, func_index, nullptr, 0); } i32 wasm_cl_direct_call_1(void* interp_ptr, void* config_ptr, i32 func_index, i64 arg0); i32 wasm_cl_direct_call_1(void* interp_ptr, void* config_ptr, i32 func_index, i64 arg0) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); Value args[] = { Value(arg0) }; return wasm_cl_direct_call_impl(interpreter, config, func_index, args, 1); } i32 wasm_cl_direct_call_2(void* interp_ptr, void* config_ptr, i32 func_index, i64 arg0, i64 arg1); i32 wasm_cl_direct_call_2(void* interp_ptr, void* config_ptr, i32 func_index, i64 arg0, i64 arg1) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); Value args[] = { Value(arg0), Value(arg1) }; return wasm_cl_direct_call_impl(interpreter, config, func_index, args, 2); } i32 wasm_cl_direct_call_3(void* interp_ptr, void* config_ptr, i32 func_index, i64 arg0, i64 arg1, i64 arg2); i32 wasm_cl_direct_call_3(void* interp_ptr, void* config_ptr, i32 func_index, i64 arg0, i64 arg1, i64 arg2) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); Value args[] = { Value(arg0), Value(arg1), Value(arg2) }; return wasm_cl_direct_call_impl(interpreter, config, func_index, args, 3); } // Thin frame push for direct compiled-to-compiled calls. Returns 1 on trap, 0 on success. i32 wasm_cl_push_frame(void* interp_ptr, void* config_ptr, Value* locals_ptr, u32, void const* module_ptr, void const* expression_ptr, u32 arity, u32 max_call_rec_size); i32 wasm_cl_push_frame(void* interp_ptr, void* config_ptr, Value* locals_ptr, u32 /* total_locals */, void const* module_ptr, void const* expression_ptr, u32 arity, u32 max_call_rec_size) { auto& interpreter = *static_cast(interp_ptr); auto& config = *static_cast(config_ptr); auto const& module = *static_cast(module_ptr); auto const& expression = *static_cast(expression_ptr); if (interpreter.trap_if_insufficient_native_stack_space()) return 1; config.set_frame_lightweight(module, locals_ptr, expression, arity); config.depth()++; // Set up call record for the callee if needed. if (max_call_rec_size > 0) { config.set_call_record_base(nullptr); config.setup_call_record(max_call_rec_size); } return 0; } // Thin frame pop for direct compiled-to-compiled calls. void wasm_cl_pop_frame(void* config_ptr, u32 arity); void wasm_cl_pop_frame(void* config_ptr, u32 arity) { auto& config = *static_cast(config_ptr); if (arity == 1) config.compiled_call_result_scratch() = config.value_stack().unsafe_take_last(); if (!config.label_stack().is_empty()) config.label_stack().take_last(); config.unwind_impl(); } } namespace Wasm { static RuntimeHelpers make_runtime_helpers() { return RuntimeHelpers { .call_function = bit_cast(&wasm_cl_call_function), .set_trap = bit_cast(&wasm_cl_set_trap), .memory_load8_s = bit_cast(&wasm_cl_memory_load8_s), .memory_load8_u = bit_cast(&wasm_cl_memory_load8_u), .memory_load16_s = bit_cast(&wasm_cl_memory_load16_s), .memory_load16_u = bit_cast(&wasm_cl_memory_load16_u), .memory_load32_s = bit_cast(&wasm_cl_memory_load32_s), .memory_load32_u = bit_cast(&wasm_cl_memory_load32_u), .memory_load64 = bit_cast(&wasm_cl_memory_load64), .memory_store8 = bit_cast(&wasm_cl_memory_store8), .memory_store16 = bit_cast(&wasm_cl_memory_store16), .memory_store32 = bit_cast(&wasm_cl_memory_store32), .memory_store64 = bit_cast(&wasm_cl_memory_store64), .memory_size = bit_cast(&wasm_cl_memory_size), .memory_grow = bit_cast(&wasm_cl_memory_grow), .read_global = bit_cast(&wasm_cl_read_global), .write_global = bit_cast(&wasm_cl_write_global), .stack_push = bit_cast(&wasm_cl_stack_push), .stack_pop = bit_cast(&wasm_cl_stack_pop), .stack_size = bit_cast(&wasm_cl_stack_size), .stack_cleanup = bit_cast(&wasm_cl_stack_cleanup), .callrec_read = bit_cast(&wasm_cl_callrec_read), .callrec_write = bit_cast(&wasm_cl_callrec_write), .call_with_record = bit_cast(&wasm_cl_call_with_record), .direct_call_0 = bit_cast(&wasm_cl_direct_call_0), .direct_call_1 = bit_cast(&wasm_cl_direct_call_1), .direct_call_2 = bit_cast(&wasm_cl_direct_call_2), .direct_call_3 = bit_cast(&wasm_cl_direct_call_3), .call_indirect = bit_cast(&wasm_cl_call_indirect), .memory_copy = bit_cast(&wasm_cl_memory_copy), .memory_fill = bit_cast(&wasm_cl_memory_fill), .regs_offset = static_cast(offsetof(Configuration, regs)), .value_size = static_cast(sizeof(Value)), .locals_base_offset = static_cast(Configuration::locals_base_offset()), .default_memory_base_offset = static_cast(Configuration::default_memory_base_offset()), .compiled_call_result_scratch_offset = static_cast(Configuration::compiled_call_result_scratch_offset()), }; } static CraneliftInsn serialize_insn(Dispatch const& dispatch, SourcesAndDestination const& addr) { CraneliftInsn out {}; auto const* insn = dispatch.instruction; out.opcode = insn->opcode().value(); out.sources[0] = addr.sources[0]; out.sources[1] = addr.sources[1]; out.sources[2] = addr.sources[2]; out.destination = addr.destination; out.imm1 = 0; out.imm2 = 0; out.imm3 = 0; auto const& args = insn->arguments(); u32 opc = out.opcode; if (opc == Instructions::i32_const.value()) { out.imm1 = static_cast(args.get()); } else if (opc == Instructions::i64_const.value()) { out.imm1 = args.get(); } else if (opc == Instructions::f32_const.value()) { out.imm1 = static_cast(bit_cast(args.get())); } else if (opc == Instructions::f64_const.value()) { out.imm1 = bit_cast(args.get()); } else if (opc == Instructions::local_get.value() || opc == Instructions::local_set.value() || opc == Instructions::local_tee.value()) { out.imm1 = static_cast(insn->local_index().value()); } else if (opc == Instructions::global_get.value() || opc == Instructions::global_set.value()) { out.imm1 = static_cast(args.get().value()); } else if (opc == Instructions::br.value() || opc == Instructions::br_if.value()) { auto const& br_args = args.get(); out.imm1 = static_cast(br_args.label.value()); } else if (opc == Instructions::block.value() || opc == Instructions::loop.value() || opc == Instructions::if_.value()) { auto const& struct_args = args.get(); out.imm1 = static_cast(struct_args.end_ip.value()); out.imm2 = struct_args.else_ip().has_value() ? static_cast(struct_args.else_ip()->value()) : -1; u32 arity = struct_args.meta.arity; u32 param_count = struct_args.meta.parameter_count; out.imm3 = arity | (param_count << 16); } else if (opc == Instructions::br_table.value()) { auto const& table_args = args.get(); if (table_args.default_.value() > NumericLimits::max()) { out.imm3 = 0xff; return out; } for (auto const& label : table_args.labels) { if (label.value() > NumericLimits::max()) { out.imm3 = 0xff; return out; } } // Pack: imm3 low byte = min(label_count, 8), upper bits = default label. // First 4 labels in imm1, next 4 in imm2 (16 bits each). // If more than 8 labels, continuation instructions carry the rest. auto const total_labels = table_args.labels.size(); auto const inline_count = min(total_labels, static_cast(8)); out.imm3 = static_cast(inline_count) | (static_cast(table_args.default_.value()) << 8); for (size_t i = 0; i < inline_count; ++i) { auto const encoded = static_cast(table_args.labels[i].value()) << ((i % 4) * 16); if (i < 4) out.imm1 |= static_cast(encoded); else out.imm2 |= static_cast(encoded); } } else if (opc == Instructions::call.value()) { out.imm1 = static_cast(args.get().value()); } else if (opc == Instructions::call_indirect.value()) { auto const& indirect_args = args.get(); out.imm1 = static_cast(indirect_args.type.value()); out.imm2 = static_cast(indirect_args.table.value()); } else if (opc == Instructions::memory_copy.value()) { auto const& copy_args = args.get(); out.imm1 = static_cast(copy_args.dst_index.value()); out.imm2 = static_cast(copy_args.src_index.value()); } else if (opc == Instructions::memory_fill.value()) { auto const& fill_args = args.get(); out.imm1 = static_cast(fill_args.memory_index.value()); } else if (opc >= Instructions::i32_load.value() && opc <= Instructions::i64_store32.value()) { auto const& mem_arg = args.get(); out.imm1 = static_cast(mem_arg.offset); out.imm3 = static_cast(mem_arg.memory_index.value()) | (mem_arg.memory_index.value() == 0 ? (1u << 31) : 0); } else if (opc == Instructions::memory_size.value() || opc == Instructions::memory_grow.value()) { auto const& mem_idx_arg = args.get(); out.imm1 = static_cast(mem_idx_arg.memory_index.value()); } auto is_syn = [opc](OpCode op) { return opc == op.value(); }; auto syn_between = [opc](OpCode lo, OpCode hi) { return opc >= lo.value() && opc <= hi.value(); }; if (opc >= Instructions::SyntheticInstructionBase.value()) { if (syn_between(Instructions::synthetic_call_00, Instructions::synthetic_call_31)) { out.imm1 = static_cast(args.get().value()); } else if (is_syn(Instructions::synthetic_call_with_record_0) || is_syn(Instructions::synthetic_call_with_record_1)) { out.imm1 = static_cast(args.get().value()); } else if (is_syn(Instructions::synthetic_br_nostack) || is_syn(Instructions::synthetic_br_if_nostack)) { auto const& br_args = args.get(); out.imm1 = static_cast(br_args.label.value()); } else if (is_syn(Instructions::synthetic_local_copy)) { out.imm1 = static_cast(insn->local_index().value()); out.imm2 = static_cast(args.get().value()); } else if (is_syn(Instructions::synthetic_i32_add2local) || syn_between(Instructions::synthetic_i32_sub2local, Instructions::synthetic_i32_shrs2local) || is_syn(Instructions::synthetic_i64_add2local) || syn_between(Instructions::synthetic_i64_sub2local, Instructions::synthetic_i64_shrs2local)) { out.imm1 = static_cast(insn->local_index().value()); out.imm2 = static_cast(args.get().value()); } else if (is_syn(Instructions::synthetic_i32_addconstlocal) || is_syn(Instructions::synthetic_i32_andconstlocal)) { out.imm1 = static_cast(args.get()); out.imm2 = static_cast(insn->local_index().value()); } else if (is_syn(Instructions::synthetic_i64_addconstlocal) || is_syn(Instructions::synthetic_i64_andconstlocal)) { out.imm1 = args.get(); out.imm2 = static_cast(insn->local_index().value()); } else if (is_syn(Instructions::synthetic_i32_storelocal) || is_syn(Instructions::synthetic_i64_storelocal)) { auto const& mem_arg = args.get(); out.imm1 = static_cast(mem_arg.offset); out.imm2 = static_cast(insn->local_index().value()); out.imm3 = static_cast(mem_arg.memory_index.value()) | (mem_arg.memory_index.value() == 0 ? (1u << 31) : 0); } else if (is_syn(Instructions::synthetic_local_seti32_const)) { out.imm1 = static_cast(args.get()); out.imm2 = static_cast(insn->local_index().value()); } else if (is_syn(Instructions::synthetic_local_seti64_const)) { out.imm1 = args.get(); out.imm2 = static_cast(insn->local_index().value()); } else if (syn_between(Instructions::synthetic_argument_get, Instructions::synthetic_argument_tee)) { out.imm1 = static_cast(insn->local_index().value()); } } return out; } static StringView resolve_cranelift_compiler_path() { // Lookup order: LADYBIRD_CRANELIFT_COMPILER, compile-time path, sibling-of-self. static NeverDestroyed s_path = []() -> ByteString { auto file_exists = [](ByteString const& path) { return !Core::System::stat(path).is_error(); }; if (auto const* env = getenv("LADYBIRD_CRANELIFT_COMPILER"); env && *env) { if (file_exists(env)) return ByteString { env }; } if (file_exists(WASM_CRANELIFT_COMPILER_PATH)) return WASM_CRANELIFT_COMPILER_PATH; if (auto self_path = Core::System::current_executable_path(); !self_path.is_error()) { auto sibling = LexicalPath::join(LexicalPath::dirname(self_path.value()), "cranelift-compiler"sv).string(); if (file_exists(sibling)) return sibling; } return WASM_CRANELIFT_COMPILER_PATH; }(); return s_path->view(); } static void try_cranelift_compile_batch(Vector& batch) { if (batch.is_empty()) return; static auto helpers = make_runtime_helpers(); u64 outcome_return = to_underlying(Outcome::Return); size_t function_count = batch.size(); auto const entries_offset = sizeof(InputHeader); auto const entries_size = sizeof(InputFunctionEntry) * function_count; size_t total_insn_count = 0; for (auto& entry : batch) total_insn_count += entry.insns.size(); auto const insn_region_offset = align_up(entries_offset + entries_size, alignof(CraneliftInsn)); auto const insn_bytes = total_insn_count * sizeof(CraneliftInsn); auto const helpers_offset = align_up(insn_region_offset + insn_bytes, alignof(RuntimeHelpers)); auto const code_region_start = align_up(helpers_offset + sizeof(RuntimeHelpers), alignof(OutputFunctionEntry)); auto const code_region_size = max(oop_code_region_min_size, total_insn_count * oop_code_bytes_per_insn); auto const reloc_region_start = align_up(code_region_start + sizeof(OutputFunctionEntry) * function_count + code_region_size, alignof(HelperReloc)); auto const reloc_region_size = max(oop_reloc_region_min_size, total_insn_count * oop_reloc_bytes_per_insn); auto const total_size = reloc_region_start + reloc_region_size; #if defined(AK_OS_WINDOWS) DWORD size_hi = static_cast(static_cast(total_size) >> 32); DWORD size_lo = static_cast(total_size & 0xFFFFFFFF); HANDLE section_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, size_hi, size_lo, NULL); if (!section_handle) return; SetHandleInformation(section_handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); ScopeGuard close_handle = [section_handle] { CloseHandle(section_handle); }; auto* mapping = MapViewOfFile(section_handle, FILE_MAP_ALL_ACCESS, 0, 0, total_size); if (!mapping) return; ScopeGuard unmap = [mapping] { UnmapViewOfFile(mapping); }; #elif defined(AK_OS_MACOS) // macOS lacks memfd_create; use shm_open + shm_unlink for an anonymous fd. char shm_name[] = "/libwasm-cranelift-XXXXXX"; arc4random_buf(shm_name + 21, 6); for (int i = 21; i < 27; ++i) shm_name[i] = 'A' + (static_cast(shm_name[i]) % 26); int fd = shm_open(shm_name, O_RDWR | O_CREAT | O_EXCL, 0600); if (fd < 0) return; shm_unlink(shm_name); // POSIX shm_open sets FD_CLOEXEC on the returned fd, which would close it // in the spawned cranelift-compiler child. Clear it so the child inherits. if (auto flags = fcntl(fd, F_GETFD); flags >= 0) fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC); ScopeGuard close_fd = [fd] { close(fd); }; if (ftruncate(fd, static_cast(total_size)) < 0) return; auto* mapping = mmap(nullptr, total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mapping == MAP_FAILED) return; ScopeGuard unmap = [mapping, total_size] { munmap(mapping, total_size); }; #else int fd = memfd_create("libwasm-cranelift", 0); if (fd < 0) return; ScopeGuard close_fd = [fd] { close(fd); }; if (ftruncate(fd, static_cast(total_size)) < 0) return; auto* mapping = mmap(nullptr, total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mapping == MAP_FAILED) return; ScopeGuard unmap = [mapping, total_size] { munmap(mapping, total_size); }; #endif auto* base = static_cast(mapping); __builtin_memset(base, 0, total_size); auto* header = reinterpret_cast(base); *header = InputHeader { .function_count = static_cast(function_count), .helpers_offset = static_cast(helpers_offset), .outcome_return = outcome_return, .code_region_start = code_region_start, .reloc_region_start = reloc_region_start, .total_size = total_size, }; size_t insn_cursor = insn_region_offset; for (size_t i = 0; i < function_count; ++i) { auto& input = batch[i]; auto* entry = reinterpret_cast(base + entries_offset + i * sizeof(InputFunctionEntry)); *entry = InputFunctionEntry { .insn_offset = static_cast(insn_cursor), .insn_count = static_cast(input.insns.size()), .result_arity = input.result_arity, }; __builtin_memcpy(base + insn_cursor, input.insns.data(), input.insns.size() * sizeof(CraneliftInsn)); insn_cursor += input.insns.size() * sizeof(CraneliftInsn); } __builtin_memcpy(base + helpers_offset, &helpers, sizeof(helpers)); Vector arguments; #if defined(AK_OS_WINDOWS) arguments.append(ByteString::number(reinterpret_cast(section_handle))); #else arguments.append(ByteString::number(fd)); #endif auto process_result = Core::Process::spawn({ .name = "cranelift-compiler"sv, .executable = resolve_cranelift_compiler_path(), .arguments = arguments, }); if (process_result.is_error()) return; auto status_result = process_result.release_value().wait_for_termination(); if (status_result.is_error() || status_result.value() != 0) return; // Extract results for each function. auto const code_base_offset = code_region_start + sizeof(OutputFunctionEntry) * function_count; for (size_t i = 0; i < function_count; ++i) { auto const* output = reinterpret_cast(base + code_region_start + i * sizeof(OutputFunctionEntry)); if (!output->compiled) continue; auto code_offset = static_cast(output->code_offset); auto code_size = static_cast(output->code_size); if (code_offset > code_region_size || code_size > code_region_size - code_offset) continue; auto code_start = code_base_offset + code_offset; if (code_start + code_size > total_size) continue; auto const reloc_offset = static_cast(output->reloc_offset); auto const reloc_count = static_cast(output->reloc_count); auto const reloc_bytes = reloc_count * sizeof(HelperReloc); if (reloc_count != 0 && reloc_bytes / sizeof(HelperReloc) != reloc_count) continue; if (reloc_offset > reloc_region_size || reloc_bytes > reloc_region_size - reloc_offset) continue; if (reloc_region_start + reloc_offset + reloc_bytes > total_size) continue; auto const trap_offset = static_cast(output->trap_offset); auto const trap_count = static_cast(output->trap_count); auto const trap_bytes = trap_count * sizeof(CraneliftTrap); if (trap_count != 0 && trap_bytes / sizeof(CraneliftTrap) != trap_count) continue; if (trap_offset > reloc_region_size || trap_bytes > reloc_region_size - trap_offset) continue; if (reloc_region_start + trap_offset + trap_bytes > total_size) continue; auto code_bytes = ReadonlyBytes { base + code_start, code_size }; auto const* relocs = reloc_count == 0 ? nullptr : reinterpret_cast(base + reloc_region_start + reloc_offset); auto traps = trap_count == 0 ? ReadonlySpan {} : ReadonlySpan { reinterpret_cast(base + reloc_region_start + trap_offset), trap_count }; auto& capture = cranelift_cache_state().cache_capture; if (capture.capturing && batch[i].function_index != NumericLimits::max()) { if (auto copy = ByteBuffer::copy(code_bytes.data(), code_bytes.size()); !copy.is_error()) { CacheRecord rec; rec.function_index = batch[i].function_index; rec.unpatched_code = copy.release_value(); rec.relocs.ensure_capacity(reloc_count); for (size_t j = 0; j < reloc_count; ++j) rec.relocs.unchecked_append(relocs[j]); rec.traps.ensure_capacity(trap_count); for (size_t j = 0; j < trap_count; ++j) rec.traps.unchecked_append(traps[j]); capture.records.append(move(rec)); } } install_compiled_function(*batch[i].target, code_bytes, relocs, reloc_count, traps, helpers); } } bool try_cranelift_compile(CompiledInstructions& compiled, u32 result_arity) { #if !WASM_COMPILED_FAULT_RECOVERY_SUPPORTED (void)compiled; (void)result_arity; return false; #else auto const& dispatches = compiled.dispatches; auto const& addresses = compiled.src_dst_mappings; if (dispatches.is_empty()) return false; // Already installed (either from a prior compile or a previous cache install in this // same validation pass) -- nothing to do. if (compiled.cranelift_compiled) return true; if (s_active_function_index == NumericLimits::max()) return false; // Cache hit: install from the parsed blob instead of going through cranelift. // dispatches[] has just been populated by try_compile_instructions, so handler_ptr is ready to be set. if (cranelift_cache_state().pending_install.active && s_active_function_index != NumericLimits::max()) { auto record = cranelift_cache_state().pending_install.records.take(s_active_function_index); if (record.has_value()) { static auto cache_install_helpers = make_runtime_helpers(); if (install_compiled_function( compiled, record->unpatched_code.bytes(), record->relocs.is_empty() ? nullptr : record->relocs.data(), record->relocs.size(), record->traps.span(), cache_install_helpers)) { return true; } // Put it back so we can try later. cranelift_cache_state().pending_install.records.set(s_active_function_index, record.release_value()); } } if constexpr (WASM_CRANELIFT_DEBUG) { // CRANELIFT_MAX_INSNS=N skip functions with more than N dispatches. // CRANELIFT_MIN_INSNS=N skip functions with fewer than N dispatches. // CRANELIFT_MIN_FN=N skip functions with id < N. // CRANELIFT_MAX_FN=N skip functions with id > N. // CRANELIFT_SKIP_FN=a,b,c skip listed function ids. // CRANELIFT_ONLY_FN=a,b,c only compile listed function ids. // CRANELIFT_TRACE=1 log a line per compiled function. static auto const read_size_env = [](char const* name, size_t fallback) { if (auto* env = getenv(name)) return static_cast(atol(env)); return fallback; }; static auto const read_set_env = [](char const* name) { HashTable out; auto* env = getenv(name); if (!env || !*env) return out; StringView view { env, strlen(env) }; view.for_each_split_view(',', SplitBehavior::Nothing, [&](auto part) { if (auto n = part.template to_number(); n.has_value()) out.set(n.value()); }); return out; }; static size_t s_max_insns = read_size_env("CRANELIFT_MAX_INSNS", NumericLimits::max()); static size_t s_min_insns = read_size_env("CRANELIFT_MIN_INSNS", 0); static size_t s_min_fn = read_size_env("CRANELIFT_MIN_FN", 0); static size_t s_max_fn = read_size_env("CRANELIFT_MAX_FN", NumericLimits::max()); static auto& s_skip_fn = *new HashTable(read_set_env("CRANELIFT_SKIP_FN")); static auto& s_only_fn = *new HashTable(read_set_env("CRANELIFT_ONLY_FN")); static auto& s_dump_fn = *new HashTable(read_set_env("CRANELIFT_DUMP_FN")); static bool s_trace = getenv("CRANELIFT_TRACE") != nullptr; static size_t s_func_counter = 0; size_t func_id = s_func_counter++; if (dispatches.size() > s_max_insns || dispatches.size() < s_min_insns) return false; if (func_id < s_min_fn || func_id > s_max_fn) return false; if (s_skip_fn.contains(func_id)) return false; if (!s_only_fn.is_empty() && !s_only_fn.contains(func_id)) return false; if (s_trace) warnln("cranelift: compiling fn#{} ({} dispatches)", func_id, dispatches.size()); if (s_dump_fn.contains(func_id)) { warnln("cranelift: dump fn#{} ({} dispatches)", func_id, dispatches.size()); auto reg_name = [](Dispatch::RegisterOrStack reg) -> ByteString { if (reg == Dispatch::RegisterOrStack::Stack) return "stack"; if (reg >= Dispatch::RegisterOrStack::CallRecord) return ByteString::formatted("cr{}", to_underlying(reg) - to_underlying(Dispatch::RegisterOrStack::CallRecord)); return ByteString::formatted("reg{}", to_underlying(reg)); }; for (size_t ip = 0; ip < dispatches.size(); ++ip) { auto const& dispatch = dispatches[ip]; auto const& addr = addresses[ip]; ssize_t in_count = 0; ssize_t out_count = 0; # define M(name, _, ins, outs) \ case Instructions::name.value(): \ in_count = ins; \ out_count = outs; \ break; switch (dispatch.instruction->opcode().value()) { ENUMERATE_WASM_OPCODES(M) } # undef M StringBuilder regs; regs.append('('); for (ssize_t j = 0; j < (in_count < 0 ? 3 : in_count); ++j) { if (j > 0) regs.append(", "sv); regs.append(reg_name(addr.sources[j])); } regs.append(')'); if (out_count > 0) { regs.appendff(" -> {}", reg_name(addr.destination)); } warnln(" [{:>03}] {} {} dst={}", ip, instruction_name(dispatch.instruction->opcode()), regs.to_byte_string(), reg_name(addr.destination)); } } } Vector flat; flat.ensure_capacity(dispatches.size()); for (size_t i = 0; i < dispatches.size(); ++i) { flat.append(serialize_insn(dispatches[i], addresses[i])); if (dispatches[i].instruction->opcode().value() == Instructions::synthetic_tier_up.value()) flat.last().imm1 = static_cast(i); if (dispatches[i].instruction->opcode().value() == Instructions::br_table.value()) { auto const& table_args = dispatches[i].instruction->arguments().get(); auto const total = table_args.labels.size(); for (size_t base = 8; base < total; base += 8) { CraneliftInsn cont {}; cont.opcode = Instructions::synthetic_br_table_cont.value(); auto const chunk = min(total - base, static_cast(8)); cont.imm3 = static_cast(chunk); for (size_t j = 0; j < chunk; ++j) { auto const encoded = static_cast(table_args.labels[base + j].value()) << ((j % 4) * 16); if (j < 4) cont.imm1 |= static_cast(encoded); else cont.imm2 |= static_cast(encoded); } flat.append(cont); } } } cranelift_cache_state().pending_batch.append({ move(flat), result_arity, s_active_function_index, &compiled }); return false; // Not compiled yet, will be compiled in flush. #endif } void flush_cranelift_batch() { if (cranelift_cache_state().pending_batch.is_empty()) return; try_cranelift_compile_batch(cranelift_cache_state().pending_batch); cranelift_cache_state().pending_batch.clear(); } void discard_cranelift_batch() { cranelift_cache_state().pending_batch.clear(); } void free_cranelift_code(void* handle) { if (handle) { auto* mapping = static_cast(handle); #if defined(AK_OS_WINDOWS) VirtualFree(mapping->mapping, 0, MEM_RELEASE); #else munmap(mapping->mapping, mapping->size); #endif delete mapping; } } void set_cranelift_active_function_index(u32 function_index) { s_active_function_index = function_index; } void begin_cranelift_cache_capture() { cranelift_cache_state().cache_capture.capturing = true; cranelift_cache_state().cache_capture.records.clear(); } void abort_cranelift_cache_capture() { cranelift_cache_state().cache_capture.capturing = false; cranelift_cache_state().cache_capture.records.clear(); } void abort_cranelift_cache_install() { cranelift_cache_state().pending_install.active = false; cranelift_cache_state().pending_install.records.clear(); } Optional serialize_cranelift_cache_blob(ReadonlyBytes wasm_hash) { ScopeGuard reset = [] { cranelift_cache_state().cache_capture.capturing = false; cranelift_cache_state().cache_capture.records.clear(); }; auto const& capture = cranelift_cache_state().cache_capture; if (!capture.capturing || capture.records.is_empty()) return {}; if (wasm_hash.size() != 32) return {}; static auto helpers = make_runtime_helpers(); size_t total_size = sizeof(CacheBlobHeader); for (auto const& r : capture.records) { total_size += sizeof(CacheBlobFunctionEntry); total_size += align_up(r.unpatched_code.size(), 16); total_size += r.relocs.size() * sizeof(HelperReloc); total_size += r.traps.size() * sizeof(CraneliftTrap); } auto blob_or_error = ByteBuffer::create_zeroed(total_size); if (blob_or_error.is_error()) return {}; auto blob = blob_or_error.release_value(); auto* out = blob.data(); auto* header = reinterpret_cast(out); header->magic = cache_blob_magic; header->format_version = cache_blob_format_version; header->helper_count = HELPER_COUNT; header->layout_hash = compute_layout_hash(helpers); __builtin_memcpy(header->wasm_hash, wasm_hash.data(), 32); header->function_count = static_cast(capture.records.size()); size_t offset = sizeof(CacheBlobHeader); for (auto const& r : capture.records) { auto* entry = reinterpret_cast(out + offset); entry->function_index = r.function_index; entry->code_size = static_cast(r.unpatched_code.size()); entry->reloc_count = static_cast(r.relocs.size()); entry->trap_count = static_cast(r.traps.size()); offset += sizeof(CacheBlobFunctionEntry); __builtin_memcpy(out + offset, r.unpatched_code.data(), r.unpatched_code.size()); offset += align_up(r.unpatched_code.size(), 16); auto reloc_bytes = r.relocs.size() * sizeof(HelperReloc); if (reloc_bytes > 0) __builtin_memcpy(out + offset, r.relocs.data(), reloc_bytes); offset += reloc_bytes; auto trap_bytes = r.traps.size() * sizeof(CraneliftTrap); if (trap_bytes > 0) __builtin_memcpy(out + offset, r.traps.data(), trap_bytes); offset += trap_bytes; } return blob; } bool try_install_cranelift_cache_blob(ReadonlyBytes expected_wasm_hash, ReadonlyBytes blob) { abort_cranelift_cache_install(); if (expected_wasm_hash.size() != 32 || blob.size() < sizeof(CacheBlobHeader)) return false; auto const* header = reinterpret_cast(blob.data()); if (header->magic != cache_blob_magic) return false; if (header->format_version != cache_blob_format_version) return false; if (header->helper_count != HELPER_COUNT) return false; if (__builtin_memcmp(header->wasm_hash, expected_wasm_hash.data(), 32) != 0) return false; static auto helpers = make_runtime_helpers(); if (header->layout_hash != compute_layout_hash(helpers)) return false; size_t offset = sizeof(CacheBlobHeader); for (u32 i = 0; i < header->function_count; ++i) { if (offset + sizeof(CacheBlobFunctionEntry) > blob.size()) return false; auto const* entry = reinterpret_cast(blob.data() + offset); offset += sizeof(CacheBlobFunctionEntry); auto code_off = offset; auto aligned_code_size = align_up(entry->code_size, 16); if (code_off + aligned_code_size > blob.size()) return false; offset += aligned_code_size; auto reloc_off = offset; auto reloc_bytes = static_cast(entry->reloc_count) * sizeof(HelperReloc); if (reloc_off + reloc_bytes > blob.size()) return false; offset += reloc_bytes; auto trap_off = offset; auto trap_bytes = static_cast(entry->trap_count) * sizeof(CraneliftTrap); if (trap_off + trap_bytes > blob.size()) return false; offset += trap_bytes; auto code_copy = ByteBuffer::copy(blob.data() + code_off, entry->code_size); if (code_copy.is_error()) return false; CacheRecord rec; rec.function_index = entry->function_index; rec.unpatched_code = code_copy.release_value(); rec.relocs.ensure_capacity(entry->reloc_count); for (u32 j = 0; j < entry->reloc_count; ++j) { HelperReloc reloc; __builtin_memcpy(&reloc, blob.data() + reloc_off + j * sizeof(HelperReloc), sizeof(HelperReloc)); rec.relocs.unchecked_append(reloc); } rec.traps.ensure_capacity(entry->trap_count); for (u32 j = 0; j < entry->trap_count; ++j) { CraneliftTrap trap; __builtin_memcpy(&trap, blob.data() + trap_off + j * sizeof(CraneliftTrap), sizeof(CraneliftTrap)); rec.traps.unchecked_append(trap); } cranelift_cache_state().pending_install.records.set(entry->function_index, move(rec)); } cranelift_cache_state().pending_install.active = true; return true; } }