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
23 changes: 23 additions & 0 deletions crates/js-component-bindgen/src/function_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,14 @@ impl Bindgen for FunctionBindgen<'_> {

Instruction::CallWasm { name, sig } => {
let debug_log_fn = self.intrinsic(Intrinsic::DebugLog);
let get_component_state = self.intrinsic(Intrinsic::Component(
ComponentIntrinsic::GetOrCreateAsyncState,
));
let component_idx_expr = self
.component_state
.as_ref()
.map(|state| state.get_js_exprs().component_idx)
.unwrap_or_else(|| "-1".into());
let has_post_return = self.post_return.is_some();
let is_async = self.is_async;
uwriteln!(
Expand All @@ -1594,6 +1602,13 @@ impl Bindgen for FunctionBindgen<'_> {
// (if we're calling into wasm then we know it was not)
uwriteln!(self.src, "const hostProvided = false;");

// Argument validation and lowering happen before this instruction. Only
// disable the instance once execution is about to enter the component.
uwriteln!(
self.src,
"{get_component_state}({component_idx_expr}).throwIfTrapped();"
);

// Inject machinery for starting a 'current' task
// (this will define the 'task' variable)
self.start_current_task(inst);
Expand Down Expand Up @@ -1689,6 +1704,7 @@ impl Bindgen for FunctionBindgen<'_> {
taskID: task.id(),
err,
}});
{get_component_state}({component_idx_expr}).markTrapped(err);
task.setErrored(err);
task.reject(err);
task.exit();
Expand All @@ -1706,6 +1722,7 @@ impl Bindgen for FunctionBindgen<'_> {
taskID: task.id(),
err,
}});
{get_component_state}({component_idx_expr}).markTrapped(err);
task.setErrored(err);
task.reject(err);
task.exit();
Expand Down Expand Up @@ -1791,6 +1808,9 @@ impl Bindgen for FunctionBindgen<'_> {
// Call to an imported interface (normally provided by the host)
Instruction::CallInterface { func, async_ } => {
let debug_log_fn = self.intrinsic(Intrinsic::DebugLog);
let get_component_state = self.intrinsic(Intrinsic::Component(
ComponentIntrinsic::GetOrCreateAsyncState,
));
let start_current_task_fn = self.intrinsic(Intrinsic::AsyncTask(
AsyncTaskIntrinsic::CreateNewCurrentTask,
));
Expand Down Expand Up @@ -1935,6 +1955,7 @@ impl Bindgen for FunctionBindgen<'_> {
subtaskID: task.getParentSubtask()?.id(),
err,
}});
{get_component_state}({component_idx_expr}).markTrapped(err);
task.setErrored(err);
task.reject(err);
task.exit();
Expand All @@ -1953,6 +1974,7 @@ impl Bindgen for FunctionBindgen<'_> {
subtaskID: task.getParentSubtask()?.id(),
err,
}});
{get_component_state}({component_idx_expr}).markTrapped(err);
task.setErrored(err);
task.reject(err);
task.exit();
Expand Down Expand Up @@ -2016,6 +2038,7 @@ impl Bindgen for FunctionBindgen<'_> {
try {{
ret = {{ tag: 'ok', val: {call} }};
}} catch (e) {{
if ({get_component_state}({component_idx_expr}).markTrapped(e)) {{ throw e; }}
ret = {{ tag: 'err', val: {err_payload}(e) }};
}}
"#,
Expand Down
15 changes: 15 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ impl ComponentIntrinsic {
let promise_with_resolvers_fn = Intrinsic::PromiseWithResolversPonyfill.name();
let stream_readable_end_class =
Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamReadableEndClass).name();
let trap_error_class = Intrinsic::TrapError.name();

output.push_str(&format!(
r#"
Expand All @@ -161,6 +162,7 @@ impl ComponentIntrinsic {
#suspendedTasksByTaskID = new Map();
#suspendedTaskIDs = [];
#errored = null;
#trapped = null;

#backpressure = 0;
#backpressureWaiters = 0n;
Expand Down Expand Up @@ -197,6 +199,19 @@ impl ComponentIntrinsic {
this.#errored = err;
}}

markTrapped(err) {{
if (!(err instanceof {trap_error_class} || err instanceof WebAssembly.RuntimeError)) {{
return false;
}}
{debug_log_fn}('[{component_async_state_class}#markTrapped()] component trapped', {{ err, componentIdx: this.#componentIdx }});
if (this.#trapped === null) {{ this.#trapped = err; }}
return true;
}}

throwIfTrapped() {{
if (this.#trapped !== null) {{ throw this.#trapped; }}
}}

callingSyncImport(val) {{
if (val === undefined) {{ return this.#callingAsyncImport; }}
if (typeof val !== 'boolean') {{ throw new TypeError('invalid setting for async import'); }}
Expand Down
7 changes: 4 additions & 3 deletions crates/js-component-bindgen/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1409,9 +1409,10 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source {
if args.intrinsics.contains(&Intrinsic::Component(
ComponentIntrinsic::ComponentAsyncStateClass,
)) {
args.intrinsics.extend([&Intrinsic::AsyncStream(
AsyncStreamIntrinsic::GlobalStreamMap,
)]);
args.intrinsics.extend([
&Intrinsic::TrapError,
&Intrinsic::AsyncStream(AsyncStreamIntrinsic::GlobalStreamMap),
]);
}

if args
Expand Down
6 changes: 3 additions & 3 deletions docs/src/advanced/detecting-traps.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ While some errors are represented at the WIT type level and expected, some inter
may cause a component instance to [trap][wiki-trap]. Jco-generated bindings report the
component-model traps they detect with the exported `_util.TrapError` class.

**After an instance has trapped, the component instance should no longer be used,
and any subsequent use (successful or otherwise) is undefined behavior**.
After an instance has trapped, Jco marks the component instance as unusable. Any subsequent
call throws the original trap without re-entering the component.

To detect these traps, use the `_util.TrapError` class:

Expand All @@ -21,7 +21,7 @@ try {
} catch (err) {
if (err instanceof _util.TrapError) {
console.error(`TRAP: ${err}`);
// avoid continuing to use the instance
// The instance is now disabled and cannot be called again.
} else {
// Other exceptions are unexpected and should be handled separately.
throw err;
Expand Down
2 changes: 1 addition & 1 deletion packages/jco-transpile/test/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { readComponentBytes } from './helpers.js';
// - (2025/02/04) increased due to stabilization changes for async tasks
// - (2025/12/16) increased due to additional async impl
// - (2026/07/02) increased due to async launch (and actually enabling the tests)
const FLAVORFUL_WASM_TRANSPILED_CODE_CHAR_LIMIT = 185_000;
const FLAVORFUL_WASM_TRANSPILED_CODE_CHAR_LIMIT = 190_000;

suite('Transpile', async () => {
const flavorfulWasmBytes = await readComponentBytes(
Expand Down