Last 12 weeks · 308 commits
2 of 6 standards met
@bartlomieju: Following up on our exchange, I would offer to write a POC for js2wasm integration into v8x. Being an ahead-of-time compiler the expectation would be a smaller footprint and higher performance than QuickJS, allowing for denser packing and reduced runtime costs per function call. WebAssembly would add isolation ideal for a multi-tenant setup. After looking into it, the idea would be to compile TypeScript to WebAssembly directly, then precompile the .wasm module with Wasmtime to .cwasm for small footprint and fast cold starts and then measure the shared runtime and per instance footprint and performance compared to QuickJS. For even smaller footprint the IR could also be lowered to a native target, but WebAssembly's module sandbox is ideal for multi-tenant setups. Would that fit into your vision for v8x and how big is the current footprint and performance with QuickJS compared to V8? Are you running native QuickJS or a Wasm build and how do you isolate tenants from each other?
I've been trying to compile a minimal V8 shell to WASM. I have completed compilation once or twice, though the resulting WASM binary didn't work as expected. I'm talking about no QuickJS, no Deno core, just the raw V8 with a minimal, ideally user-defined shell. This is where I'm at now https://github.com/guest271314/v8/blob/wasm-build/.github/workflows/build-d8-wasm.yml https://github.com/guest271314/v8/actions I'm wondering if the Rust crate would make the process simpler?
Adds , a fourth backend. Draft-quality scaffold, not a working engine — 75 symbols implemented, 745 still stubbed. Why this one is different JSC, QuickJS and Hermes are interpreters: hand them a source string at runtime and they run it. Porffor is an ahead-of-time compiler — it lowers JS to C and hands that to a C compiler, with nothing interpreted or JIT-compiled. So cannot mean "compile this string now". It can only resolve source already compiled into the binary. That makes the target flattened bundles (a celld Wrangler bundle, a graph) rather than arbitrary runtime input — and it is the reason this is worth trying at all: it tests whether the "generated ABI is the portability boundary" claim holds against an engine with no runtime compiler. Build shape Follows the hermes spike: → pure-Rust stub backend, links with zero porffor dependency → build.rs runs over and links the emitted object There is no engine library to link against. We compile the runtime itself out of porffor — its heap, collector and builtins — via a new target on the porffor side. points at a porffor checkout (default ). is the ABI contract. Porffor only emits builtins a compilation actually references, so a builtin absent from the seed is absent from the linked object. Every entry costs binary size in every consumer, which is the one axis porffor is unambiguously winning on (~500 KB object for a broad builtin surface, vs ~1 MB for QuickJS static and ~48 MB for JSC). Handles Same problem QuickJS has — a porffor is , 16 bytes, not a pointer — so the same solution: handles are arena slots and the slot address is the v8 handle. The discipline differs. QuickJS arena slots own exactly one refcount. Porffor has a tracing collector, so slots must be GC roots. The arena lives on the porffor side and registers with the collector in whole blocks rather than per handle, so entering and leaving a scope costs no per-handle root bookkeeping. What works Isolate, handle scope, context, primitives, strings (including , zero-copy into porffor's heap — safe because the collector is non-moving and the handle roots the string), objects, , platform lifecycle. churns 20k allocations then forces a full GC and reads the handle back, which is the test that actually exercises the root-range wiring. Notes for review One isolate per process. Porffor emits a single global heap per binary, so isolate liveness is a process-wide , not per-thread. A second live isolate panics rather than silently sharing the heap. The smoke test serializes on its own lock instead of relying on . is reused from via — it has zero engine coupling (plain calloc/free plus the Rust allocator vtable), so a second copy would only drift. differs from the hermes generator in one way: it emits stubs for symbols implemented in link-gated files instead of dropping them outright. A symbol getting a real body for the first time therefore needs no hand-added gate. (The hermes carries a hand-written block to compensate for the old behaviour; porffor's copy has that block removed, with a comment explaining why.) Not wired into the CI matrix.** No — adding a fourth column to the dashboard felt like a separate decision. Happy to add it. Diff to existing files is purely additive and feature-gated: (+13), (+51), (+5). Nothing under or is modified. Depends on an unlanded porffor change needs a target in porffor that suppresses and emits a stable C ABI. That is not upstream yet — the stub-only build () is unaffected and works against any checkout. Next over . Porffor's side is already built (a whose call target is a C function pointer, dispatched through ), and it is what celld's 126 sites bind to.
Summary add an experimental v8-shaped backend that loads trusted Wasmtime-precompiled js2wasm modules without shipping Cranelift or a JavaScript compiler at runtime use Wasmtime 47.0.3 and share the and precompiled across instances add a reproducible V8 / QuickJS / js2wasm footprint, startup, and warm-execution benchmark adapted from Deno's benchmark cover runtime-input, compile-time-folded, and input-dependent mixed workloads, with enforced This is a proof of concept for using v8x as the host/API compatibility layer while compiling typed JavaScript/TypeScript ahead of time with js2wasm. After compilation, deployment contains Wasmtime and the precompiled artifact—not js2wasm, Binaryen, Cranelift, V8, or QuickJS. Results The companion remainder optimization moved the matched js2wasm control from 22,839.8 to 16,521.7 ns/call: 1.4× faster (27.7% less elapsed time). The optimized kernel has four guarded dynamic remainders and one guard-free statically proven direct remainder per round instead of five unconditional helper calls. Compiler PR: https://github.com/loopdive/js2wasm/pull/4412 Related to #74. Scope This proves compiler-free execution and measures the runtime tradeoffs. It is not yet a complete Deno API implementation or a general replacement for every V8 API surface. Validation Rust unit and integration tests for artifact loading, import resolution, export calls, instance isolation, and shared runtime reuse separate-process V8, QuickJS, and js2wasm measurements with rotated engine order input-dependent mixed-kernel correctness and kill-switch A/B control strict artifact generation
Summary hard-reset and fully clean newly recovered Cargo-owned submodules before applying v8x patches preserve LF checkout semantics while resetting on Windows keep normal source checkouts untouched Root cause The recovery merged in #70 can reinitialize a stale Cargo git checkout, but Git for Windows may internally retry a failed submodule clone and leave the recovered worktree partially modified. v8x then sees patch 02 as neither clean nor fully applied and aborts Deno QuickJS release packaging. The reset is limited to initialization inside Cargo-owned checkouts marked by , after stale recovery has already been selected. Validation the Windows x64 dynamic CI fixture holds stale submodule metadata open until recovery starts, then verifies recovery and link completion Deno full release CI pins this exact commit for Windows x64 and ARM64 artifact builds
Summary select the WAMR build target from the Cargo target architecture align the WAMR MSVC runtime with the Rust target feature use the portable WAMR native-call bridge on Windows ARM64, avoiding the unsupported MASM path recover stale vendored submodules only inside Cargo-owned git checkouts cover x86_64 dynamic/static CRT, native ARM64 static-CRT, and stale Cargo-checkout recovery in Windows CI link a test executable so CRT mismatches are detected inside v8x CI Root cause The Deno Windows release jobs exposed configurations the existing v8x Windows check did not cover: On native ARM64, WAMR fails to recognize the CMake processor spelling and falls back to X86_64 assembly. Once AARCH64 is selected, Visual Studio ASM_MASM integration still invokes x86 MASM, which is unsupported on the native ARM64 runner and ignores the armasm64 compiler override. On x86_64, Deno enables the Rust target feature while WAMR retained the CMake default dynamic CRT. The mixed and objects leave unresolved when a downstream executable links. Cargo cache restoration can leave a populated vendor worktree with missing or stale submodule metadata. The deferred build-script initialization then fails before compilation on both Windows and Linux. The recovery is deliberately limited to Cargo-owned checkouts marked by ; normal source checkouts are never discarded automatically. Validation The expanded Windows matrix reproduces the Deno x86_64 and ARM64 release configurations, links the library test executable, and injects stale Cargo submodule state to exercise recovery.
Repository: denoland/v8x. Description: Engine agnostic JavaScript Stars: 44, Forks: 4. Primary language: Rust. Languages: Rust (94.7%), JavaScript (2.3%), C++ (1.1%), Shell (0.9%), Typst (0.9%). License: Apache-2.0. Homepage: https://denoland.github.io/v8x/ Topics: jsc, quickjs-ng, v8, wamr. Latest release: v149.4.0-rc.4 (1mo ago). Open PRs: 2, open issues: 4. Last activity: 1mo ago. Community health: 37%. Top contributors: nathanwhit, divybot, littledivy.