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_indirectthrough an untypedfuncreftable 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_imagehelper:
- the table is immutable in the #13445 sense — defined in this module, not exported, and no instruction can write to it;
- its initial contents were fully precomputed from element segments into the static image that
finalize_table_initalready builds; and- no element segment was deferred to instantiation time.
finalize_table_initfolds 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
StaticMatchpath 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
- new disas pair: the uniform immutable table loses the check; the identical module with one
table.setkeeps ittests/misc_testsuite/immutable-table-call-indirect.wastruntime coverage, including the deferred-segment cases (wrong-signature deferred entry still traps; deferred null write still traps) and mutated-table counterparts where nothing may be elidedcrates/environ/tests/table_mutability.rsunit tests for the image conditionsindirect-call-no-caching.watgets atable.setadded to its module so it keeps demonstrating the full non-cached dispatch sequence it was written to pin
matthargett updated PR #13909.
github-actions[bot] added the label wasmtime:api on PR #13909.
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_indirectthrough an untypedfuncreftable 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_imagehelper:
- the table is immutable in the #13445 sense — defined in this module, not exported, and no instruction can write to it;
- its initial contents were fully precomputed from element segments into the static image that
finalize_table_initalready builds; and- no element segment was deferred to instantiation time.
finalize_table_initfolds 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
StaticMatchpath 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
- new disas pair: the uniform immutable table loses the check; the identical module with one
table.setkeeps ittests/misc_testsuite/immutable-table-call-indirect.wastruntime coverage, including the deferred-segment cases (wrong-signature deferred entry still traps; deferred null write still traps) and mutated-table counterparts where nothing may be elidedcrates/environ/tests/table_mutability.rsunit tests for the image conditionsindirect-call-no-caching.watgets atable.setadded to its module so it keeps demonstrating the full non-cached dispatch sequence it was written to pin
:cross_mark: matthargett closed without merge PR #13909.
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