A WebAssembly build of the nvc VHDL analyser/elaborator/simulator. Runs in Node.js today and in any modern browser with SharedArrayBuffer + cross-origin isolation.
Working. nvc.wasm analyses, elaborates, and simulates real VHDL designs
using the IEEE 1993 and 2008 standard libraries:
=== Counter using IEEE numeric_bit (VHDL 2008) ===
** Note: 10ns+0: count = 1
** Note: 20ns+0: count = 2
** Note: 30ns+0: count = 3
...
What works:
- VHDL 1993 standard library (
std,ieeeincludingstd_logic_1164,numeric_bit,numeric_std,math_real, thevital_*packages) - VHDL 2008 standard library, minus
IEEE.FIXED_PKG(known crash; see Limitations) - The bytecode interpreter path (
--disable-llvm), which is the only viable execution model on wasm - Threading via Emscripten pthreads (SharedArrayBuffer)
- File I/O via
NODERAWFS=1(the wasm module sees the host filesystem directly when run via Node)
What doesn't:
- VHDL 2019
STD.REFLECTIONcrashes the wasm interpreter mid-elaboration (memory access out of bounds — an interpreter bug, not a fundamental blocker) IEEE.FIXED_PKG/IEEE.FLOAT_PKG(both std-2008 and std-2019) hit a similar crash during the generic-package instantiation- Foreign C subprograms (VHPI, GHDL-style
--foreign) are disabled — there's nodlopenon wasm. The C ABI required to call them would need to be replaced with statically-linked symbols at wasm link time. - LLVM JIT and x86 JIT backends are both disabled; wasm cannot generate executable code at runtime.
The previous README said: "impossible to build nvc for [wasm] without massive changes because NVC uses dynamic linking and wasm does not support so."
That's correct in substance — nvc has four uses of dynamic linking:
- Per-design
.sogeneration at elaboration time (cgen.c,make.c). Each VHDL unit becomes a native shared library. Elaboration callsfork()to invoke the system C compiler, thendlopens the result. - LLVM JIT (
jit-llvm.c) and x86 JIT (jit-x86.c) — runtime native code generation; needsmmap(PROT_EXEC). dlopen(NULL, ...)injit-ffi.cto expose nvc's own internal symbols (theINTERNALforeign-function convention).dlopen(user.so)injit-ffi.cfor VHPI / GHDL-style foreign C.
Wasm can do exactly none of these. But nvc also has a complete bytecode
interpreter (jit-interp.c) that needs none of them. The previous attempt
tried to build the whole machine with cmake glob *.c and didn't reach the
point of discovering that ./configure --disable-llvm --without-system-cc
sidesteps blockers (1) and (2) entirely. (3) and (4) are addressed by the
patches in patches/nvc-wasm.patch.
You need:
- emsdk —
latestis fine autoconf,automake,flex,bison,pkg-configgit,curl
git submodule update --init 3rdparty/nvc
source /path/to/emsdk/emsdk_env.sh
./build.shProduces build/wasm/bin/nvc.{js,wasm} plus the compiled standard
libraries in build/wasm/lib/.
node build/wasm/bin/nvc -L build/wasm/lib -a your.vhd -e top -rFor VHDL-2008:
node build/wasm/bin/nvc --std=08 -L build/wasm/lib -a your.vhd -e top -rbuild.sh does five things, in order:
-
Applies
patches/nvc-wasm.patchto the pinned nvc submodule. The patch is small (~40 lines net) and guards wasm-incompatible code with#ifdef __EMSCRIPTEN__. Source files touched:src/jit/jit-ffi.c— stub dlopen/dlsym/dlclose for wasmsrc/jit/jit-code.c— skip__builtin___clear_cacheon wasmsrc/thread.c— disable signal-based stop-the-world on wasmsrc/util.c— routenvc_memalign/munmapthroughposix_memalign/free(mmap's partial-munmap trick doesn't work on Emscripten)thirdparty/cpustate.c— no-op stub for register capture on wasmsrc/vhpi/vhpi_user.h— include<stdint.h>on Emscriptensrc/rt/{assert,simpkg,standard,stdenv}.c— fix UB in foreign symbol signatures (some declared 1-arg but called as 2-arg; works on x86, trapped by wasm's strict indirect-call type checking)src/jit/Makemodule.am— include the generatedwasm_symtab.c
-
Generates
src/jit/wasm_symtab.cfromsrc/symbols.txt. This is a static(name → function pointer)lookup table that replacesdlsym(NULL, name)for nvc's INTERNAL foreign symbols. -
Builds wasm dependencies in
build/wasm-deps/:- Real
libzstd.a(used by nvc's compressed work-library format) - Stub
libffi.a(the wasm32 backend in upstream libffi 3.4.6 references Emscripten JS helpers that were renamed; the stub satisfies the linker and any actual call traps at runtime, which is fine because FFI is disabled)
- Real
-
Configures nvc with
emconfigure ./configure --disable-llvm --without-system-cc --host=wasm32-unknown-emscripten, passing--disable-llvm(no LLVM JIT) and--without-system-cc(no AOT-via- external-CC). Also pre-seeds a fewac_cv_func_*=nocache vars because autoconf's function probes give false positives under cross-compile. -
Runs
emmake make -k. The-klets the build continue past the two known stdlib crash points (STD.REFLECTION, IEEE.FIXED_PKG) so we still get all the working stdlib units.
.
├── 3rdparty/nvc/ # nvc submodule (pinned upstream commit)
├── patches/
│ └── nvc-wasm.patch # applied to nvc at build time
├── scripts/
│ ├── build_deps.sh # builds wasm libzstd + stub libffi
│ ├── gen_wasm_symtab.sh # generates wasm symbol lookup table
│ └── libffi_stub.c # stub libffi source
├── build.sh # top-level entry point
└── README.md
This builds on top of:
- Zhukov Georgiy's nvc_wasm, which
mapped out the dependency space and concluded it would need "massive
changes." It does — but they're tractable when you start from
--disable-llvmand don't try to recreate nvc's own autotools in CMake. - The nvc compiler by Nick Gasson, GPLv3.