Skip to content

Fix instantiation failure with wazero >= v1.12 (cross-runtime CompiledModule reuse) - #50

Open
elkhantar wants to merge 1 commit into
fastschema:masterfrom
elkhantar:fix/same-runtime-compile
Open

Fix instantiation failure with wazero >= v1.12 (cross-runtime CompiledModule reuse)#50
elkhantar wants to merge 1 commit into
fastschema:masterfrom
elkhantar:fix/same-runtime-compile

Conversation

@elkhantar

@elkhantar elkhantar commented Jul 2, 2026

Copy link
Copy Markdown

Problem

With wazero v1.12.0, every qjs.New() fails:

failed to instantiate module: import func[env.jsFunctionProxy]: signature mismatch: i32i64i32i32_i64 != i32i64i32i32_i64

wazero v1.9.0 through v1.11.0 work fine; v1.12.0 is the first breaking release (bisected). This currently breaks qjs for any application whose dependency graph pulls wazero >= v1.12, and it will block Dependabot bumps like #38 once they reach v1.12.

Minimal reproduction — just qjs.New() with wazero pinned to v1.12.0:

_, err := qjs.New() // signature mismatch on env.jsFunctionProxy

Root cause

createGlobalCompiledModule compiles the QuickJS module once on a throwaway wazero.Runtime and caches the CompiledModule in a package global; New() then instantiates that global module on a different, per-instance Runtime.

The hidden coupling: Runtime.CompileModule doesn't only produce machine code — it also resolves every function type to a numeric FunctionTypeID from the compiling runtime's store and bakes that array into the CompiledModule (wazero runtime.go: "typeIDs are static and compile-time known"). Type IDs are assigned sequentially per store, in order of first registration.

Up to v1.11, import resolution compared signatures structurally (bytes.Equal on params/results), so the baked IDs were never consulted and the cross-runtime reuse worked by accident. v1.12 implemented the WebAssembly GC proposal, where structural comparison is no longer sufficient (concrete ref types), so resolveImports now compares the numeric type IDs when both modules carry them:

matched = importedModule.TypeIDs[actualTypeIdx] == m.TypeIDs[i.DescFunc]

The qjs module instance arrives with the throwaway runtime's numbering while the env host module was registered in the new runtime's store, so the same signature gets different IDs — and the error prints the structural signatures, which is why the message shows two identical strings. Instrumenting wazero confirms both sides are params=[0x7f 0x7e 0x7f 0x7f] results=[0x7e]; only the store numbering differs.

Fix

Compile the module on the runtime that instantiates it, so the baked type IDs come from the correct store. This does not reintroduce the compilation cost the global module was avoiding: all runtimes share one wazero.CompilationCache via cachedRuntimeConfig, and wazero shares the engine (and its compiled machine code) through that cache, so the per-New() CompileModule is a cache hit — only per-store type-ID resolution and module metadata work run per call.

createGlobalCompiledModule is kept as-is: it still validates the bytes, primes the shared cache, and maintains the existing hash/recompile semantics for custom QuickJSWasmBytes.

Testing

  • The full test suite passes with wazero v1.9.0 (current pin) on darwin/arm64 and the reproduction above passes with wazero v1.12.0. (One unrelated pre-existing failure on macOS, TestEvalOptions/CWDOption/deleted_working_directory, fails identically on untouched v0.0.6.)
  • Concurrent New() stress (pool of runtimes created in parallel) works under both versions.

Disclosure

This contribution was developed with AI assistance (Claude). The root-cause analysis (version bisect, wazero instrumentation) and the change itself were reviewed by me, and all testing described above was run and verified on real hardware.

Runtime.CompileModule bakes the compiling runtime's store-assigned
function type IDs into the CompiledModule. Since wazero v1.12, import
resolution compares those numeric IDs (required for the GC proposal's
concrete ref types) rather than structural signatures, so a module
compiled on the global throwaway runtime fails to instantiate on the
per-New runtime with 'signature mismatch' on identical-looking
signatures (wazero <= v1.11 compared structurally and tolerated this).

Compiling on the instantiating runtime assigns consistent type IDs.
The machine code is shared through the CompilationCache held in
cachedRuntimeConfig, so the per-runtime compile is a cache hit.
@wwqgtxx

wwqgtxx commented Jul 26, 2026

Copy link
Copy Markdown

This PR is no longer needed because the issue was actually a regression caused by wazero, and it has already been fixed upstream:
wazero/wazero@236c245

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