Repository navigation
Closed world optimization of wasm modules that have function types in import/export boundary #9237
Description
Let's say we have a module that can be optimized under closed world assumptions but we want to export some functions that the outside can invoke and pass callbacks of a specific type in. Concretely:
(module
(type $sig1 (func (param i32 i64 f64) (result i32)))
(type $sig2 (func (param i32 i32 i64) (result i32)))
(func $unrelated (type $sig2) (param i32 i32 i64) (result i32)
(i32.const 12345)
)
(export "getUnrelated" (func $getUnrelated))
(func $getUnrelated (result (ref $sig2))
(ref.func $unrelated)
)
;; Takes a (ref $sig1) directly from outside and calls it.
(export "callForeignUninhabited" (func $callForeignUninhabited))
(func $callForeignUninhabited (param $fn (ref $sig1)) (param $a i32) (param $b i64) (param $c f64) (result i32)
(call_ref $sig1 (local.get $a) (local.get $b) (local.get $c) (local.get $fn))
)
;; Takes a (ref $sig2) directly from outside and calls it.
(export "callForeignDevirtualized" (func $callForeignDevirtualized))
(func $callForeignDevirtualized (param $fn (ref $sig2)) (param $a i32) (param $b i32) (param $c i64) (result i32)
(call_ref $sig2 (local.get $a) (local.get $b) (local.get $c) (local.get $fn))
)
)If we compile this with
% wasm-opt -all --closed-world --traps-never-happen --gufa -Os -S repro.wat -o repro.opt.wat
we get
(module
(type $sig2 (func (param i32 i32 i64) (result i32)))
(type $sig1 (func (param i32 i64 f64) (result i32)))
(type $2 (func (result (ref $sig2))))
(type $3 (func (param (ref $sig1) i32 i64 f64) (result i32)))
(type $4 (func (param (ref $sig2) i32 i32 i64) (result i32)))
(elem declare func $unrelated)
(export "getUnrelated" (func $getUnrelated))
(export "callForeignUninhabited" (func $callForeignUninhabited))
(export "callForeignDevirtualized" (func $callForeignDevirtualized))
(func $unrelated (type $sig2) (param $0 i32) (param $1 i32) (param $2 i64) (result i32)
(unreachable)
)
(func $getUnrelated (type $2) (result (ref $sig2))
(ref.func $unrelated)
)
(func $callForeignUninhabited (type $3) (param $0 (ref $sig1)) (param $1 i32) (param $2 i64) (param $3 f64) (result i32)
(unreachable)
)
(func $callForeignDevirtualized (type $4) (param $0 (ref $sig2)) (param $1 i32) (param $2 i32) (param $3 i64) (result i32)
(i32.const 12345)
)
)So the call_ref may either be turned into an unreachable trap or devirtualized to the wrong target.
I would assume that even under --closed-world assumptions, if binaryen sees that outside world passes a function reference in that it may pass a function from a different module in (i.e. that binaryen cannot assume it knows all functions of said signature type).
Note: For the other way around, where we take the reference of a wasm function defined in our module and pass it to the outside world, we can annotate it with the (binaryen.js-called) annotation and that will prevent binaryen from changing the function type. Isn't there a mechanism for the reverse: An outside function enters our module and we call it?
/cc @tlively @kripken Is this expected? Is there a way around this?
Activity
tlively commentedon Oct 8, 2026
This is working as intended. With
--closed-world, Binaryen assumes that the outside world never creates new instances of any type, including new functions. This use case would require either open world optimizations, or a(@public)annotation (or similar) on the callback types$sig1and$sig2(which does not exist yet).As toolchains experiment with more sophisticated multi-module setups, I expect there will be increased need for some kind of new annotation here.
mkustermann commentedon Oct 8, 2026
Yeah, I would think this may be a common problem: Someone wants to build a (closed world optimized) wasm module and it has an API surface it offers to users. That API surface allows 3rd parties to supply functionality via passing their function refs to the optimized component.
tlively commentedon Oct 8, 2026
As a temporary workaround, you could export a dummy imported function with the relevant function type. The import will prevent Binaryen from thinking it knows anything about the results of indirect calls to that function type, and the export will prevent Binaryen from removing the import.
mkustermann commentedon Oct 9, 2026
One could probably provide this information in 3 different places:
call_refinstructions that can possibly call 3rd party functionsHow hard would it be to add this?
Would the binaryen team be in favor of adding this?
tlively commentedon Oct 9, 2026
Yes, I'd be in favor of improvements here. There's some old discussion about related ideas at #6965. There's some additional discussion about nuance in the open/closed world assumptions at #8754.
kripken commentedon Oct 9, 2026
I am also in favor of improvements here.
Probably the next steps are to figure out how useful this would be (to help prioritize) and to get a concrete design.
tlively commentedon Oct 9, 2026
It's unclear how hard this would be to add because the scope of things we need to add is still a little unclear. There are many different levels of assumptions you may want to make about a type, and many scopes at which those assumptions may apply.
Can a type have its contents observed externally? Its identity? Its contents/identity modulo JS conversions? Can it be allocated externally? Written externally?
For a given callback parameter / type, is it possible for another referenced function of the same type to be provided as the callback? Of a different type, with some adapter code wrapped around it? Or an export of the same or different type? Or a function we've never seen before?
stevenfontanella commentedon Oct 12, 2026
Like Thomas said we had some discussion about this in #8754, but we never went ahead with code changes for this. I'll take a look in the next week or so and propose an annotation for this here.
stevenfontanella commentedon Oct 12, 2026
@mkustermann, can you tell us more about your use case? What's the module that's compiled in
--closed-worldand how are functions flowing to it from outside? Could that function reference GC types from the module? If so, we'd have to make sure that those types are public too, or Binaryen would be free to change their structure. Currently--closed-worldcorresponds roughly to a 'one-module' case where types and references don't flow between different Wasm modules, but the goal is to relax this.So far it sounds like this case corresponds to the 'created and used externally' case described here. These functions are already public because they exist on the API boundary (i.e. even in closed world today, we know not to rewrite the function type), but currently under closed world it's assumed that references must originate from the same module. The rough idea would be to add an annotation that tell us that references can originate from outside for a given type.
[dart2wasm] Allow WasmFunction<X> in wasm import/export signatures
mkustermann commentedon Oct 14, 2026
There are several potential use cases for this:
In some sense one can approach this from both sides:
--gufawork in open world, open world public types, ...)(binaryen.js_called)annotations)