Stream: C#/.net-collaboration

Topic: wit-bindgen async/future


view this post on Zulip Scott Waye (Jul 20 2025 at 17:37):

Hi, is it too early to start adding support for async and future in the c# code gen ? I see there are tests already in the main branch?

view this post on Zulip Joel Dice (Jul 20 2025 at 20:45):

No, not too early -- now is a great time to get started. I was planning to work on it myself but haven't had a chance yet. Happy to answer any ABI-related questions if you're ready to take a stab at it.

view this post on Zulip Scott Waye (Jul 21 2025 at 15:14):

Thanks I'll make a start.

view this post on Zulip Scott Waye (Jul 23 2025 at 01:04):

@Joel Dice hi, might take me a few questions to get in the right space with this. For
https://github.com/bytecodealliance/wit-bindgen/blob/454d6885b5b54adb3601a9b80ca9ff64f6ed7b8d/tests/codegen/futures.wit#L6
We have a function that takes a future as a parameter. The import of this function sounds like it will in c#, take a Task and lower it to an i32. But what can the receiver of this i32 do with it? I would expect that it could await the future, but how would it do that?

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Scott Waye (Jul 23 2025 at 02:06):

For this single import I think I also need to provide a couple of extra functions , waitable-set-wait, that I must call before the calling the import, and provide a future-poll that the runtime can call to check the state of the future, is that sort of right?

view this post on Zulip Scott Waye (Jul 25 2025 at 00:00):

Think I've confused future and async I will start with just async as that looks simpler

view this post on Zulip Joel Dice (Jul 25 2025 at 14:54):

Sorry for the late reply -- I was out on vacation for a few days.

To call an imported function that takes a future, the guest needs to first call future.new, which returns a pair of i32 handles -- one representing the writable end (roughly equivalent to a TaskCompletionSource) and the other representing the readable end (roughly equivalent to the Task corresponding to the TaskCompletionSource). Then it can pass the readable end to the function and later write a value to the writable end when such a value is available.

In the Rust bindings, we represent the writable end as a FutureWriter and the readable end as a FutureReader, which implements the std::future::Future trait, allowing it to be awaited.

Anyway, I agree that you'll probably want to start with just supporting the async import and export ABIs before moving on to future and stream support. Note that a call to an async-lowered import will either return a result immediately or return BLOCKED with a subtask handle representing the status of the call. That handle may be added to a waitable-set, which in turn may be waited on using either waitable-set.wait or by returning CALLBACK_CODE_WAIT from the async-lifted-with-callback export function that the host originally called the guest on. The latter is generally preferable when using the callback-based ABI, since it allows the host to make other concurrent calls to the guest.

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen
A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Scott Waye (Jul 25 2025 at 23:49):

For every async function we generate a [async-lift].... and a [callback][async-lift] for those 2 scenarios you describe ?

view this post on Zulip Scott Waye (Jul 25 2025 at 23:52):

on the export side I'm looking at first

view this post on Zulip Joel Dice (Jul 26 2025 at 09:28):

Yes, and the signature of the [async-lift]... function will take the same set of parameters the sync version would, but return an i32 representing the "callback code". The [callback][async-lift]... function will take 3 i32 parameters (the first is the event type, and the meaning of the rest depends on the event type) and return an i32 callback code. Either the [async-lift]... or the [callback][async-lift]... function will return its value to the caller by calling the task.return function, whose signature depends on the WIT-level return type of the function being exported.

One way to get a feel for all this is to examine the output of the C bindings generator for various async-lifted exports.

view this post on Zulip Scott Waye (Jul 26 2025 at 13:21):

Thanks very much, I should be able to get the first tests passing from here.

view this post on Zulip Scott Waye (Jul 26 2025 at 19:20):

are you using a locally built clang to get support for async in wit ?

view this post on Zulip Scott Waye (Jul 26 2025 at 19:20):

Ive got

C:\github\wit-bindgen\target\artifacts\simple-import-params-results\test-csharp\bindings>"c:\github\wasi-sdk25/bin/clang" @obj\Debug\net9.0\wasi-wasm\native\link.rsp "
error: unable to add component type "TestWorld_component_type.wit"

Caused by:
    0: expected keyword `func`, found an identifier
            --> TestWorld_component_type.wit:4:17
             |
           4 |   one-argument: async func(x: u32);
             |                 ^
clang: error: linker command failed with exit code 1 (use -v to see invocation)

view this post on Zulip Joel Dice (Jul 26 2025 at 19:28):

I believe clang is calling wasm-component-ld, which is producing that error. WASI-SDK's version of wasm-component-ld isn't new enough to understand async, so you'll want to replace the one in WASI-SDK with the latest release (which isn't using the very latest wasm-tools, but it's only one version behind, so should be new enough).

Command line linker for creating WebAssembly components - Release v0.5.15 · bytecodealliance/wasm-component-ld

view this post on Zulip Joel Dice (Jul 26 2025 at 19:29):

I've been using wasm-tools directly to convert modules to components and/or embed component type custom sections, so I haven't tried using wasm-component-ld yet -- hopefully it should just work.

view this post on Zulip Scott Waye (Jul 26 2025 at 19:30):

lets see :-)

view this post on Zulip Scott Waye (Jul 26 2025 at 20:01):

that links at least. What is the significance of [export] in this c
__attribute__((__import_module__("[export]a:b/i"), __import_name__("[task-return][async]one-argument")))

view this post on Zulip Scott Waye (Jul 26 2025 at 20:06):

I understand that this is the callback (task-return) for returning the async result, so it needs some specially handling by the linker I guess.

view this post on Zulip Joel Dice (Jul 26 2025 at 20:24):

yeah, that's just the convention we made up and which wit-component recognizes as the import module to expect the task-return function to be imported from. We use the [export] prefix to indicate that, although this is an import, we're doing the import on behalf of a specific exported function.

view this post on Zulip Scott Waye (Jul 27 2025 at 02:50):

thanks, this can all wait until Monday, I'm just asking today. I have this error

------ Failure: simple-import-params-results --------
  component: test
  path: tests\runtime-async\async\simple-import-params-results\test.cs
  error: failed to compile component "tests\\runtime-async\\async\\simple-import-params-results\\test.cs"

  Caused by:
      0: compiler produced invalid wasm file "C:\\github\\wit-bindgen\\.\\target\\artifacts\\simple-import-params-results\\test-csharp.wasm"
      1: lowered result types `[]` do not match result types `[I32]` of core function 140 (at offset 0xfdd769)
1 tests FAILED

Which seems like I've got the [async-lower] or [async-lift] wrong, but they are both int32 (int32) for a void async that takes an int32 which I think is right and the same as the c code gen. Is there any modification of signatures that goes on wasm-component-ld or wasmtime ?

view this post on Zulip Scott Waye (Jul 27 2025 at 17:49):

its a bit weird becuse function 140 is unrelatead

   (func $"Thread::ResetCachedTransitionFrame()" (;140;) (type 0) (param i32)

view this post on Zulip Scott Waye (Jul 27 2025 at 21:49):

oh, Im reading the alias wrong, but I still don't understand how this works. The core function for this needs to return the callback code so it will have a result type of i32. Is it the canon lift on the alias that is incorrect perhaps

(type (;49;) (func (param "x" u32)))
  (alias core export 7 "[async-lift]a:b/i#[async]one-argument" (core func (;140;)))
  (func (;41;) (type 49) (canon lift (core func 140) async))
  (component (;0;)
    (type (;0;) (func (param "x" u32)))
    (import "import-func-one-argument" (func (;0;) (type 0)))
    (type (;1;) (func (param "x" u32)))
    (export (;1;) "[async]one-argument" (func 0) (func (type 1)))
  )
  (instance (;20;) (instantiate 0
      (with "import-func-one-argument" (func 41))
    )
  )

view this post on Zulip Scott Waye (Jul 27 2025 at 22:25):

I' m going to dig further into wasm-component-ld as I suspect something there currently. I'm probably wrong, but need to see if what you do , generating the sections manually, fixes things

view this post on Zulip Joel Dice (Jul 28 2025 at 14:00):

If you can point me to a branch with your changes and instructions for reproducing the issue, I can take a look.

view this post on Zulip Scott Waye (Jul 28 2025 at 14:18):

Thanks very much, I wonder if its because I've got 2 wits in the link, haven't investigated that yet.
https://github.com/yowl/cs-wit-bindgen/tree/csharp-async-simple

Download wasi sdk 25 and replace wasm-component-ld, Im using a locally built one from main yesterday.
Then cargo build
and

cargo run test --languages csharp tests\runtime-async --artifacts .\target\artifacts --rust-wit-bindgen-path .\crates\guest-rust --runner "wasmtime -W component-model-async"
Testing wit-bindgen for c# and NativeAOT-LLVM. Contribute to yowl/cs-wit-bindgen development by creating an account on GitHub.

view this post on Zulip Joel Dice (Jul 28 2025 at 14:19):

Thanks. I've got a bunch of meetings this morning, but I'll dig in later today.

view this post on Zulip Scott Waye (Jul 28 2025 at 14:19):

I'm not totally stuck yet, I can do more research later if you have better things to do.

view this post on Zulip Joel Dice (Jul 28 2025 at 18:15):

Okay, I reproduced this on my end. It looks like wit-component is getting confused and generating an async lift without a callback because the core module is not exporting a callback function. It should be rejecting the module when it sees that you're using the with-callback ABI but not providing a callback, but instead it produces a component that fails validation. I'll fix the wit-component issue so we can catch the problem there (and hopefully provide a clearer diagnostic).

Meanwhile, maybe you can dig into why the generated C# code is not exporting a callback function.

view this post on Zulip Scott Waye (Jul 28 2025 at 18:17):

Ah thanks, right, I haven't implemented that bit yet as I don't think it will be on the call path yet. But definitely I need to do it, so might as well be now

view this post on Zulip Scott Waye (Jul 29 2025 at 14:03):

Great, that seesm to be passing the first test. Thanks very much.

view this post on Zulip Scott Waye (Jul 31 2025 at 02:11):

WIth an async function that returns a value, not void, I understand that it takes an extra pointer parameter is passed . How does that relate to the task-return or the pointer is nothing to do with the return value, but is just used by the runtime? What should this pointer point to ?

view this post on Zulip Joel Dice (Jul 31 2025 at 13:50):

Are you referring to the async lower ABI? If so, yes, the core signature of an async lowered function will include an extra parameter if the component-level signature returns a value, and that must point to an allocation in linear memory which is aligned and sized correctly for the result to be stored. task-return isn't relevant here because we're talking about lowering imports, not lifting exports.

The async lift ABI has no such result pointer parameter, since task-return is used to return the result, if any. The main thing to keep in mind is that the lift and lower ABIs are different.

view this post on Zulip Scott Waye (Jul 31 2025 at 19:56):

Thank you!

view this post on Zulip Scott Waye (Aug 01 2025 at 23:01):

I see the codegen tests use the waitable-... way of returning , is the [task-return]flow well tested ?

view this post on Zulip Scott Waye (Aug 02 2025 at 00:05):

ignore that, I see the c tests use it.

view this post on Zulip Scott Waye (Aug 15 2025 at 22:43):

I have a question about the c test for simple-future. There are 2 functions in the wit
https://github.com/bytecodealliance/wit-bindgen/blob/eaf42f6c12d9cca8231d5a361d20b6c21c2a2739/tests/runtime-async/async/simple-future/test.wit#L4-L5
Looking at the test for drop-future where it writes to the writer:
https://github.com/bytecodealliance/wit-bindgen/blob/eaf42f6c12d9cca8231d5a361d20b6c21c2a2739/tests/runtime-async/async/simple-future/runner.c#L34
It calls test_future_void_write which is generated as

runner_waitable_status_t test_future_void_write(test_future_void_writer_t writer) {
  return test_future_void__write(writer, NULL);
}

which in turn calls test_future_void__write defined as

__attribute__((__import_module__("my:test/i"), __import_name__("[async-lower][future-write-0][async]read-future")))
extern uint32_t test_future_void__write(uint32_t, const uint8_t*);

Which has the import name for the read-future function. What am I missing here?

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen
A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Scott Waye (Aug 15 2025 at 22:44):

I.e., I would expect to see an import for drop-future.

view this post on Zulip Scott Waye (Aug 15 2025 at 22:45):

Is it that internally any "void future" would work here, due to some internal things. I think I read that only one task (subtask?) can be active at a time?

view this post on Zulip Joel Dice (Aug 15 2025 at 22:59):

I'm not sure I understand what you're asking, but note that we use a "trick" to refer to a given future or stream type in the binding generator, allowing wit-component to determine which type to use when generating references to the future.write, future.drop, etc. intrinsics: https://github.com/bytecodealliance/wasm-tools/blob/f6967282dce8f2b3be29ddcd0f33cb90a37ec8f4/crates/wit-parser/src/lib.rs#L1160-L1174

Trying to encode the entire type using only e.g. name mangling would be difficult if not impossible (imagine mangling future<list<tuple<some-record, some-resource-type>>>, especially given that resource types are not necessarily uniquely identified by their names, e.g. when a resource is both imported and exported), so we instead refer to some arbitrary function that uses the type we care about and use an index to indicate which one we mean.

CLI and Rust libraries for low-level manipulation of WebAssembly modules - bytecodealliance/wasm-tools

view this post on Zulip Scott Waye (Aug 15 2025 at 23:04):

I'm curious about read-future in the name of the import used by the drop-future test. Reading the link you posted, and your explanation, I think I get it. The type is the same, so the name at the end of the import is not that important, its the type that must be matched.

view this post on Zulip Joel Dice (Aug 16 2025 at 14:53):

Yeah, the point is to pick a function that refers to the type -- if there is more than one which refers to that type, then the binding generator picks one arbitrarily and uses it.

view this post on Zulip Scott Waye (Jan 09 2026 at 23:22):

I'm thinking to add the cancel functionality next. We can't extend Task to hold the handle as its sealed, so i was thinking we maintain a list of some sort of created async function tasks, so that we can map the task back to the handle. We could then have an extensions perhaps to Task, e.g. Cancel, that would look up the handle from the hash set of Tasks and call cancel on that handle. Does that sound sensible?

view this post on Zulip Joel Dice (Jan 09 2026 at 23:33):

Personally, I'd recommend leaving cancellation until later. I haven't even added cancel support to Python or Go yet; I'm basically waiting until someone asks for it. I'd suggest finishing everything except cancellation first, at which point developers will be able to start doing interesting things with WASIp3 in .NET.

view this post on Zulip Joel Dice (Jan 09 2026 at 23:35):

Anyway, whenever you do tackle cancellation, the hash set sounds plausible to me; I'd be interested to here what Pavel thinks. I'd have to go back and study Task again to have more of an opinion.

view this post on Zulip Scott Waye (Jan 10 2026 at 00:24):

Ok, I will leave it for now and look at a different runtime
test to tackle.

view this post on Zulip Pavel Šavara (Jan 10 2026 at 16:31):

Joel Dice said:

I'd be interested to here what Pavel thinks. I'd have to go back and study Task again to have more of an opinion.

I'm afraid I would have to spent some time playing with latest p3 before I can offer more valuable insight. Right now I think :

view this post on Zulip Scott Waye (Jan 10 2026 at 20:42):

I'm happy to add GC Collects in places to see what breaks in the runtime tests

view this post on Zulip Joel Dice (Jan 12 2026 at 16:04):

Pavel Šavara said:

I don't have a direct answer to this, but I'll note that we've tried to make WASIp3 "less trappy" in that we try to avoid parent/child relationships between component model handles in general. In p2, we had a lot of parent/child relationships such that if you dropped the parent handle before any of the children, you'd trap, which meant GC-based languages had to do extra work to ensure handles were dropped in the correct order. p3 is a lot more relaxed about that, FWIW.

Pavel Šavara said:

Yeah, I don't think a stream should be represented as a Task. Instead, each call to Read or Write should return a new Task, which is equivalent to how the Rust and Python bindings generators work.

Pavel Šavara said:

It's looking like we might have cooperative multi-threading as part of 0.3.0, or if not, soon afterward. And it's already implemented in Wasmtime, so I think it would be reasonable for the .NET port to just use mult-threading from the beginning.

Pavel Šavara said:

This is the most up-to-date resource I'm aware of: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Concurrency.md. You can also read about the thread.* intrinsics starting here for a summary of how threads can be created, suspended, and resumed.

Repository for design and specification of the Component Model - WebAssembly/component-model
Repository for design and specification of the Component Model - WebAssembly/component-model

view this post on Zulip Scott Waye (Jan 12 2026 at 22:21):

Regards this wit

  export run: async func();

which is from a test runner. Will the runtime try to call a [callback] function, and if it does, what would it pass for the waitable, second parameter?

view this post on Zulip Joel Dice (Jan 12 2026 at 22:25):

It will call the callback function if the run export returns CALLBACK_CODE_YIELD or CALLBACK_CODE_WAIT. If it's the former, the waitable will be zero (i.e. not applicable). If it's the latter, the waitable will be whichever waitable the event concerns, i.e. one of the waitables in the waitable-set returned as part of the CALLBACK_CODE_WAIT return value.

If the run export returns CALLBACK_CODE_EXIT, then the [callback] function won't be called (at least not for that particular task).

view this post on Zulip Scott Waye (Jan 12 2026 at 22:33):

Thanks, that makes sense. I'm trying to work out how I should handle
https://github.com/bytecodealliance/wit-bindgen/blob/90b50130ee83f298581e23dffc916ff36c8fd8a3/tests/runtime-async/async/pending-import/test.c#L34
As you know this drop is part of the vtable, but I didn't really want to expose the vtable to the user code, i.e. the test code, so I though I could capture the correct vtable in the event, and then and a method to the event class to "drop the waitable", e.g.

        public static unsafe int PendingImportCallback(AsyncSupport.Event ev)

Then inside this method:

            ev.DropWaitable();

However if I do this in the export for the exported async method in the test code, the runner code, which exports a different method, the run, it also ends up with that export

            [global::System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute(EntryPoint = "[callback][async-lift]run")]
            public static int RunCallback(int eventRaw, int waitable, int code)
            {
                AsyncSupport.Event e = new AsyncSupport.Event(eventRaw, waitable, code, FutureVTable);
                return RunnerWorldExportsImpl.RunCallback(e);
            }

And of course I have no future vtable here.

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Scott Waye (Jan 12 2026 at 22:35):

Maybe I should capture in the function bindgen, if I have used a future, and then I can write the callbackinterop appropriately.

view this post on Zulip Scott Waye (Jan 12 2026 at 22:36):

Its also possible, that this will be more obvious to me, when I get to a more sophisticated wit.

view this post on Zulip Scott Waye (Jan 12 2026 at 22:44):

Let me think again about this, I may have a better question later.

view this post on Zulip Joel Dice (Jan 12 2026 at 22:44):

Sorry, in a meeting now, but will chime in when I can

view this post on Zulip Scott Waye (Jan 12 2026 at 22:45):

Ok, thanks, I was just thinking about how my current scheme doesn't work when the export has multiple future params of different types.

view this post on Zulip Joel Dice (Jan 12 2026 at 23:05):

Generally speaking, before returning CALLBACK_CODE_WAIT the bindings generator or associated runtime library should store an entry in task-local map representing the pair of the waitable and e.g. a closure or equivalent representing what to do when an event arrives for that waitable. The key thing to note is that "task-local" means saved and restored using the context.set and context.get intrinsics, respectively.

Don't hesitate to look at what the Rust, Python, or Go bindings generators and their runtimes do.

view this post on Zulip Scott Waye (Jan 12 2026 at 23:09):

There is also a rust example for this test, I will look at what it generates here, thanks

view this post on Zulip Scott Waye (Jan 12 2026 at 23:13):

Ah, now I see something I tried to kick down the road, that is likely useful. I just have a single context type. I did notice that the c code created one for the function, but it didn't dawn on me that I could customise the one of the fields

view this post on Zulip Scott Waye (Jan 12 2026 at 23:20):

One thing that throws me a bit here, is that the rust test here is just

impl crate::exports::my::test::i::Guest for Component {
    async fn pending_import(x: FutureReader<()>) {
        x.await
    }
}

Whereas the c is way longer . Perhaps I should not be looking at the 'c' to get an idea of what should be in the user code, but just for the final sequence of events?

view this post on Zulip Joel Dice (Jan 12 2026 at 23:23):

That's because the C generator is ultra-minimal, leaving the details to the application developer. The Rust, Python, and Go generators and their runtime libraries do a lot more, handling the details on behalf of the app developer. For Rust, see async_support.rs and related code under the rt module, which is where those details are handled.

view this post on Zulip Joel Dice (Jan 12 2026 at 23:24):

You can also generate Rust bindings using the wit-bindgen rust subcommand to see how the generator bridges the application code with the ABI, using the rt library.

view this post on Zulip Scott Waye (Jan 12 2026 at 23:25):

THanks, I will reset my thinking a bit here.

view this post on Zulip Scott Waye (Jan 16 2026 at 22:34):

Hi, I'm on the way to making the C# bindings more like Go and Rust. Still on the pending-inport example

package my:test;

interface i {
  pending-import: async func(x: future);
}

world test {
  export i;
}

When the runner writes to the future x, that completes successfully, but the runtime should resume the future read in the test side, as I understand it. But I'm missing how that happens as there doesn't seem to be an export that is called in the test, should I have registered a callback or something before returning CallbackCode.Yield; ?

view this post on Zulip Scott Waye (Jan 16 2026 at 22:41):

Perhaps Im missing a waitable join somewhere..

view this post on Zulip Scott Waye (Jan 16 2026 at 22:49):

yes, that might be it, let me try that.

view this post on Zulip Scott Waye (Jan 16 2026 at 23:18):

Actually still I bit confused, I can't see where the rust is doing that, and there is no go implementation, except for the generated code which has

wit_async.SubtaskWait(uint32(wasm_import_pending_import((x).TakeHandle())))

Which does do a join in SubtaskWait but it doesn't seem to leave space to do the writing to the future, so not sure how that works....

view this post on Zulip Scott Waye (Jan 17 2026 at 14:25):

I 've added a join which I think was the missing bit, was trying to get away without implementing the [callback] part so looking at that bit now.

view this post on Zulip Scott Waye (Jan 17 2026 at 17:18):

I'm getting this error at the end of the test execution after calling the callback on the test side

wasm trap: async-lifted export failed to produce a result

The callback I have for [callback][async-lift]my:test/i#pending-import is returning 0 which I thought was CALLBACK_CODE_EXIT, is that not right ?

view this post on Zulip Scott Waye (Jan 17 2026 at 17:28):

ah, I 've not called task-return. Sorry for spanming

view this post on Zulip Ralph (Jan 19 2026 at 09:57):

@Scott Waye you spam all you want. It helps to see your thinking process -- it shows what we need to explain better and/or modify. If you can't figure it out, how can others? :-)

view this post on Zulip Scott Waye (Jan 19 2026 at 18:44):

Lol, I do find writing things down sometimes kicks the brain into doing something useful. I need a chat client that just go > /dev/null :-)

view this post on Zulip Scott Waye (Jan 30 2026 at 02:15):

About the implementation of the Stream Write/StartWrite method, the buffer is passed, but who is responsible for freeing that, is it the reader?

view this post on Zulip Scott Waye (Jan 30 2026 at 02:16):

I say freeing, but could be unpinning in C#

view this post on Zulip Joel Dice (Jan 30 2026 at 17:05):

The reader will normally be a different component from the writer (or the host), so it wouldn't be able to free the buffer even if it wanted to. Instead, the writer is responsible for freeing or unpinning the write buffer once the write completes (i.e. after it either completes immediately without delay or after an EVENT_STREAM_WRITE is received for that write). Same goes for the read end: the component that's reading must ensure the read buffer stays valid (and doesn't move) until it has been notified either via a return code or an event that the write has been completed (or cancelled).

view this post on Zulip Scott Waye (Jan 30 2026 at 19:42):

EVENT_STREAM_WRITE gives the buffer in the args i suppose.

view this post on Zulip Scott Waye (Jan 30 2026 at 19:44):

I might be able to kick this bit down the road I suppose, with a TODO

view this post on Zulip Joel Dice (Jan 30 2026 at 19:47):

Hmm, I guess it depends on what, precisely, you plan to kick down the road. It's fine to pin the buffer longer than necessary, but if you pin it shorter than necessary you'll end up with hard-to-debug heap corruption sooner or later.

view this post on Zulip Scott Waye (Jan 30 2026 at 19:48):

That is true, I was thinking to pin it, and leave it there. But if the EVENT_STREAM_WRITE gets the buffer, then it would be trivial to clean up

view this post on Zulip Scott Waye (Jan 30 2026 at 19:53):

Looking at the Go

        case EVENT_STREAM_READ, EVENT_STREAM_WRITE, EVENT_FUTURE_READ, EVENT_FUTURE_WRITE:
            waitableJoin(event1, 0)
            channel := state.pending[event1]
            delete(state.pending, event1)
            channel <- event2

I suppose delete would be the thing to look at, if I can find it.

view this post on Zulip Joel Dice (Jan 30 2026 at 19:59):

That just removes an entry from the state.pending map; it doesn't affect pinning. Unpinning the read buffer for Go happens via this defer statement

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Joel Dice (Jan 30 2026 at 20:00):

That works because wit_async.FutureOrStreamWait (called later in the same function) blocks the calling goroutine until the read completes, at which point the buffer can be unpinned.

view this post on Zulip Scott Waye (Jan 30 2026 at 20:06):

Think I understand that code, once the lifting is complete the read buffer can go. I suppose write is similar.

view this post on Zulip Scott Waye (Jan 30 2026 at 20:06):

thanks

view this post on Zulip Joel Dice (Jan 30 2026 at 20:20):

Technically, we could unpin as soon as wit_async.FutureOrStreamWait returns and before lifting. Unpinning just means the GC is free to move the buffer, but that won't harm pure Go code. The pinning is just to ensure the pointer we passed to the host remains valid, and thus is no longer needed once the host tells us the read has completed.

view this post on Zulip Scott Waye (Jan 31 2026 at 00:10):

Ah yes, of course.

view this post on Zulip Scott Waye (Feb 01 2026 at 15:31):

Not sure what I've done, but I have this error
---- simple-stream-payload | runner.cs | test.cs ----
runner: .\tests\runtime-async\async\simple-stream-payload\runner.cs
compiled runner: C:\github\wit-bindgen\.\target\artifacts\simple-stream-payload\simple-stream-payload\runner-csharp.wasm
error: failed to run simple-stream-payload

Caused by:
0: failed to compose "C:\\github\\wit-bindgen\\.\\target\\artifacts\\simple-stream-payload\\simple-stream-payload\\runner-csharp.wasm" with "C:\\github\\wit-bindgen\\.\\target\\artifacts\\simple-stream-payload\\simple-stream-payload\\test-csharp.wasm"
1: no dependencies of component C:\github\wit-bindgen\.\target\artifacts\simple-stream-payload\simple-stream-payload\runner-csharp.wasm were found

What dependencies is it looking for, a particular import?

view this post on Zulip Scott Waye (Feb 02 2026 at 00:51):

Think I've missed some code in the runner, will check that.

view this post on Zulip Alex Crichton (Feb 02 2026 at 01:29):

ah yeah that error means that the runner doesn't actually import the test component, so it might be that the runner is stubbed out or not importing anything?

view this post on Zulip Scott Waye (Feb 02 2026 at 15:15):

Yep, if I don't actually use it the NAOT compiler will trim the import

view this post on Zulip Scott Waye (Feb 04 2026 at 17:43):

In the runtime tests, the runner exports a run method, and I have this error

(base) PS C:\github\wit-bindgen> wasmtime -W component-model-async C:\github\wit-bindgen\target\artifacts\simple-future\composed-runner.cs-test.cs.wasm
Error: failed to run main module `C:\github\wit-bindgen\target\artifacts\simple-future\composed-runner.cs-test.cs.wasm`

Caused by:
    no exported instance named `wasi:cli/run@0.2.6`

My export is [async-lift]run" which I think used to be correct. Has something changed - something now expects the cli run export ?

view this post on Zulip Joel Dice (Feb 04 2026 at 18:40):

The wasmtime CLI defaults to the run command, which by default expects the component input to export wasi:cli/run. However, the runner tests in wit-bindgen now export a top-level run function, which you can invoke using wasmtime run --invoke='run()' foo.wasm

view this post on Zulip Scott Waye (Feb 04 2026 at 18:41):

ah, that explains how CI is passing, thanks

view this post on Zulip Scott Waye (Feb 05 2026 at 12:47):

With this test https://github.com/bytecodealliance/wit-bindgen/blob/350e8ad7abe8f65357c8984535d7bca5b9882c2c/tests/runtime-async/async/simple-stream-payload/runner.rs , would you expect the first write to be blocked (and need to return to the event loop before getting its count back?

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Joel Dice (Feb 05 2026 at 14:38):

Possibly; it depends on the details of how future::join! is implemented and how the read_stream function in the composed component is implemented. If the read_stream function is called first and initiates a read immediately, then the write will finish as soon as it is called. I.e. if the read happens first it will return BLOCKED and the write will complete immediately, but if the write happens first then it will return BLOCKED and the read will complete immediately.

view this post on Zulip Scott Waye (Feb 05 2026 at 14:41):

I see, in my case the write will happen first so the BLOCK is expected. In which case the mechanism is for the runner to yield I think?

view this post on Zulip Scott Waye (Feb 05 2026 at 14:42):

This seems to happen in go with the reading from the channel, but I can't see exactly what that does in terms of the low level ABI calls, is it pollable.poll or there is a callback ?

view this post on Zulip Scott Waye (Feb 05 2026 at 14:44):

Here I think https://github.com/bytecodealliance/wit-bindgen/blob/350e8ad7abe8f65357c8984535d7bca5b9882c2c/crates/go/src/package/wit_async/wit_async.go#L195

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Scott Waye (Feb 05 2026 at 14:46):

Are you with Akamai now ?

view this post on Zulip Joel Dice (Feb 05 2026 at 14:59):

Scott Waye said:

I see, in my case the write will happen first so the BLOCK is expected. In which case the mechanism is for the runner to yield I think?

It will return CALLBACK_CODE_WAIT with a waitable-set containing the stream handle. That's true for both Rust (because the "stackless" concurrency model using a callback for async exports matches its async/await model) and Go (because even though it has "stackful" goroutines, those are currently implemented by the Go compiler using a CPS transform that effectively makes them "stackless").

view this post on Zulip Joel Dice (Feb 05 2026 at 15:00):

Scott Waye said:

Are you with Akamai now ?

Yes!

view this post on Zulip Scott Waye (Feb 05 2026 at 15:10):

I'm missing something, or looking in the wrong place. https://github.com/bytecodealliance/wit-bindgen/blob/350e8ad7abe8f65357c8984535d7bca5b9882c2c/crates/go/src/package/wit_async/wit_async.go#L187C6-L187C24 catches RETURN_CODE_BLOCKED and creates the waitable set and does the join. Then after the write is unblocked, it enters here https://github.com/bytecodealliance/wit-bindgen/blob/350e8ad7abe8f65357c8984535d7bca5b9882c2c/crates/go/src/package/wit_async/wit_async.go#L120 doesnt it?

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen
A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Joel Dice (Feb 05 2026 at 15:10):

I'm in a meeting now; but will take a look when I have a chance.

view this post on Zulip Scott Waye (Feb 05 2026 at 15:18):

If you prefer we can cover this in the next meeting, which I think is in 2 weeks?

view this post on Zulip Joel Dice (Feb 05 2026 at 17:22):

In FutureOrStreamWait, the code = (<-channel) line blocks the current goroutine, which unwinds the goroutine stack and passes control to the Go scheduler. If there are no other runnable goroutines, then the "root" goroutine resumes here, and that returns control to the host here.

Go is its own special beast; to really understand what's going on you have to read this and study the Wasm code it generates. I don't recommend you do that, though. Just imagine FutureOrStreamWait is an async function in C# that awaits a Task representing the future or stream and returns control to the top level event loop in the meantime.

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen
A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen
The Go programming language. Contribute to golang/go development by creating an account on GitHub.

view this post on Zulip Scott Waye (Feb 05 2026 at 17:37):

Thanks, an async Task is where I want to get to. I can create that Task using a TaskCompletionSource and store it somewhere as go does. Then I will need to complete that task at some point, in go that would be from Callback I think, where is Callback hooked up? https://github.com/bytecodealliance/wit-bindgen/blob/350e8ad7abe8f65357c8984535d7bca5b9882c2c/crates/go/src/package/wit_async/wit_async.go#L61

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Scott Waye (Feb 05 2026 at 17:38):

Oh I see it

view this post on Zulip Scott Waye (Feb 05 2026 at 17:39):

Its a [callback] export

view this post on Zulip Scott Waye (Feb 05 2026 at 17:40):

Awesome, I can play some more, thanks a lot.

view this post on Zulip Scott Waye (Feb 06 2026 at 15:31):

Hi, made a little progress, but have another question, if I return CALLBACK_CODE_WAIT out of the async run in the runner, how do I run the event loop, or is that handled by wasmtime?

view this post on Zulip Joel Dice (Feb 06 2026 at 15:34):

Returning CALLBACK_CODE_WAIT tells the host: "wait until one of the waitables in this waitable-set has an event and then deliver that event to my callback function". During that wait, no guest code is run for that task -- it's entirely in the host's hands at that point.

view this post on Zulip Joel Dice (Feb 06 2026 at 15:35):

In other words, you should _not_ return CALLBACK_CODE_WAIT to the host until you've run out of things to do in the guest (i.e. there's nothing you can do until one of the pending waitables has an event)

view this post on Zulip Scott Waye (Feb 06 2026 at 15:40):

    public static async Task Run()
    {
        var (rx, tx) = IIImports.StreamNewByte();
        async Task Test()
        {
            var writtenOne = await tx.Write([0]);
        }

        AsyncSupport.Join(Test(), IIImports.ReadStream(rx));
    }

Taking this simplified runner, if Write blocks, AsyncSupport.Join should loop calling [waitable-set-poll] or return and Run returnsCALLBACK_CODE_WAIT ?

view this post on Zulip Scott Waye (Feb 06 2026 at 15:43):

Like Run is marked async but if Join waits for its Tasks to finish in a busy loop, then there is nothing async about it, so I suppose it needs to return CALLBACK_CODE_WAIT

view this post on Zulip Scott Waye (Feb 06 2026 at 15:45):

So I think that fits with your statement "run ouf of things to do", its the last statement in the function after all.

view this post on Zulip Scott Waye (Feb 06 2026 at 15:46):

you must be fed up with me by now :-)
\

view this post on Zulip Joel Dice (Feb 06 2026 at 15:50):

Somewhere there should be a function that returns a uint32; it should run a task, and if that task hasn't completed yet, it should collect all the waitables created by that task into a waitable-set and return CALLBACK_CODE_WAIT. It's been long enough since I worked on .NET that I can't remember what "run a task" looks like, so apologies for being vague about that.

view this post on Zulip Joel Dice (Feb 06 2026 at 15:53):

if there aren't any waitables, then it could just return CALLBACK_CODE_YIELD, meaning "I, the guest, don't have anything I'm waiting on, but I'm in the middle of a compute-intensive operation and want to give other tasks a chance to run because I'm a good citizen; call my callback after you've let other tasks run and I'll continue"

view this post on Zulip Joel Dice (Feb 06 2026 at 15:53):

it might help to do a zoom call at some point and walk through this together

view this post on Zulip Scott Waye (Feb 06 2026 at 15:56):

thanks, let me try a few things first based on the above. I feel it is very close, just might need somethings moving around.

view this post on Zulip Joel Dice (Feb 06 2026 at 15:59):

BTW, to clarify what I said above: if there are waitables, you still might want to return CALLBACK_CODE_YIELD instead of CALLBACK_CODE_WAIT and also call waitable-set.poll, which says "I've got some waitables I'm interested in, but I also have stuff to do in the meantime, so give other tasks a chance to run and let me know if any of those waitables are ready, but otherwise let me run some more". You can see that logic for Rust here

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen

view this post on Zulip Joel Dice (Feb 06 2026 at 16:03):

In pseudo-code:

if I have more work to do, regardless of any pending waitables:
  if I have pending waitables:
    event = call waitable-set.poll(my-waitable-set)
    if event non nil:
      deliver(event)
      continue
  return CALLBACK_CODE_YIELD
else:
  return CALLBACK_CODE_WAIT | (my-waitable-set << 4)

Last updated: Feb 24 2026 at 05:28 UTC