Stream: git-wasmtime

Topic: wasmtime / PR #13909 cranelift: elide the call_indirect s...


view this post on Zulip Wasmtime GitHub notifications bot (Jul 20 2026 at 21:34):

matthargett opened PR #13909 from rebeckerspecialties:call-indirect-sig-elision to bytecodealliance:main:

Stacked on #13445 — the first three commits here are that PR (the table-mutability analysis and the constant-bound change). Only the last commit is new; happy to rebase once #13445 lands.

What this does

WebAssembly's call_indirect through an untyped funcref table performs a runtime signature check on every call: load the callee's type id, compare it against the type the call site declared, trap on mismatch. This PR drops that check when it can be decided at compile time that it never fails: every function the table can ever contain has exactly the signature the call site expects.

That's only knowable when the table's contents are fixed for the lifetime of every instance, so the proof has three parts, all owned by a new Module::static_funcref_image helper:

  1. the table is immutable in the #13445 sense — defined in this module, not exported, and no instruction can write to it;
  2. its initial contents were fully precomputed from element segments into the static image that finalize_table_init already builds; and
  3. no element segment was deferred to instantiation time. finalize_table_init folds segments into the image only up to the first one it can't apply statically (a dynamic offset or expressions-form elements); anything after that is applied when the instance is created, so the image alone doesn't describe the table's runtime contents. This is the soundness bug from the earlier combined PR mentioned in the #13445 thread: without this condition, a deferred entry with a different signature would be reachable through an elided check. Regression tests for it are at all three levels (runtime *.wast, the analysis unit tests, and the condition living inside the helper so no call site can skip it).

When the proof holds, the call site takes the same StaticMatch path that typed-funcref tables already use. Null slots keep their check — a null entry still traps as before.

Who has this table shape

Uniform-signature tables are rare in big mixed C/C++ modules (an Emscripten sqlite3 build has ~25 signatures across ~620 table entries) but common in single-language toolchain output: in my benchmark corpus, 5 of the 8 table-bearing modules are uniform — both AssemblyScript builds, both Porffor builds (Porffor routes every indirect call through one calling-convention signature by design), and a Rust library. tests/disas/readonly-funcrefs.wat — the tracking test for exactly this never-written-table pattern (#8195) — now shows the check gone.

On native backends this removes a load, compare, and trap per dispatch. On Pulley it removes interpreted dispatches per call, which is the motivating use case (App Store platforms, where JIT isn't an option).

Tests

view this post on Zulip Wasmtime GitHub notifications bot (Jul 20 2026 at 21:42):

matthargett updated PR #13909.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 20 2026 at 23:03):

github-actions[bot] added the label wasmtime:api on PR #13909.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 21 2026 at 02:11):

matthargett edited PR #13909:

Stacked on #13445 — the first three commits here are that PR (the table-mutability analysis and the constant-bound change). Only the last commit is new; happy to rebase once #13445 lands.

What this does

WebAssembly's call_indirect through an untyped funcref table performs a runtime signature check on every call: load the callee's type id, compare it against the type the call site declared, trap on mismatch. This PR drops that check when it can be decided at compile time that it never fails: every function the table can ever contain has exactly the signature the call site expects.

That's only knowable when the table's contents are fixed for the lifetime of every instance, so the proof has three parts, all owned by a new Module::static_funcref_image helper:

  1. the table is immutable in the #13445 sense — defined in this module, not exported, and no instruction can write to it;
  2. its initial contents were fully precomputed from element segments into the static image that finalize_table_init already builds; and
  3. no element segment was deferred to instantiation time. finalize_table_init folds segments into the image only up to the first one it can't apply statically (a dynamic offset or expressions-form elements); anything after that is applied when the instance is created, so the image alone doesn't describe the table's runtime contents. This is the soundness bug from the earlier combined PR mentioned in the #13445 thread: without this condition, a deferred entry with a different signature would be reachable through an elided check. Regression tests for it are at all three levels (runtime *.wast, the analysis unit tests, and the condition living inside the helper so no call site can skip it).

When the proof holds, the call site takes the same StaticMatch path that typed-funcref tables already use. Null slots keep their check — a null entry still traps as before.

Who has this table shape

Uniform-signature tables are rare in big mixed C/C++ modules (an Emscripten sqlite3 build has ~25 signatures across ~620 table entries) but common in single-language toolchain output: in my benchmark corpus, 5 of the 8 table-bearing modules are uniform — both AssemblyScript builds, both Porffor builds (Porffor routes every indirect call through one calling-convention signature by design), and the xmrsplayer Rust library. tests/disas/readonly-funcrefs.wat — the tracking test for exactly this never-written-table pattern (#8195) — now shows the check gone. Note that I brought AssemblyScript and Porffor into the solve matrix for this feature by request of the wasmtime team in the chat.

On native backends this removes a load, compare, and trap per dispatch. On Pulley it removes interpreted dispatches per call, which is the motivating deployment scenario for me (App Store platforms, where JIT isn't an option).

Tests

view this post on Zulip Wasmtime GitHub notifications bot (Jul 24 2026 at 03:46):

:cross_mark: matthargett closed without merge PR #13909.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 24 2026 at 03:46):

matthargett commented on PR #13909:

Per the discussion on #13445 , this work stream (and the product goal it supports) isn't a good match for the wasmtime project. Closing.


Last updated: Jul 29 2026 at 05:03 UTC