Skip to content

build: add emscripten/wasm32 support - #462

Merged
jow- merged 4 commits into
masterfrom
emscripten-support
Oct 2, 2026
Merged

jow- merged 4 commits into
masterfrom
emscripten-support

Conversation

@jow-

@jow- jow- commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Adds build support for compiling ucode to WebAssembly via emscripten, plus a few related fixes:

  • build: add emscripten/wasm32 CMake support — cmake -B build-wasm -DCMAKE_TOOLCHAIN_FILE=cmake/emscripten.cmake builds libucode.a and ucode-modules.a for wasm32. The CLI, udbg, plugin modules, unit tests and examples are skipped; json-c and libmd are fetched and built as external projects since the emscripten sysroot lacks them. Modules are selected via UCODE_WASM_MODULES (default: io math struct fs zlib resolv socket digest), each with its uc_module_init renamed to uc_module_init_<name> so several can be linked into one executable.
  • build: add minimal emscripten/wasm32 build guards — treat __EMSCRIPTEN__ like __linux__ in internal/platform.h, mark the uc_module_entry() trampoline weak (multiple-definition error when linking several modules statically).
  • source: initialize runpath in uc_source_new_buffer — the buffer constructor left runpath uninitialized, so module path resolution and include() saw garbage for buffer-based sources, and the destructor's free() of the "foreign" runpath was a heap corruption waiting to happen.
  • vm: clear a pending exception when entering uc_vm_execute — an unhandled exception left vm->exception set, so the next uc_vm_execute() on that VM unwound immediately and returned ERROR_RUNTIME forever, retiring the VM for the life of the process. uc_vm_call() already cleared it on entry; uc_vm_execute() now matches it.

An unhandled exception leaves vm->exception set on the way out of
uc_vm_execute_chunk(), and nothing clears it afterwards. The next
uc_vm_execute() on that VM reaches the exception check before it retires
a single instruction, unwinds immediately and returns ERROR_RUNTIME,
having handed the exception handler the message from the previous run.
Every call after that does the same, so one uncaught error retires the VM
for the life of the process.

uc_vm_call() is the other way into the interpreter and already clears the
exception on entry for this reason; uc_vm_execute() now matches it. There
was no way for an embedder to do this itself: uc_vm_clear_exception() is
static and nothing equivalent is exported.

The ucode binary executes one program and exits, so it never sees this.
It is embedders that run several scripts against one long-lived VM that
lose it, and the failure is quiet, because the reported error is a real
one from earlier rather than anything the current script did.

Signed-off-by: John Crispin <john@phrozen.org>
@jow-
jow- force-pushed the emscripten-support branch 4 times, most recently from 3d6139d to baa1d11 Compare October 2, 2026 20:09
jow- added 3 commits October 2, 2026 22:14
Two small guards so the core compiles and links under emscripten:

- internal/platform.h: treat __EMSCRIPTEN__ like __linux__ for the
  <endian.h>/<sys/sysmacros.h> include branch (both are provided by
  the emscripten sysroot)

- module.h: mark the uc_module_entry() trampoline weak. It is defined
  in every module object via this header, so linking several modules
  into a single executable (static/wasm builds) previously failed with
  a multiple-definition error; the linker now keeps one copy. The
  dlopen() path is unaffected (the symbol stays exported).

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
uc_source_new_file() sets runpath to the filename, but the buffer
constructor left it uninitialized, so module path resolution
(uc_compiler_canonicalize_path) and include() saw garbage for
buffer-based sources, and the free() of the "foreign" runpath in the
source destructor was a heap corruption waiting to happen.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Build libucode.a and ucode-modules.a for wasm32:

  cmake -B build-wasm -DCMAKE_TOOLCHAIN_FILE=cmake/emscripten.cmake

libucode is built as a static library (no dlopen under wasm) and the
CLI, udbg, plugin modules, unit tests and examples are skipped. The
default LIB_SEARCH_PATH is "./*.uc"; embedders extend
REQUIRE_SEARCH_PATH at runtime.

ucode-modules.a contains the modules selected via UCODE_WASM_MODULES
(default: io math struct fs zlib resolv socket digest), each compiled
with uc_module_init renamed to uc_module_init_<name> so several can be
linked into one executable. The embedder calls the renamed inits
itself (e.g. to preload the modules into the global modules
dictionary) and lists the names in config->force_dynlink_list, so the
compiler resolves them at compile time and the runtime require() cache
hits the preloaded objects.

The emscripten sysroot ships neither json-c nor libmd, so
cmake/emscripten-deps.cmake fetches and builds both as external
projects (only included for the wasm build; toolchain files cannot
host this because they are re-included by every try_compile() probe):

- json-c 0.18 via its own CMake build, with -Wno-macro-redefined
  (math_compat.h redefines NAN/INFINITY/isnan/isinf) and
  -DHAVE_SNPRINTF=1 (its snprintf check is gated on UNIX/MINGW/
  CYGWIN, which the Generic system does not match)

- libmd 1.0.4 via the cmake/deps/libmd wrapper, since upstream is
  autotools-only. The wrapper compiles the hash sources directly with
  a minimal config.h and replicates the autotools expansion of the
  helper.c template (one translation unit per algorithm) for the
  one-shot <ALGO>Data() functions.

Per-module build details: zlib needs -sUSE_ZLIB=1 (emscripten's
sysroot zlib), socket needs -Wno-sign-compare (emscripten's
CMSG_NXTHDR macro compares size_t and ptrdiff_t). Server-side socket
APIs (bind/listen/accept) are compiled in but not supported by
emscripten; they fail at runtime.

Also un-ignore cmake/*.cmake (the *.cmake pattern previously caught
the new toolchain and dependency files).

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
@jow-
jow- force-pushed the emscripten-support branch from baa1d11 to 4e65efb Compare October 2, 2026 20:14
@jow-
jow- merged commit c70c9ec into master Oct 2, 2026
3 checks passed
@jow-
jow- deleted the emscripten-support branch October 2, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants