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.


Last updated: Dec 06 2025 at 07:03 UTC