Skip to content

Closed world optimization of wasm modules that have function types in import/export boundary #9237

Open

Description

@mkustermann

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

  1. tlively commented on Oct 8, 2026

    @tlively
    Member

    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 $sig1 and $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.

  2. mkustermann commented on Oct 8, 2026

    @mkustermann
    ContributorAuthor

    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.

  3. tlively commented on Oct 8, 2026

    @tlively
    Member

    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.

  4. mkustermann commented on Oct 9, 2026

    @mkustermann
    ContributorAuthor

    or a (@public) annotation (or similar) on the callback types $sig1 and $sig2 (which does not exist yet).

    One could probably provide this information in 3 different places:

    • In the functions being imported (if they return function types), functions being exported (if parameters allow callers to pass function types)
    • In the actual call_ref instructions that can possibly call 3rd party functions
    • In the function signatures

    How hard would it be to add this?
    Would the binaryen team be in favor of adding this?

  5. tlively commented on Oct 9, 2026

    @tlively
    Member

    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.

  6. kripken commented on Oct 9, 2026

    @kripken
    Member

    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.

  7. tlively commented on Oct 9, 2026

    @tlively
    Member

    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?

  8. stevenfontanella commented on Oct 12, 2026

    @stevenfontanella
    Member

    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.

  9. stevenfontanella commented on Oct 12, 2026

    @stevenfontanella
    Member

    @mkustermann, can you tell us more about your use case? What's the module that's compiled in --closed-world and 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-world corresponds 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.

  10. mkustermann commented on Oct 14, 2026

    @mkustermann
    ContributorAuthor

    There are several potential use cases for this:

    • Allow fully optimized Dart apps to expose wasm APIs
    • For development mode (where we want fast recompilation of apps):
      • Fully optimized dart core library wasm file (perf is important and corelibs don't change) & modularly unoptimized application libraries (for fast incremental compilation)
      • Have a fully optimized dart runtime app (e.g. containing dart program metadata, dart runtime type system, etc) offering an API to load modularly compiled dart libraries.

    In some sense one can approach this from both sides:

    • make the wasm-opt open world passes do more optimizations so code quality becomes more like closed world but it doesn't break multi module scenarios (e.g. work for making --gufa work in open world, open world public types, ...)
    • make the wasm-opt closed world mode allow annotations to declare where interaction with other modules happens (e.g. the added (binaryen.js_called) annotations)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions