-
Notifications
You must be signed in to change notification settings - Fork 82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Defining client callback type in WIT #223
Comments
Hi, good question, thanks for asking. As a general design choice, the component model doesn't support passing first-class functions/callbacks to imports since this usually leads to cyclic leaks (due to entrained scope chain) which aren't possible to collect without cross-component GC, which we don't have (also as a design choice). This design choice was based on significant experience with this issue in browsers (which all ended up being forced into some form of complex cross-language garbage-/cycle-collection). Instead, to address the main concurrency use cases for callbacks, the plan is to express concurrency in terms of Wit-level futures and streams, which we currently emulate using resource types (e.g., see wasi-io). The nice things about futures and streams is that they maintain acyclic ownership and can also be mapped directly to many languages' native concurrency support by wit-bindgen (instead of requiring this to be done by hand). Thus, for your use case, I'd try to see if it can be re-expressed in terms of futures/streams, emulated by resource types. (Another example of this is wasi-http (which hasn't been updated yet to use the bleeding-edge resource type support yet, so it's emulating future/stream in terms of emulated resource types, but that will soon change :P ). There are also other use cases for callbacks, so happy to discuss them more here; there are other approaches for these alternative use cases. |
I might be wrong, but wouldn't reference counting work for most of the case? For example, the client can define the callback as a shared (reference counting) pointer to a closure. Then the client convert the shared pointer into a resource which has a What I think is missing here is a way for the client to import a function from host, like |
The challenge arises when the reference-counted callback holds alive the callback's closure (aka environment or scope chain), which holds (via local or global variable) a handle to the host resource that owns the callback. (E.g.: in the Web, an example is when a JS callback is stored on a DOM node where the callback function's scope chain holds a reference to the DOM node.) When this happens, you get a reference count cycle which leaks unless you additionally have a way to detect and free cycles. Browsers tried to work around this problem for years with various partial fixes for special cases, but these kept breaking down and leaking in subtle ways (this is type of problem that only shows up at scale too), ultimately making their way to general cross-language cycle collection of some sort, which we don't want. For what it's worth, there is a sort of brute force way to achieve something like what you're talking about: world guest {
import register-callback: func(callback-name: string)
import unregister-callback: func(callback-name: string)
export call-callback: func(callback-name: string, args: list<string>) -> string
} The idea being that the guest registers the name of the callback and is responsible for keeping this callback alive as long as |
Thank you for the detailed explanation. But doesn't that the reference cycle problems will come up when host and clients are allowed to exchange types with resources (I guess one can create a reference cycle if resources type pointing to each other). Also, just curious, is making JS bindings able to be specified by the component model in WIT format one of the future goals? If yes, how are we going to support this? Perhaps by allowing GC types to be specified in the component model / WIT?
I think this is the way I'm seeking. I'm wondering if we can replace There are particular reasons I don't want to go for the async route. I'm trying to add a WASM runtime to Neovim that exposes all the public APIs. With component model I only need to create the WIT files of the API functions, auto-generate a little glue code in the host side, and I don't even need to provide any glue code in the client side. But if callbacks can't be express in the component model easily, like I need to translate to async style or using the "brute force way" you mentioned, then I'll need to either wrap the host API function manually, or provide glue code or detailed binding specification in the client side. |
Hi, great questions again, thanks.
In the normal client/server relationship between two components A and B, where A imports an instance of B (through a Wit
Because Wit and the Component Model are supposed to represent a sort of fuzzy cross-language intersection of types that can be mapped "pretty well" into "most" languages, it isn't a goal to be able to express every possible JS interface in full fidelity in Wit. That being said, for concurrency, JS APIs have generally been moving away from callbacks toward promises and streams (making code much nicer when using That being said, there is another less-well-developed idea to add a form of "scoped" callback (scoped to either the call (similar to how Extrapolating from the use case you described (which sounds really cool, btw!), maybe what I'd do in the short term is have a world that exports each possible type of callback using a |
Thank you so much for this detailed discussion! I'm trying to write an API where a resource cycle between a parent and a child component would be really handy. Unfortunately, I don't think that WIT trait Channel<M, R> {
fn send(&mut self, msg: M) -> R;
} Since this type does not exist, I'm trying to understand if the resource cycle workaround you've sketched out would be possible and useful in my case.
Does this refer to a resource type from an interface that A exports?
How can the parent component's export be supplied to a child component's import? So far my understanding is that an export can only be supplied to a parent, grandparent, ... while and import can only be satisfied by a child, grandchild, ... .
Does this mean exporting the same resource under the same name as B? Thanks for your help! |
Yes, that makes sense; in a single-threaded context, callbacks and channels are pretty similar things. So it does seem like some form of scoped callback is what you'd ideally want.
Good question! In addition to being able to expose resource types to the outside world through exports, parent components can pass any local definition directly to a child they are instantiating via (component $Parent
(type $A (resource (rep i32)))
(component $Child
(import "A" (type (sub resource)))
...
)
(instance $child (instantiate $Child) (with "A" (type $A)))
) So this allows a single parent component to both supply imports to and project exports from its child, allowing certain kinds of resource cycles through the parent. Unfortunately, even in this context, there's not a way for a child component to define and export a resource type that the child also uses in the types of its imports (which is I think what you're getting after); this is due to the acyclic validation rules of instantiation and types. (Using a structural type for callbacks would avoid this circularity.) |
That's a really cool feature - thank you so much for bringing it to my attention! I think this might be enough to get my use case to work. Is there a way to instantiate this very local parent-child cycle using e.g.
I already came across this issue when I prototyped my WIT definition and have found a ... workaround for now. I essentially break the resource definition cycle by defining explicit handle records for resource imports that would create a cycle, and manually do the encoding and decoding between real resources and these pseudo-handles wherever it's needed. While this works for my small prototype, I'm definitely very interested in a more canonical approach using structural callbacks or channels (I think a single-thread channel would be what stream is to future in that you can use it multiple times?). Do you think that a structural callback (or channel) type |
Not at the moment; wasm-compose currently focuses on doing black-box composition of pre-existing components. I think to expose this sort of parent-wraps-child composition in an easily-usable manner , we'll need to add support in source language toolchains for emitting
Hypothetically yes (not right now, but after this Preview2 milestone we're currently focused on), it feels like a callback type might make sense as part of the overall concurrency story (filling in gaps left by future and stream), so it could fit in with the Preview3 focus on "async support". To be clear, though, I don't think a simple function type (incl.
and give it strong semantic guarantees, then a GC language's caller-side bindings for |
@lukewagner This was such an interesting discussion you had with the goal of getting Component Wasm into neovim by the original OP and another of making a parent Component with a child Component more tractable. Are there any updates now that Preview 2 is out and work is under way for a better async story? Small/trivial use case for context: I just found this thread because I wondered about creating a wasm module to handle a new filetype that would let a raw JSON file be displayed and edited as something richer, maybe mind-map link with nodes and edges (but within the capabilities of the neovim UI). Being able to write the wasm component once and then see it work in neovim, and perhaps a browser with appropriate additional JS glue, would be very nice. |
Glad to hear it! Indeed, work is well underway to flesh out async for Preview 3. I also continue to really like this idea of adding scoped callbacks of the form sketched above (allowing traditional-style callbacks while avoiding the cyclic leaks). Unfortunately, it seems like, given the already-large scope of Preview 3, that scoped callbacks would need to be part of the next batch of functionality (perhaps "1.0-rc) which I believe would include scoped resource handles and runtime instantiation (all of these features being subtly inter-related and thus a coherent unit of design+implementation). |
I'm trying to define a component interface in WIT format so that components (client) can pass callbacks (closures) to host. Host can then store the callback somewhere and then invoke the callback when certain event happens.
I'm a bit confuse how can I describe this using WIT format. I'm thinking of something like this:
But because I didn't export the resource (I can't do
export callback
apparently), I guess this is treated as ifcallback
is implemented by the host, which is not I want. I can change it to:But then, will
host/callback
andclient/callback
be treated as the same type?The text was updated successfully, but these errors were encountered: