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?
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.
Thanks I'll make a start.
@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?
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?
Think I've confused future and async I will start with just async as that looks simpler
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.
For every async function we generate a [async-lift].... and a [callback][async-lift] for those 2 scenarios you describe ?
on the export side I'm looking at first
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.
Thanks very much, I should be able to get the first tests passing from here.
are you using a locally built clang to get support for async in wit ?
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)
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).
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.
lets see :-)
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")))
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.
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.
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 ?
its a bit weird becuse function 140 is unrelatead
(func $"Thread::ResetCachedTransitionFrame()" (;140;) (type 0) (param i32)
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))
)
)
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
If you can point me to a branch with your changes and instructions for reproducing the issue, I can take a look.
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"
Thanks. I've got a bunch of meetings this morning, but I'll dig in later today.
I'm not totally stuck yet, I can do more research later if you have better things to do.
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.
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
Great, that seesm to be passing the first test. Thanks very much.
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 ?
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.
Thank you!
I see the codegen tests use the waitable-... way of returning , is the [task-return]flow well tested ?
ignore that, I see the c tests use it.
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?
I.e., I would expect to see an import for drop-future.
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?
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.
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.
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