Add support for wasm32-unknown-unknown - #728
Conversation
richarddd
left a comment
There was a problem hiding this comment.
I think we can clean this up a bit. Also not a 100% sure i like that we're downloading sysroot in build.rs. What does other crates do? Either gate this in a feature flag (so its opt in) or put a script in scripts. I get that it's convenient but also a bit of a side effect and a fot gun.
|
@Sytten toughts? |
Build quickjs for the bare wasm target with no wasi runtime: reuse the existing emscripten-flavored wasi defines, take headers from a pinned wasi-sysroot, link wasi-libc's pure-compute members, and supply the OS tail from a small shim. The linked module imports exactly one host function, `env.__rquickjs_host_now_us`. - sys/build.rs: wasm32-unknown-unknown branch; sha256-pinned wasi-sysroot download cached under CARGO_HOME, overridable with RQUICKJS_WASM_SYSROOT - sys/wasm-shim/shim.c: clock, UTC localtime_r, abort/assert traps, no-op stdio and stdio backends (the latter are what keep fd_write/fd_seek/ fd_close/fd_fdstat_get out of the module) - sys/src/bindings/wasm32-unknown-unknown.rs: identical ILP32 layout to the wasip1 bindings - wasm-test/: cdylib + deno harness asserting the import list and a language/containment battery
cebac5b to
4f4c5c2
Compare
|
Thanks for taking a look, I've pushed a rebase on master plus some cleanup:
On the sysroot download, I held off rather than guess. My hesitation is that One option that might address both: the SDK already downloaded contains Happy to do that, or the feature flag, or move it to a script if thats preferable. |
Description of changes
wasm32-unknown-unknown#727This engine is very useful as a sandbox, even in environments with an existing js engine like browsers and cloudflare workers.
I've used Fable to add support for
wasm32-unknown-unknown. As an application layer guy the exact solution is a bit above my pay grade, but I'm happy to iterate on the PR to add tests and make changes as required, and welcome edits from maintainers.Agent summary tldr
So the patch is: Reuse the wasi define set, point the compiler at a wasi-sysroot include tree, statically link the pure-compute members of wasi-libc (
snprintf,strtod, libm, dlmalloc), and supply the OS tail from a ~110 line shim. No quickjs source is touched.Full agent summary
Upstream PR
Branch
wasm32-unknown-unknownonmrchantey/rquickjs, based onDelSkayn/rquickjs@edfc45c(version 0.12.2).Command
Run from the repo root. The body is everything below the
## Bodyheading in this file.Title
Body
Adds
wasm32-unknown-unknownto the supported targets. A consumer adds rquickjs as a dependency and builds for the target: no feature flag, no extra crate, no wasi runtime, no local setup.The resulting module imports exactly one host function,
env.__rquickjs_host_now_us, and nothing else.Why this is a small patch
Every ifdef this relies on already exists. The wasi support in this crate was lifted from javy and works by pretending to be emscripten (
-DEMSCRIPTEN=1, plusFE_DOWNWARD/FE_UPWARDstubs, because wasi has no rounding-mode control).wasm32-unknown-unknownneeds exactly the same defines. What it additionally needs is a libc, because unlike wasi the target ships none.So the patch is: reuse the wasi define set, point the compiler at a wasi-sysroot include tree, statically link the pure-compute members of wasi-libc (
snprintf,strtod, libm, dlmalloc), and supply the OS tail from a ~110 line shim. No quickjs source is touched.What is in the patch
sys/build.rs— atarget_arch == "wasm32" && target_os == "unknown"branch that pushes the emscripten defines, adds-isystem <sysroot>/include/wasm32-wasi, compiles the shim intolibquickjs.a, and emits-L <sysroot>/lib/wasm32-wasi -lc.Sysroot acquisition — mirrors the existing
download_wasi_sdk(): curl + tar, guarded by existence checks. It fetcheswasi-sysroot-24.0.tar.gzfrom the same wasi-sdk-24 release the crate already pins, and additionally verifies a pinned sha256 (a mismatch is a hard failure). It caches under$CARGO_HOME/rquickjs-wasi-sysrootrather thanOUT_DIR, since the archive is 68MB and anOUT_DIRcache is per-profile and wiped bycargo clean.RQUICKJS_WASM_SYSROOTpoints at an existing sysroot and skips the download entirely.One header is then patched idempotently in place:
wasi/api.hself-guards with#ifndef __wasi__/#error, so that one#erroris commented out. Defining__wasi__instead would be simpler but is wrong — quickjs's__wasi__path pinsstack_limit = 0, disabling stack-overflow checking outright.sys/wasm-shim/shim.c— the OS tail. A single host clock import backsgettimeofday/clock_gettime;localtime_ris UTC (no tz database exists here);abort/__assert_failbecome__builtin_trap; the stdio surface quickjs's dump paths reach for becomes no-ops.The shim is compiled into
libquickjs.arather than its own archive, so lld resolves it to a fixpoint before it ever reaches wasi-libc, and each definition keeps the matching wasi-libc member out of the link.sys/src/bindings/wasm32-unknown-unknown.rs— a copy of the wasip1 bindings. The layout is identical (same ILP32 wasm32 ABI), and the existingbindings_env!("TARGET")selection picks it up with no other change.wasm-test/— a cdylib plus a deno harness (wasm-test/runner.ts) that asserts the import list and runs a battery through the embedded engine.Zero leaked wasi imports
A naive port leaks four:
fd_write,fd_seek,fd_close,fd_fdstat_get.--why-extracttraces all four to one chain — quickjs referencesprintf,printf.oreferences__stdout_FILE, andstdout.o's FILE initialiser names__stdout_write/__stdio_seek/__stdio_close, with__stdout_writein turn reaching__stdio_writeand__isatty.Defining those five backends in the shim keeps those members unlinked, and the module's import list becomes exactly
env.__rquickjs_host_now_us.(Two deliberate non-shims, both commented in the source:
fwrite, because musl'sfwrite.oalso defines__fwritexwhich the snprintf core needs, so a shim would collide; and__towrite, because__fwritexcalls it to arm the write buffer of the stackFILEthatsnprintfbuilds — stubbing it silently corrupts every number-to-string conversion quickjs performs.)set_max_stack_sizecaveatJS_DEFAULT_STACK_SIZEis 1MB, which is exactly the size of the wasm shadow stack, so the defaultstack_top - 1MBwraps and stack checking is lost. Embedders must callRuntime::set_max_stack_size. This is documented in the README section added by this PR. If you would prefer it handled in-crate (defaulting the limit for this target inRuntime::new) I am happy to add that.Verification
Built and run under deno 2.9.3 (rustc 1.95.0, clang 22.1.8).
WebAssembly.Module.imports()reports one import. The battery covers classes and private fields, BigInt, unicode regexp and NFC normalization, promises andasync/awaitwith a microtask drain, exact float formatting (0.1 + 0.2,Number.MAX_VALUE,toFixed,toPrecision),Dateformatting andDate.now(), and the three containment limits: awhile (true)interrupted at a 200ms deadline, an allocation bomb contained by a 64MB memory limit, and deep recursion surfacing as a catchableRangeErrorunder a 256KB max stack size.typeof fetch/typeof Deno/typeof requireare allundefinedinside the engine. All 32 cases pass.The native workspace
cargo checkis unaffected; every change is behind the target check.Checklist