Skip to content
Open
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
154 changes: 98 additions & 56 deletions crates/js-component-bindgen/src/function_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ impl FunctionBindgen<'_> {
/// let ret;
/// ```
///
/// This function returns as a first tuple parameter a list of
/// variables that should be created via let statements beforehand.
///
/// # Arguments
///
/// * `amt` - number of results
Expand All @@ -290,35 +293,37 @@ impl FunctionBindgen<'_> {
amt: usize,
results: &mut Vec<String>,
is_async: bool,
) -> String {
) -> (String, String) {
let mut s = String::new();
let mut vars_init = String::new();
match amt {
0 => {
// Async functions with no returns still return async code,
// which will be used as the initial callback result going into the async driver
if is_async {
uwrite!(s, "let ret = ")
} else {
uwrite!(s, "let ret;")
uwrite!(s, "ret = ")
}
uwriteln!(vars_init, "let ret;");
}
1 => {
uwrite!(s, "let ret = ");
uwrite!(s, "ret = ");
results.push("ret".to_string());
uwriteln!(vars_init, "let ret;");
}
n => {
uwrite!(s, "var [");
uwrite!(s, "[");
for i in 0..n {
if i > 0 {
uwrite!(s, ", ");
}
uwrite!(s, "ret{}", i);
uwrite!(s, "ret{i}");
results.push(format!("ret{i}"));
uwriteln!(vars_init, "let ret;");
}
uwrite!(s, "] = ");
}
}
s
(vars_init, s)
}

fn bitcast(&mut self, cast: &Bitcast, op: &str) -> String {
Expand Down Expand Up @@ -394,31 +399,8 @@ impl FunctionBindgen<'_> {
let start_current_task_fn = self.intrinsic(Intrinsic::AsyncTask(
AsyncTaskIntrinsic::CreateNewCurrentTask,
));
let global_task_map = self.intrinsic(Intrinsic::AsyncTask(
AsyncTaskIntrinsic::GlobalAsyncCurrentTaskMap,
));
let component_instance_idx = self.canon_opts.instance.as_u32();

// If we're within an async function, wait for all top level previous tasks to finish before running
// to ensure that guests do not try to run two tasks at the same time.
if is_async && self.requires_async_porcelain {
uwriteln!(
self.src,
r#"
// All other tasks must finish before we can start this one
const taskMetas = {global_task_map}.get({component_instance_idx});
if (taskMetas) {{
const taskPromises = taskMetas
.filter(mt => mt.componentIdx === {component_instance_idx})
.map(mt => mt.task)
.filter(t => !t.getParentSubtask())
.map(t => t.exitPromise());
await Promise.allSettled(taskPromises);
}}
"#,
);
}

uwriteln!(
self.src,
r#"
Expand Down Expand Up @@ -1508,13 +1490,31 @@ impl Bindgen for FunctionBindgen<'_> {
// Output result binding preamble (e.g. 'var ret =', 'var [ ret0, ret1] = exports...() ')
// along with the code to perofrm the call
let sig_results_length = sig.results.len();
let s = self.generate_result_assignment_lhs(sig_results_length, results, is_async);

let (call_prefix, call_wrapper) = if self.requires_async_porcelain | self.is_async {
("await ", Intrinsic::WithGlobalCurrentTaskMetaFnAsync.name())
} else {
("", Intrinsic::WithGlobalCurrentTaskMetaFn.name())
};
let (vars_init, assignment_lhs) =
self.generate_result_assignment_lhs(sig_results_length, results, is_async);

let (call_prefix, call_wrapper, call_err_cleanup) =
if self.requires_async_porcelain | self.is_async {
(
"await ",
Intrinsic::WithGlobalCurrentTaskMetaFnAsync.name(),
r#"
task.reject(err);
task.exit();
return Promise.reject(err);
"#,
)
} else {
(
"",
Intrinsic::WithGlobalCurrentTaskMetaFn.name(),
r#"
task.setErrored(err);
task.exit();
throw err;
"#,
)
};

let args = if self.asmjs {
let split_i64 =
Expand Down Expand Up @@ -1559,12 +1559,18 @@ impl Bindgen for FunctionBindgen<'_> {

uwriteln!(
self.src,
r#"{s} {call_prefix} {call_wrapper}({{
taskID: task.id(),
componentIdx: task.componentIdx(),
fn: () => {callee_invoke},
}});
"#,
r#"
{vars_init}
try {{
{assignment_lhs} {call_prefix} {call_wrapper}({{
taskID: task.id(),
componentIdx: task.componentIdx(),
fn: () => {callee_invoke},
}});
}} catch (err) {{
{call_err_cleanup}
}}
"#,
);

if self.tracing_enabled {
Expand Down Expand Up @@ -1593,6 +1599,11 @@ impl Bindgen for FunctionBindgen<'_> {
self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask));
let component_instance_idx = self.canon_opts.instance.as_u32();

// At first, use the global current task metadata, in case we are executing from
// inside a with-global-current-task wrapper
let get_global_current_task_meta_fn =
self.intrinsic(Intrinsic::GetGlobalCurrentTaskMetaFn);

uwriteln!(
self.src,
"{debug_log_fn}('{prefix} [Instruction::CallInterface] ({async_}, @ enter)');",
Expand Down Expand Up @@ -1647,7 +1658,11 @@ impl Bindgen for FunctionBindgen<'_> {
}};

taskCreation: {{
parentTask = {current_task_get_fn}({component_instance_idx})?.task;
parentTask = {current_task_get_fn}(
{component_instance_idx},
{get_global_current_task_meta_fn}({component_instance_idx})?.taskID,
)?.task;

if (!parentTask) {{
createTask();
break taskCreation;
Expand Down Expand Up @@ -1701,30 +1716,57 @@ impl Bindgen for FunctionBindgen<'_> {
}

// Build the JS expression that calls the callee
let (call_prefix, call_wrapper) = if is_async || self.requires_async_porcelain {
("await ", Intrinsic::WithGlobalCurrentTaskMetaFnAsync.name())
} else {
("", Intrinsic::WithGlobalCurrentTaskMetaFn.name())
};
let (call_prefix, call_wrapper, call_err_cleanup) =
if is_async || self.requires_async_porcelain {
(
"await ",
Intrinsic::WithGlobalCurrentTaskMetaFnAsync.name(),
r#"
task.reject(err);
task.exit();
return Promise.reject(err);
"#,
)
} else {
(
"",
Intrinsic::WithGlobalCurrentTaskMetaFn.name(),
r#"
task.reject(err);
task.exit();
throw err;
"#,
)
};
let call = format!(
r#"{call_prefix} {call_wrapper}({{
componentIdx: task.componentIdx(),
taskID: task.id(),
fn: () => {callee_fn_js}({callee_args_js})
}})
componentIdx: task.componentIdx(),
taskID: task.id(),
fn: () => {callee_fn_js}({callee_args_js}),
}})
"#,
);

match self.err {
// If configured to do *no* error handling at all or throw
// error objects directly, we can simply perform the call
ErrHandling::None | ErrHandling::ThrowResultErr => {
let s = self.generate_result_assignment_lhs(
let (vars_init, assignment_lhs) = self.generate_result_assignment_lhs(
fn_wasm_result_count,
results,
is_async,
);
uwriteln!(self.src, "{s}{call};");
uwriteln!(
self.src,
r#"
{vars_init}
try {{
{assignment_lhs}{call};
}} catch (err) {{
{call_err_cleanup}
}}
"#
);
}
// If configured to force all thrown errors into result objects,
// then we add a try/catch around the call
Expand Down
50 changes: 48 additions & 2 deletions crates/js-component-bindgen/src/intrinsics/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,6 @@ impl ComponentIntrinsic {
this.#locked = locked;
}}

// TODO(fix): we might want to check for pre-locked status here, we should be deterministically
// going from locked -> unlocked and vice versa
exclusiveLock() {{
{debug_log_fn}('[{component_async_state_class}#exclusiveLock()]', {{
locked: this.#locked,
Expand Down Expand Up @@ -337,6 +335,54 @@ impl ComponentIntrinsic {
this.#onExclusiveReleaseHandlers.push(fn);
}}

// nextTaskPromise & nextTaskQueue are used to await current task completion and queues
// any tasks attempting to enter() and complete.
//
// see: nextTaskExecutionSlot()
//
// TODO(threads): this should be unnecessary once threads are properly implemented,
// as the task.enter() logic should suffice (it should be guaranteed that we cannot re-enter
// unless the task in question is the current task in the thread execution, and only one can
// run at a time)
#nextTaskPromise = Promise.resolve(true);
#nextTaskQueue = [];

async nextTaskExecutionSlot(args) {{
const {{ task }} = args;

const placeholder = {{
completed: false,
task,
promise: task.exitPromise().then(() => {{
placeholder.completed = true;
}}),
}};
this.#nextTaskQueue.push(placeholder);

let next;
while (true) {{
await this.#nextTaskPromise;

next = this.#nextTaskQueue.find(placeholder => !placeholder.completed);

// This task is next in the queue, we can continue
if (next === undefined || next === placeholder) {{
this.#nextTaskPromise = next.promise;
if (this.#nextTaskQueue.length > 1000) {{
this.#nextTaskQueue = this.#nextTaskQueue.filter(p => !p.completed);
if (this.#nextTaskQueue.length > 1000) {{
{debug_log_fn}('[{component_async_state_class}#()nextTaskExecutionSlot] next task queue length > 1000 even after cleanup, tasks may be leaking');
}}
}}
break;
}}

// If we get here, this task was *not* next in the queue, continue waiting
// (at this point the task that *is* next will likely have already set itself
// as this.#nextTaskPromise)
}}
}}

#getSuspendedTaskMeta(taskID) {{
return this.#suspendedTasksByTaskID.get(taskID);
}}
Expand Down
Loading
Loading