Stream: general

Topic: Should StringLift be emitted for async return values?


view this post on Zulip ktz_alias (Nov 29 2025 at 09:46):

I'm seeing a problem when using jco transpile on a minimal async string-returning export.

Minimal WIT:

package example:async-exports;

world w {
    export get-literal: async func() -> string;
}

Rust implementation:

async fn get_literal() -> String {
    "hello".into()
}

This compiles to Wasm successfully.

However, when running jco transpile, the generated JS fails at runtime.

The generated JS includes a trampoline like:

const trampoline17 = taskReturn.bind(
    null,
    0,
    true,
    memory0,
    null,
    [_liftFlatStringUTF8],
);

_liftFlatStringUTF8 internally uses utf8Decoder, but the generated JS
never defines:

const utf8Decoder = new TextDecoder();

As a result, calling the export throws:

ReferenceError: utf8Decoder is not defined

After debugging, I found that the wasm instructions do not contain a
StringLift instruction. Generator::lift inside wit-bindgen is private,
and StringLift is only emitted in specific cases. For this async export,
no lift instruction is emitted, so jco never generates the decoding code.

My question:

**Should wit-bindgen be emitting a StringLift instruction for an async
function returning string? Or is jco expected to generate the string-lift
code for returned strings even when wit-bindgen doesn't emit StringLift?**

At the moment, the result is that any async export returning string
causes runtime failure due to missing UTF-8 decoder initialization.

Would appreciate clarification on where the responsibility lies.

view this post on Zulip Victor Adossi (Dec 01 2025 at 11:24):

Hey @ktz_alias so currently async P3 work in Jco is experimental, but this is a case that should work. There is a question of which version of the tooling you used to produce this component as well, since there have been recent changes to make async part of the function rather than a hint which we're not compatible with at all right now. I'm going to assume you're using just slightly older tooling that is still compatible (please let me know if I'm wrong here -- IIRC if you weren't lots of things would have broken before you could run of this, if you're using the Rust toolchain).

I think you raise a good point here about upstream wit-bindgen's behavior (please feel free to jump in @Joel Dice / @Alex Crichton ), as the resulting string is lifted "dynamically" during task.return. StringLift is basically ListCanonLift which is supposedly called to deal with values by popping them off the stack -- I think in this case the async code is writing those values directly to the memory (the results storage area). This goes through lift_from_memory, and as you mentioned, String does not emit when it reads, where as others (eg.Records) do (and they have the same blurb about stacks).

The thing is, string lifting code there has been in place for 2 years (!), so reads from memory don't seem to have done this for a while.

(While we're here, ErrorContext also maybe should be emitting an ErrorContextLift...)

Regardless of whether the tooling should be changed or not (it seems like it should!) on the Jco side I think we should fix the lifting function here to require the decoder intrinsic every time we see see the flat string lift happen, since we really can't have one without the other in any case and the shared text decoder is a jco-side optimization.

Will have a patch up for this soon.

Repository for design and specification of the Component Model - WebAssembly/component-model
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 Victor Adossi (Dec 01 2025 at 11:30):

If you wouldn't mind submitting an issue to jco along with a reproduction component that would be fantastic -- happy to include this as part of the test suite since it's an external toolchain interop bug.

view this post on Zulip Victor Adossi (Dec 01 2025 at 11:32):

PR for the jco-side fix is up, I'll either get that reproduction from you or make one and add a test on to the PR

This commit fixes a bug by which strings loaded from memory during async returns did not have the shared decoder (created via intrinsic) present. This commit stops short of reworking the dependency...

view this post on Zulip Victor Adossi (Dec 01 2025 at 13:09):

Another jco side fix was required -- a patch release will be out soon, should be out within the next hour!

Note that for now you should be using (and I assume you already are using) a version of wit-bindgen strictly earlier than 0.48.0. This is what jco is pinned to, for now until we absorb the aforementioned function async-ness changes!

This commit fixes a bug where the memory for writing to was being retrieved too eagerly (@ trampoline fn binding time), and was not available properly when performing async task.returns.

view this post on Zulip ktz_alias (Dec 01 2025 at 14:04):

Thanks!
Here are the versions of the toolchain I used to build the component:

rust: 1.91.1 (ea2d97820 2025-10-10)
wit-bindgen: 0.47.0
wit-bindgen-cli: 0.47.0
wasm-tools: 1.240.0 (dafe42f8f 2025-10-08)

These were used to build the small test component (async func() -> string) that reproduces the issue.
The component was compiled for the wasm32-wasip1 target and then componentized using wasm-tools component new.

view this post on Zulip ktz_alias (Dec 01 2025 at 14:04):

Note that for now you should be using (and I assume you already are using) a version of wit-bindgen strictly earlier than 0.48.0. This is what jco is pinned to, for now until we absorb the aforementioned function async-ness changes!

Yes, I was indeed using wit-bindgen 0.47.0 (the version jco is pinned to).
I didn’t use 0.48.0 or later.


Last updated: Dec 06 2025 at 05:03 UTC