Stream: wasi

Topic: How exactly does this problem work?


view this post on Zulip Nathan Petrangelo (Aug 24 2025 at 07:36):

https://github.com/WebAssembly/component-model/issues/223#issuecomment-1664258641

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 w...

view this post on Zulip Nathan Petrangelo (Aug 24 2025 at 07:36):

Sorry if this isn't a good format, I'm new here

view this post on Zulip Nathan Petrangelo (Aug 24 2025 at 08:03):

I want to understand, why strictly reference counting? And can there be a way to prevent closures from referencing host resourses?

view this post on Zulip Nathan Petrangelo (Aug 24 2025 at 08:06):

The problem seems to be when host references callback references host, making a circular dependency graph. It wouldn't be a callback if the host didn't reference it, so the other place to break the circle is preventing the callback from referencing the host.

view this post on Zulip Nathan Petrangelo (Aug 24 2025 at 08:08):

In the example given, that translates to the JS callback not being able to reference the DOM node that registers it. Is that so bad?

view this post on Zulip Andy Wingo (Aug 25 2025 at 07:01):

i think one of the points is that it is hard to know what a JS callback references; it's not just textually what is in the closure, and not even specified on a language level. (all implementations that i know of use a scope chain which may include objects not captured by the closure.)

view this post on Zulip Andy Wingo (Aug 25 2025 at 07:02):

but yes, there are a few ways to break the cycles, they are just annoying to do, and if there is a design pattern that doesn't have the tendency to make cycles, then it is better for the component model

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 07:32):

In Rust, only stuff used in a closure get moved into the closure. It's unfortunate that JavaScript has this problem.

view this post on Zulip Pat Hickey (Aug 25 2025 at 15:46):

the component model needs to be agnostic of whatever language is compiled to the wasm modules inside the component (and also whatever language the host is implemented in), and it cant protect against anything it cant determine from the arguments passed to the canonical abi functions. the component model can't "see" into a module to determine whether or not the implementation of a closure contains cycles

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 20:32):

I have an idea. What if the spec disallowed callbacks from capturing the host resource, it's up to the various implementations to enforce that for their language, and the JS implementation does it by just punting on callbacks because of the scope chains? Thereby making this Javascript's problem, and not WASI's problem, such that you specifically can't register callbacks from JS.

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 20:36):

Whereas for Rust, the tooling could detect the host resource being moved into the closure and only disallow that.

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 20:40):

You could, however, still register callbacks into JS, because those callbacks could know they are not capturing the host.

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 20:44):

So overall, JS would struggle to produce its own callbacks because of scope chains, but it could still easily work with callbacks from elsewhere.

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 20:47):

Then in the future, if a JS implementation figures out how to adhere to the spec less bluntly, that would be all it takes for JS to produce callbacks.

view this post on Zulip Pat Hickey (Aug 25 2025 at 21:28):

in general we don't design features that only work in some languages and not others, one of the big ideas of the component model is that its an abstraction boundary where one side of an interface doesn't know or care what language the implementation on the other side of the interface is written in

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 22:57):

I understand that. I don't think my idea makes one side know or care what the other side is written in. Only a component developer would have to care, if they were working with JS and were met with "sorry we don't know how to determine that your callback does not reference the host." The choice around my idea boils down to, should we let JS's difficulties keep everyone else from having the feature as well?

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 23:13):

Of course, if there was a way for JS to be included that would be ideal, but it seems to me that scope chains pose a fundamental difficulty here and it sucks for other languages to be burdened with that. It feels like right now, no one gets to have callbacks because a component might be Javascript, which doesn't quite feel in the spirit of language agnostic. I would rather have callbacks for as many languages as possible, than not at all because one language struggles with it.

view this post on Zulip Nathan Petrangelo (Aug 25 2025 at 23:36):

I'll put it another way: if the JS implementation was ever gonna support WASI callbacks, it would probably have to work something like this out first regardless, because closures capturing entire scope chains seems inherently problematic in this context. WASI callbacks, if we ever get them, should capture as little as possible. So, should the other languages be made to wait for JS, or can we let them have callbacks in the meantime while JS works it out?

view this post on Zulip Nathan Petrangelo (Aug 26 2025 at 01:46):

For context, my motivation is I want to make a UI engine centering WASM, and part of that is a signals engine inspired by SolidJS and Svelte. I need callbacks for the declaration of memos and effects, which to the best of my knowledge never need to reference their host, the signal engine.

view this post on Zulip Andy Wingo (Aug 26 2025 at 07:36):

can you implement signals in terms of futures or streams?

view this post on Zulip Nathan Petrangelo (Aug 26 2025 at 09:43):

I'd have to think on it. The basic requirement is telling the engine "do this whenever one of the signal dependencies updates". It must be declared by the component, but registered by the engine so the engine can invoke it later. I can't specify it as a regular function in the WIT because a component might declare arbitrarily many memos and effects, and I don't want the interface to limit a component to just one.

view this post on Zulip Nathan Petrangelo (Aug 26 2025 at 09:46):

I think probably the answer is no, because the engine has to invoke them, ideally synchronously. Whereas, futures and streams both have their execution driven by the component that owns them.

view this post on Zulip Andy Wingo (Aug 26 2025 at 12:31):

well, just spitballing here, if it's the sort of thing that doesn't need synchronous execution, streams sound great, and if it does need synchronous execution, i wonder if the framework could pass the writer side of a future in the stream and then wait on the reader side.

view this post on Zulip Andy Wingo (Aug 26 2025 at 12:33):

the engine could transfer control directly to the stream reader (signal consumer) and then directly back if the consumer fulfilled the promise synchronously

view this post on Zulip Nathan Petrangelo (Aug 26 2025 at 18:59):

Good suggestions. I think I need to study how async will work. For instance, will futures and streams all share a single wasm async runtime, or will some languages bring their own?

view this post on Zulip Joel Dice (Aug 26 2025 at 19:14):

In a sense, the async runtime is provided by the host, given that it's the one responsible for starting and resuming tasks, dispatching events, etc. And a guest must return control to the host in order to allow other tasks to run (i.e. this is not preemptive multithreading we're talking about; that will be a separate project).

So yes, all async tasks, futures, and streams are backed by a single runtime in the host, but each guest language will have an appropriate abstraction on top of the raw intrinsic operations implemented by the host, and that will be responsible for bridging those low-level operations to the higher-level idioms of the language (e.g. async/await in JS, Rust, Python, and C#; goroutines in Go; virtual threads in Java; etc.). That abstraction might need to do non-trivial things, so you could consider it a guest-level runtime on top of the host-level runtime.

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 01:57):

Today's breakthrough: My signal engine should register components as signal subscribers, rather than individual memos and effects. I'm still working out the details, but I think this makes sense. What I've worked out so far is components need to be able to register signals with the engine by creating streams and passing the reader to the signal engine so the engine can propagate updates, and also they need to be able to subscribe to signals from the engine, which will prompt the engine to create a new stream, subscribe its writer to the designated signal, and return its reader to the component. From there, I think these mechanisms should plug into a component's internal signals library.

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 01:59):

On the engine's part, it needs to receive and store stream readers, subscribe new stream writers to them, and pass readers to the components requesting them.

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 02:02):

A signal, in the engine, will consist of one stream reader and a collection of subscribed stream writers. The engine will pass on everything from the reader to all of the writers. In the end, a WASI signal engine is basically a stream mediator.

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 02:12):

A key insight is that a component should only have one stream reader for a given foreign signal, meaning from the signal's pov, every subscribed stream corresponds to a single component.

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 02:15):

Normally, a signal library implicitly subscribes to a signal the first time its getter is called with a global variable holding the context. This works for native signals, but foreign signals must be brought into scope before they can be used, which makes that the natural time to create the subscription instead.

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 02:18):

It's looking like this will result in a component's internal signals library needing a new signal type in addition to signal, memo, and effect for this kind of foreign signal.

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 05:12):

I prototyped out a possible importSignal() function. Not sure what the stream API will actually look like in JS though

function importSignal(id) {
  let stream = Engine.subscribe(id);
  const observers = [];
  const getter = () => {
    if (current && !observers.includes(current)) {
      observers.push(current);
    }
    return stream.pop(); // Guessing at stream API
  };
  // Extra logic needed for calling all the observers on a stream event.
  // Depends on stream API.
  return getter;
}

view this post on Zulip Nathan Petrangelo (Aug 27 2025 at 05:15):

Say, are there any plans to support generics in WIT? I sure would like to let Engine.subscribe() here return stream<T>.

view this post on Zulip Lann Martin (Aug 27 2025 at 12:40):

Some discussion here: https://github.com/WebAssembly/component-model/issues/543

Doing a search in the github issues, I have yet to see a ticket where anyone has explicitly asked for this yet. Apparently, there has been discussion on something similar to this, but from what I c...

Last updated: Dec 06 2025 at 06:05 UTC