Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions crates/js-component-bindgen/src/function_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3569,6 +3569,10 @@ impl Bindgen for FunctionBindgen<'_> {
// then we should start it, given that the task we have recently created (however we got to
// the async return) is going to continue to be polled soon (via the driver loop).
//
// Host-import tasks may be parented by a canonical ABI subtask. Their
// resolution is propagated to that subtask, and their own completion
// promise is intentionally left unsettled, so return the lifted host
// value directly after resolving and exiting the task.
uwriteln!(
self.src,
r#"
Expand All @@ -3580,7 +3584,7 @@ impl Bindgen for FunctionBindgen<'_> {
}})
task.resolve([ret]);
task.exit();
return {return_awaited_completion_promise};
return ret;
}}

const componentState = {get_or_create_async_state_fn}({component_idx_expr});
Expand Down Expand Up @@ -3617,14 +3621,6 @@ impl Bindgen for FunctionBindgen<'_> {

return {return_task_res};
"#,
// If we are returning the awaited task completion promise directly
// that contains a future<t>, we must wrap the result so we can deal
// with nesting if present
return_awaited_completion_promise = if self.wrap_async_future_result {
"{ value: await task.completionPromise() }"
} else {
"await task.completionPromise()"
},
// If we are returning the task result post-resolution, and it contains a future<t>,
// we must wrap the result so we can deal with nesting if present
return_task_res = if self.wrap_async_future_result {
Expand Down
18 changes: 18 additions & 0 deletions crates/test-components/src/bin/async_host_import_hang.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
mod bindings {
use super::Component;
wit_bindgen::generate!({
world: "async-host-import-hang",
});
export!(Component);
}

struct Component;

impl bindings::Guest for Component {
async fn run() -> u32 {
bindings::load().await
}
}

// Stub only to ensure this works as a binary
fn main() {}
5 changes: 5 additions & 0 deletions crates/test-components/wit/all.wit
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ world async-simple-import {
export get-string: async func() -> string;
}

world async-host-import-hang {
import load: async func() -> u32;
export run: async func() -> u32;
}

world async-error-context {
export local-run-async;
}
Expand Down
50 changes: 50 additions & 0 deletions packages/jco-transpile/test/p3/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { suite, test, assert, expect } from 'vitest';

import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';

import { transpile } from '../../src/index.js';
import { setupAsyncTest } from '../helpers.js';
import { AsyncFunction, LOCAL_TEST_COMPONENTS_DIR, createReadableStreamFromValues, timeoutMs } from '../common.js';

Expand Down Expand Up @@ -190,6 +191,55 @@ suite('Async (WASI P3)', () => {
await cleanup();
});

// https://github.com/bytecodealliance/jco/issues/1898 (bug 4)
test.concurrent('async host import tasks do not hang after resolving their parent subtask', async () => {
const { instance, cleanup } = await setupAsyncTest({
component: {
path: join(LOCAL_TEST_COMPONENTS_DIR, 'async-host-import-hang.wasm'),
imports: {
...new WASIShim().getImportObject(),
load: { default: async () => 42 },
},
},
jco: {
transpile: {
extraArgs: {
asyncMode: 'jspi',
asyncImports: ['load'],
asyncExports: ['run'],
},
},
},
});

try {
assert.strictEqual(
await Promise.race([instance.run(), timeoutMs(5_000, 'async host import timed out')]),
42,
);
} finally {
await cleanup();
}
});

test.concurrent('async host import trampolines return lifted values directly', async () => {
const { files } = await transpile(join(LOCAL_TEST_COMPONENTS_DIR, 'async-simple-import.wasm'), {
name: 'async-simple-import',
minify: false,
asyncMode: 'jspi',
asyncImports: ['load-string', 'load-u32'],
asyncExports: ['get-string', 'get-u32'],
});
const source = new TextDecoder().decode(files['async-simple-import.js']);
const directHostReturns = source.match(/task\.resolve\(\[ret\]\);\s*task\.exit\(\);\s*return ret;/g);

assert.isAtLeast(directHostReturns?.length ?? 0, 2);
assert.notMatch(
source,
/task\.resolve\(\[ret\]\);\s*task\.exit\(\);\s*return await task\.completionPromise\(\);/,
);
});

test.concurrent('async return of imported owned resource', async () => {
class ExampleResource {
constructor(id) {
Expand Down
Loading