Skip to content
Closed
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
2 changes: 1 addition & 1 deletion crates/js-component-bindgen/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl<'a> Translation<'a> {
}

// Set up wasm features to use
let mut features = WasmFeatures::default();
let mut features = WasmFeatures::WASM3 | WasmFeatures::WIDE_ARITHMETIC;
features.set(WasmFeatures::MULTI_MEMORY, false);

match Validator::new_with_features(features).validate_all(translation.wasm) {
Expand Down
17 changes: 15 additions & 2 deletions crates/js-component-bindgen/src/function_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1491,11 +1491,24 @@ impl Bindgen for FunctionBindgen<'_> {
taskID: task.id(),
componentIdx: task.componentIdx(),
fn: () => {callee}({args}),
}});
"#,
}});"#,
callee = self.callee,
args = operands.join(", "),
);
// For async exports using task.return, the JSPI Promising wrapper
// returns undefined (void core wasm function). The task's completion
// promise (resolved by task.return) provides the already-lifted result.
if self.is_async && sig_results_length > 0 {
uwriteln!(
self.src,
r#"
if (ret === undefined) {{
const taskResult = await task.completionPromise();
if (taskResult !== undefined) {{ return taskResult; }}
Comment on lines +1505 to +1507
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code isn't quite right -- async exports will go through Instruction::AsyncTaskReturn which will return the completion promise. Did you find that to not work? An examlpe case here would be great to add to the test suite as a regression test if that's the case.

}}
"#,
);
}

if self.tracing_enabled {
let prefix = self.tracing_prefix;
Expand Down
21 changes: 12 additions & 9 deletions crates/js-component-bindgen/src/intrinsics/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,15 +542,18 @@ impl LowerIntrinsic {

ctx.resultPtr = originalPtr + payloadOffset32;

const payloadBytesWritten = lowerFn({{
memory,
realloc,
vals: [val],
storagePtr,
storageLen,
componentIdx,
}});
let bytesWritten = payloadOffset + payloadBytesWritten;
let payloadBytesWritten = 0;
if (lowerFn) {{
payloadBytesWritten = lowerFn({{
memory,
realloc,
vals: [val],
storagePtr,
storageLen,
componentIdx,
}});
}}
let bytesWritten = payloadOffset32 + payloadBytesWritten;

const rem = ctx.storagePtr % align32;
if (rem !== 0) {{
Expand Down
23 changes: 23 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,9 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source {
if args
.intrinsics
.contains(&Intrinsic::Waitable(WaitableIntrinsic::WaitableSetPoll))
|| args
.intrinsics
.contains(&Intrinsic::Waitable(WaitableIntrinsic::WaitableSetWait))
{
args.intrinsics
.extend([&Intrinsic::Host(HostIntrinsic::StoreEventInComponentMemory)]);
Expand Down Expand Up @@ -1268,6 +1271,22 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source {
]);
}

if args
.intrinsics
.contains(&Intrinsic::Lower(LowerIntrinsic::LowerFlatResult))
{
args.intrinsics
.insert(Intrinsic::Lower(LowerIntrinsic::LowerFlatVariant));
}

if args
.intrinsics
.contains(&Intrinsic::Lower(LowerIntrinsic::LowerFlatOption))
{
args.intrinsics
.insert(Intrinsic::Lower(LowerIntrinsic::LowerFlatVariant));
}

if args
.intrinsics
.contains(&Intrinsic::Lower(LowerIntrinsic::LowerFlatVariant))
Expand Down Expand Up @@ -1364,10 +1383,14 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source {
if args
.intrinsics
.contains(&Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamWrite))
|| args
.intrinsics
.contains(&Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamRead))
{
args.intrinsics.extend([
&Intrinsic::GlobalBufferManager,
&Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncBlockedConstant),
&Intrinsic::AsyncEventCodeEnum,
]);
}

Expand Down
48 changes: 40 additions & 8 deletions crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,7 @@ impl AsyncStreamIntrinsic {
r#"
class {host_stream_class_name} {{
#componentIdx;
#streamEndIdx;
#streamEndWaitableIdx;
#streamTableIdx;

#payloadLiftFn;
Expand All @@ -1162,24 +1162,28 @@ impl AsyncStreamIntrinsic {
if (!args.payloadLowerFn) {{ throw new TypeError("missing payload lower fn"); }}
this.#payloadLowerFn = args.payloadLowerFn;

if (args.streamEndIdx === undefined) {{ throw new Error("missing stream idx"); }}
if (args.streamEndWaitableIdx === undefined) {{ throw new Error("missing stream idx"); }}
if (args.streamTableIdx === undefined) {{ throw new Error("missing stream table idx"); }}
this.#streamEndIdx = args.streamEndIdx;
this.#streamEndWaitableIdx = args.streamEndWaitableIdx;
this.#streamTableIdx = args.streamTableIdx;

this.#isUnitStream = args.isUnitStream;
}}

setRep(rep) {{
this.#rep = rep;
}}

createUserStream(args) {{
if (this.#userStream) {{ return this.#userStream; }}
if (this.#rep === null) {{ throw new Error("unexpectedly missing rep for host stream"); }}

const cstate = {get_or_create_async_state_fn}(this.#componentIdx);
if (!cstate) {{ throw new Error(`missing async state for component [${{this.#componentIdx}}]`); }}

const streamEnd = cstate.getStreamEnd({{ tableIdx: this.#streamTableIdx, streamEndIdx: this.#streamEndIdx }});
const streamEnd = cstate.getStreamEnd({{ tableIdx: this.#streamTableIdx, streamEndWaitableIdx: this.#streamEndWaitableIdx }});
if (!streamEnd) {{
throw new Error(`missing stream [${{this.#streamEndIdx}}] (table [${{this.#streamTableIdx}}], component [${{this.#componentIdx}}]`);
throw new Error(`missing stream [${{this.#streamEndWaitableIdx}}] (table [${{this.#streamTableIdx}}], component [${{this.#componentIdx}}]`);
}}

return new {external_stream_class}({{
Expand Down Expand Up @@ -1266,6 +1270,20 @@ impl AsyncStreamIntrinsic {
this.#writeFn(obj);
}}

intoReadableStream() {{
const stream = this;
return new ReadableStream({{
async pull(controller) {{
const chunk = await stream.next();
if (chunk === undefined) {{
controller.close();
}} else {{
controller.enqueue(chunk);
}}
}},
}});
}}

[{symbol_dispose}]() {{
this.#dropFn();
}}
Expand Down Expand Up @@ -1366,7 +1384,7 @@ impl AsyncStreamIntrinsic {
{debug_log_fn}('[{stream_new_from_lift_fn}()] args', {{ ctx }});
const {{
componentIdx,
streamEndIdx,
streamEndWaitableIdx,
streamTableIdx,
payloadLiftFn,
payloadTypeSize32,
Expand All @@ -1376,7 +1394,7 @@ impl AsyncStreamIntrinsic {

const stream = new {host_stream_class}({{
componentIdx,
streamEndIdx,
streamEndWaitableIdx,
streamTableIdx,
payloadLiftFn: payloadLiftFn,
payloadLowerFn: payloadLowerFn,
Expand Down Expand Up @@ -1457,9 +1475,23 @@ impl AsyncStreamIntrinsic {
throw new Error(`stream end table idx [${{streamEnd.streamTableIdx()}}] != operation table idx [${{streamTableIdx}}]`);
}}

const memory = getMemoryFn();

// Host stream write hook: if a global hook is registered, deliver
// stream data directly from linear memory, bypassing the rendezvous.
// This enables WASI shims to receive bulk data from stream.write.
if (typeof globalThis._jcoStreamWriteHook === 'function' && streamEnd.isWritable()) {{
const actualCount = count >>> 0;
const data = new Uint8Array(memory.buffer, ptr, actualCount);
const handled = globalThis._jcoStreamWriteHook(streamEndWaitableIdx, new Uint8Array(data));
if (handled) {{
return (actualCount << 4) | 0;
}}
}}

const result = await streamEnd.copy({{
isAsync,
memory: getMemoryFn(),
memory,
ptr,
count,
eventCode: {event_code},
Expand Down
5 changes: 3 additions & 2 deletions crates/js-component-bindgen/src/intrinsics/p3/waitable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,9 @@ impl WaitableIntrinsic {
throw Error(`task component [${{task.componentIdx}}] !== executing component [${{componentIdx}}]`);
}}

const event = await task.waitForEvent({{ waitableSetRep, isAsync }});
return {store_event_in_component_memory_fn}(memory, task, event, resultPtr);
const memory = getMemoryFn();
const event = await task.waitUntil({{ waitableSetRep, readyFn: () => true, cancellable: false }});
return {store_event_in_component_memory_fn}({{ memory, ptr: resultPtr, event }});
}}
"#));
}
Expand Down
7 changes: 2 additions & 5 deletions crates/js-component-bindgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,14 @@ pub fn transpile(component: &[u8], opts: TranspileOpts) -> Result<Transpiled> {
// This does not require the correct execution of the related features post-transpilation,
// but without the right features specified, components won't load at all.
let mut validator = wasmtime_environ::wasmparser::Validator::new_with_features(
WasmFeatures::WASM2
WasmFeatures::WASM3
| WasmFeatures::COMPONENT_MODEL
| WasmFeatures::CM_ASYNC
| WasmFeatures::CM_ASYNC_BUILTINS
| WasmFeatures::CM_ASYNC_STACKFUL
| WasmFeatures::CM_ERROR_CONTEXT
| WasmFeatures::CM_FIXED_SIZE_LIST
| WasmFeatures::EXCEPTIONS
| WasmFeatures::EXTENDED_CONST
| WasmFeatures::MEMORY64
| WasmFeatures::MULTI_MEMORY,
| WasmFeatures::WIDE_ARITHMETIC,
);

let mut types = ComponentTypesBuilder::new(&validator);
Expand Down
4 changes: 2 additions & 2 deletions crates/js-component-bindgen/src/transpile_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,12 +1202,12 @@ impl<'a> Instantiator<'a, '_> {
uwriteln!(
self.src.js,
r#"
const trampoline{i} = {waitable_set_wait_fn}.bind(null, {{
const trampoline{i} = new WebAssembly.Suspending({waitable_set_wait_fn}.bind(null, {{
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great catch!

componentIdx: {instance_idx},
isAsync: {async_},
memoryIdx: {memory_idx},
getMemoryFn: () => memory{memory_idx},
}});
}}));
"#,
);
}
Expand Down
Binary file not shown.
37 changes: 35 additions & 2 deletions packages/jco/test/p3/async.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { join } from "node:path";

import { suite, test, assert, expect } from "vitest";
import { suite, test, assert, expect, afterEach } from "vitest";

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

import { setupAsyncTest } from "../helpers.js";
import { AsyncFunction, LOCAL_TEST_COMPONENTS_DIR } from "../common.js";
import { AsyncFunction, LOCAL_TEST_COMPONENTS_DIR, P3_COMPONENT_FIXTURES_DIR } from "../common.js";

suite("Async (WASI P3)", () => {
// see: https://github.com/bytecodealliance/jco/issues/1076
Expand Down Expand Up @@ -88,4 +88,37 @@ suite("Async (WASI P3)", () => {

await cleanup();
});

test("GC + WASI P3 hello world via stream write hook", async () => {
const chunks = [];
const origHook = globalThis._jcoStreamWriteHook;
globalThis._jcoStreamWriteHook = (_writableEndIdx, data) => {
chunks.push(new TextDecoder().decode(data));
return true;
};

try {
const { instance, cleanup } = await setupAsyncTest({
component: {
path: join(P3_COMPONENT_FIXTURES_DIR, "gc/hello-gc-p3.wasm"),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind including the source of this component? Is it written in Wado? Is there any way it can be written in Rust instead and built with the rest of the test components?

It's also possible to add the wado code + toolchain to testing infrastructure, but that should be under a separate PR if possible.

imports: {
"wasi:cli/stdout": {
writeViaStream: () => ({ tag: "ok" }),
},
},
},
});

await instance.run();
assert.strictEqual(chunks.join(""), "Hello, world!\n");

await cleanup();
} finally {
if (origHook) {
globalThis._jcoStreamWriteHook = origHook;
} else {
delete globalThis._jcoStreamWriteHook;
}
}
});
});
2 changes: 2 additions & 0 deletions packages/jco/test/p3/transpile.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const P3_FIXTURE_COMPONENTS = [
"error-context/async-error-context.wasm",
"error-context/async-error-context-callee.wasm",
"error-context/async-error-context-caller.wasm",

"gc/hello-gc-p3.wasm",
];

suite("Transpile (WASI P3)", () => {
Expand Down
Loading