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.
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.
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.
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
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!
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.
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.
Victor I suspect that wit-bindgen is going to need some sort of top-level helper function to assist with generating code for task.return. The way guest bindgen interacts with task.return is completely different than the host, so I think there'd need to be a specific hook for "run the generation of task.return which does lifts" which doesn't currently exist. That I think would be the one to generate StringLift and various instructions
This topic was moved here from #general > Should StringLift be emitted for async return values? by Alex Crichton.
A bit late getting back here, but the bug is fixed via jco-side changes in jco v1.15.4.
Since we do actually have the type information and the lifts that need to happen... it was solvable completely via Jco host generation code -- I think we can put off changes to bindgen (and possibly someday a fork/rework/rebuild of host-focused stuff) to another day!
Last updated: Dec 06 2025 at 07:03 UTC