WebAssembly / component-model

Repository for design and specification of the Component Model
Other
933 stars 79 forks source link

Is it possible to express "anonymous host-provided functions" in WIT? #200

Closed konall closed 1 year ago

konall commented 1 year ago

Hi there,

Thanks for all the work that's gone into this initiative- the potential for WIT is very exciting! I suspect there isn't any way of achieving what I'm asking for here, but I thought I'd double-check to be sure:

I'd like to use WIT to allow my guest module to accommodate an arbitrary number of arbitrarily-named handler functions provided by the host. I would represent this in, eg: Rust, as a HashMap<String, fn()> but the analogous list<(string, func())> in WIT isn't possible due to the lack of function types.

I'm planning to work around this by specifying a named host-provided function that accepts a string parameter and is implemented by the host as an RPC, but the above scenario would be much neater for them.

Am I correct in saying there's no way to achieve this in WIT currently? As long as I can make the host's life simpler, I'd even be willing to use some trick like manually indexing functions in the WASM function table within the WASM runtime if such a scenario could be ensured via WIT- provided it's actually based on a fixture of the spec and not heuristics.

Thank you!

sunfishcode commented 1 year ago

You are correct; there's no way to achieve this in wit, other than something like the way that you identified with string identifiers.

A question that arises with these kinds of APIs though is, how does the guest know which strings to use, and what those strings mean? Do you expect to have some externally-arranged convention that tells the guest which names it might expect to find, and what their meanings are? If so, then it's worth taking a close look at that convention, because it's possible that there may be some form of interface hiding inside that convention. Wit may or may not be able to express that interface, but if it can't, it'd be interesting to understand why it can't, and what we'd need to get there.

konall commented 1 year ago

Thanks very much for clarifying that!

I think this sort of an interface is fairly specific to my use-case;

I'm making my GUI engine available to embed in any supported language by compiling it to a WASM module w/ WIT descriptor, for which users should be able to provide event handlers written in their host language. I'm just implementing the core logic (think DOM manipulations using bare React-ish functions, no JSX etc.), so users of my library will map their markup of choice to my interface, and users of those users' libraries will just write the markup as usual. So since my users will be parsing their users' markup anyway, it provides a sort of intermediary step where my users could just read the handlers into a list, tag them, and chuck them into my (currently impossible) list of functions named in my WIT file, where I could then access them as needed.

Excusing my ignorance of the WASM internals, I'd guess that it's really just the lack of something along the lines of a function type that's "missing" here (again, with the massive asterisk that I know extremely little about this topic in WASM!)

Thanks again for your help!

lukewagner commented 1 year ago

IIUC, using resource types, one alternative is for the host to expose an interface imported by the guest that looks like:

interface callbacks {
  resource callback {
    call: func(...)
  }
  get-callbacks: func() -> list<tuple<string, callback>>
}

(Once value imports were implemented, get-callbacks could be replaced by a direct import of the list.) Under the hood, call turns into an imported function named [method]callback.call taking a handle to a callback resource as the first parameter.

lukewagner commented 1 year ago

Oh, sorry, I forgot to answer the last question: the most practical issue with first-class functions is that they quickly lead to cycles between the producer and consumer (due to the entrained closure-state) which, in the context of interoperating GC and linear-memory (and GC-implemented-in-terms-of-linear-memory) languages, produces uncollectable cross-component refcount cycles. In contrast, the combination of resource type generativity and component instantiation acyclicy encourages acyclic resource ownership.

konall commented 1 year ago

Interesting- I haven't experimented with resource types at all as they haven't been implemented since I first began looking into WIT, but that looks like a nice enough pattern! Thanks for the suggestion!