Skip to content

Commit 5a23303

Browse files
authored
Refactor how guest exported tasks work (#1268)
* Refactor how guest exported tasks work This commit refactors some of the code generation and ABI instructions used for exported tasks. Instead of a future + closure they're represented now only as a single future which internally handles the invocation of `task.return`. No major functional change here, just some refactoring. * Review comments
1 parent 4d1e865 commit 5a23303

File tree

6 files changed

+86
-139
lines changed

6 files changed

+86
-139
lines changed

crates/core/src/abi.rs

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::fmt;
12
pub use wit_parser::abi::{AbiVariant, WasmSignature, WasmType};
23
use wit_parser::{
34
align_to_arch, Alignment, ArchitectureSize, ElementInfo, Enum, Flags, FlagsRepr, Function,
@@ -497,11 +498,13 @@ def_instruction! {
497498
/// Same as `CallWasm`, except the dual where an interface is being
498499
/// called rather than a raw wasm function.
499500
///
500-
/// Note that this will be used for async functions.
501+
/// Note that this will be used for async functions, and `async_`
502+
/// indicates whether the function should be invoked in an async
503+
/// fashion.
501504
CallInterface {
502505
func: &'a Function,
503506
async_: bool,
504-
} : [func.params.len()] => [if *async_ { 1 } else { usize::from(func.result.is_some()) }],
507+
} : [func.params.len()] => [usize::from(func.result.is_some())],
505508

506509
/// Returns `amt` values on the stack. This is always the last
507510
/// instruction.
@@ -551,16 +554,13 @@ def_instruction! {
551554
blocks: usize,
552555
} : [1] => [0],
553556

554-
/// Generate code to run after `CallInterface` for an async-lifted export.
557+
/// Call `task.return` for an async-lifted export.
555558
///
556-
/// For example, this might include task management for the
557-
/// future/promise/task returned by the call made for `CallInterface`.
558-
AsyncPostCallInterface { func: &'a Function } : [1] => [usize::from(func.result.is_some()) + 1],
559-
560-
/// Call `task.return` for an async-lifted export once the task returned
561-
/// by `CallInterface` and managed by `AsyncPostCallInterface`
562-
/// yields a value.
563-
AsyncCallReturn { name: &'a str, params: &'a [WasmType] } : [params.len()] => [0],
559+
/// This will call core wasm import `name` which will be mapped to
560+
/// `task.return` later on. The function given has `params` as its
561+
/// parameters and it will return no results. This is used to pass the
562+
/// lowered representation of a function's results to `task.return`.
563+
AsyncTaskReturn { name: &'a str, params: &'a [WasmType] } : [params.len()] => [0],
564564

565565
/// Force the evaluation of the specified number of expressions and push
566566
/// the results to the stack.
@@ -654,7 +654,7 @@ pub trait Bindgen {
654654
/// The intermediate type for fragments of code for this type.
655655
///
656656
/// For most languages `String` is a suitable intermediate type.
657-
type Operand: Clone;
657+
type Operand: Clone + fmt::Debug;
658658

659659
/// Emit code to implement the given instruction.
660660
///
@@ -1044,14 +1044,13 @@ impl<'a, B: Bindgen> Generator<'a, B> {
10441044
}
10451045

10461046
// ... and that allows us to call the interface types function
1047-
self.emit(&Instruction::CallInterface {
1048-
func,
1049-
async_: async_,
1050-
});
1051-
1052-
let (lower_to_memory, async_results) = if async_ {
1053-
self.emit(&Instruction::AsyncPostCallInterface { func });
1047+
self.emit(&Instruction::CallInterface { func, async_ });
10541048

1049+
// Asynchronous functions will call `task.return` after the
1050+
// interface function completes, so lowering is conditional
1051+
// based on slightly different logic for the `task.return`
1052+
// intrinsic.
1053+
let (lower_to_memory, async_flat_results) = if async_ {
10551054
let mut results = Vec::new();
10561055
if let Some(ty) = &func.result {
10571056
self.resolve.push_flat(ty, &mut results);
@@ -1124,18 +1123,15 @@ impl<'a, B: Bindgen> Generator<'a, B> {
11241123
}
11251124
}
11261125

1127-
if let Some(results) = async_results {
1126+
if let Some(results) = async_flat_results {
11281127
let name = &format!("[task-return]{}", func.name);
1128+
let params = if lower_to_memory {
1129+
&[WasmType::Pointer]
1130+
} else {
1131+
&results[..]
1132+
};
11291133

1130-
self.emit(&Instruction::AsyncCallReturn {
1131-
name,
1132-
params: &if results.len() > MAX_FLAT_PARAMS {
1133-
vec![WasmType::Pointer]
1134-
} else {
1135-
results
1136-
},
1137-
});
1138-
self.emit(&Instruction::Return { func, amt: 1 });
1134+
self.emit(&Instruction::AsyncTaskReturn { name, params });
11391135
} else {
11401136
self.emit(&Instruction::Return {
11411137
func,
@@ -1150,8 +1146,9 @@ impl<'a, B: Bindgen> Generator<'a, B> {
11501146

11511147
assert!(
11521148
self.stack.is_empty(),
1153-
"stack has {} items remaining",
1154-
self.stack.len()
1149+
"stack has {} items remaining: {:?}",
1150+
self.stack.len(),
1151+
self.stack,
11551152
);
11561153
}
11571154

crates/csharp/src/function.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,8 +1256,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
12561256
results.extend(operands.iter().take(*amt).map(|v| v.clone()));
12571257
}
12581258

1259-
Instruction::AsyncPostCallInterface { .. }
1260-
| Instruction::AsyncCallReturn { .. }
1259+
Instruction::AsyncTaskReturn { .. }
12611260
| Instruction::FutureLower { .. }
12621261
| Instruction::FutureLift { .. }
12631262
| Instruction::StreamLower { .. }

crates/guest-rust/rt/src/async_support.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub use futures;
5454
type BoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
5555

5656
/// Represents a task created by either a call to an async-lifted export or a
57-
/// future run using `block_on` or `first_poll`.
57+
/// future run using `block_on` or `start_task`.
5858
struct FutureState {
5959
/// Remaining work to do (if any) before this task can be considered "done".
6060
///
@@ -287,22 +287,18 @@ const STATUS_STARTING: u32 = 1;
287287
const STATUS_STARTED: u32 = 2;
288288
const STATUS_RETURNED: u32 = 3;
289289

290-
/// Poll the future generated by a call to an async-lifted export once, calling
291-
/// the specified closure (presumably backed by a call to `task.return`) when it
292-
/// generates a value.
290+
/// Starts execution of the `task` provided, an asynchronous computation.
293291
///
294-
/// This will return an appropriate status code to be returned from the
295-
/// exported function.
292+
/// This is used for async-lifted exports at their definition site. The
293+
/// representation of the export is `task` and this function is called from the
294+
/// entrypoint. The code returned here is the same as the callback associated
295+
/// with this export, and the callback will be used if this task doesn't exit
296+
/// immediately with its result.
296297
#[doc(hidden)]
297-
pub fn first_poll<T: 'static>(
298-
future: impl Future<Output = T> + 'static,
299-
fun: impl FnOnce(&T) + 'static,
300-
) -> u32 {
298+
pub fn start_task(task: impl Future<Output = ()> + 'static) -> u32 {
301299
// Allocate a new `FutureState` which will track all state necessary for
302300
// our exported task.
303-
let state = Box::into_raw(Box::new(FutureState::new(Box::pin(
304-
future.map(|v| fun(&v)),
305-
))));
301+
let state = Box::into_raw(Box::new(FutureState::new(Box::pin(task))));
306302

307303
// Store our `FutureState` into our context-local-storage slot and then
308304
// pretend we got EVENT_NONE to kick off everything.

crates/moonbit/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,8 +2660,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
26602660
results.extend(operands.iter().take(*amt).map(|v| v.clone()));
26612661
}
26622662

2663-
Instruction::AsyncPostCallInterface { .. }
2664-
| Instruction::AsyncCallReturn { .. }
2663+
Instruction::AsyncTaskReturn { .. }
26652664
| Instruction::FutureLower { .. }
26662665
| Instruction::FutureLift { .. }
26672666
| Instruction::StreamLower { .. }

crates/rust/src/bindgen.rs

Lines changed: 36 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use wit_bindgen_core::{dealias, uwrite, uwriteln, wit_parser::*, Source};
88
pub(super) struct FunctionBindgen<'a, 'b> {
99
pub r#gen: &'b mut InterfaceGenerator<'a>,
1010
params: Vec<String>,
11-
async_: bool,
1211
wasm_import_module: &'b str,
1312
pub src: Source,
1413
blocks: Vec<String>,
@@ -19,7 +18,6 @@ pub(super) struct FunctionBindgen<'a, 'b> {
1918
pub import_return_pointer_area_align: Alignment,
2019
pub handle_decls: Vec<String>,
2120
always_owned: bool,
22-
pub async_result_name: Option<String>,
2321
}
2422

2523
pub const POINTER_SIZE_EXPRESSION: &str = "::core::mem::size_of::<*const u8>()";
@@ -28,14 +26,12 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> {
2826
pub(super) fn new(
2927
r#gen: &'b mut InterfaceGenerator<'a>,
3028
params: Vec<String>,
31-
async_: bool,
3229
wasm_import_module: &'b str,
3330
always_owned: bool,
3431
) -> FunctionBindgen<'a, 'b> {
3532
FunctionBindgen {
3633
r#gen,
3734
params,
38-
async_,
3935
wasm_import_module,
4036
src: Default::default(),
4137
blocks: Vec::new(),
@@ -46,7 +42,6 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> {
4642
import_return_pointer_area_align: Default::default(),
4743
handle_decls: Vec::new(),
4844
always_owned,
49-
async_result_name: None,
5045
}
5146
}
5247

@@ -859,13 +854,35 @@ impl Bindgen for FunctionBindgen<'_, '_> {
859854
self.push_str(");\n");
860855
}
861856

862-
Instruction::CallInterface { func, .. } => {
863-
if self.async_ {
864-
self.push_str(&format!("let result = "));
865-
results.push("result".into());
866-
} else {
867-
self.let_results(usize::from(func.result.is_some()), results);
868-
};
857+
Instruction::CallInterface { func, async_ } => {
858+
let prev_src = mem::replace(self.src.as_mut_string(), String::new());
859+
860+
self.let_results(usize::from(func.result.is_some()), results);
861+
// If this function has a result and it's async, then after
862+
// this instruction the result is going to be lowered. This
863+
// lowering must happen in terms of `&T`, so force the result
864+
// of this expression to have `&` in front.
865+
if func.result.is_some() && *async_ {
866+
self.push_str("&");
867+
}
868+
869+
// Force evaluation of all arguments to happen in a sub-block
870+
// for this call. This is where `handle_decls` are pushed to
871+
// ensure that they're scoped to just this block. Additionally
872+
// this is where `prev_src` is pushed, just before the call.
873+
//
874+
// The purpose of this is to force `borrow<T>` arguments to get
875+
// dropped before the call to `task.return` that will happen
876+
// after an async call. For normal calls this happens because
877+
// the result of this function is returned directly, but for
878+
// async calls the return happens as part of this function as
879+
// a call to `task.return` via the `AsyncTaskReturn`
880+
// instruction.
881+
self.push_str("{\n");
882+
for decl in self.handle_decls.drain(..) {
883+
self.src.push_str(&decl);
884+
}
885+
self.push_str(&prev_src);
869886
let constructor_type = match &func.kind {
870887
FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => {
871888
self.push_str(&format!("T::{}", to_rust_ident(&func.name)));
@@ -884,12 +901,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
884901
.as_deref()
885902
.unwrap()
886903
.to_upper_camel_case();
887-
let call = if self.async_ {
888-
let async_support = self.r#gen.r#gen.async_support_path();
889-
format!("{async_support}::futures::FutureExt::map(T::new")
890-
} else {
891-
format!("{ty}::new(T::new",)
892-
};
904+
let call = format!("{ty}::new(T::new");
893905
self.push_str(&call);
894906
Some(ty)
895907
}
@@ -910,67 +922,19 @@ impl Bindgen for FunctionBindgen<'_, '_> {
910922
}
911923
}
912924
self.push_str(")");
913-
if let Some(ty) = constructor_type {
914-
self.push_str(&if self.async_ {
915-
format!(", {ty}::new)")
916-
} else {
917-
")".into()
918-
});
919-
}
920-
if self.async_ {
925+
if *async_ {
921926
self.push_str(".await");
922927
}
923-
self.push_str(";\n");
924-
}
925-
926-
Instruction::AsyncPostCallInterface { func } => {
927-
let result = &operands[0];
928-
self.async_result_name = Some(result.clone());
929-
results.push("result".into());
930-
let params = (0..usize::from(func.result.is_some()))
931-
.map(|_| {
932-
let tmp = self.tmp();
933-
let param = format!("result{}", tmp);
934-
results.push(param.clone());
935-
param
936-
})
937-
.collect::<Vec<_>>()
938-
.join(", ");
939-
let params = if func.result.is_none() {
940-
format!("({params})")
941-
} else {
942-
params
943-
};
944-
let async_support = self.r#gen.r#gen.async_support_path();
945-
// TODO: This relies on `abi::Generator` emitting
946-
// `AsyncCallReturn` immediately after this instruction to
947-
// complete the incomplete expression we generate here. We
948-
// should refactor this so it's less fragile (e.g. have
949-
// `abi::Generator` emit a `AsyncCallReturn` first, which would
950-
// push a closure expression we can consume here).
951-
//
952-
// Also, see `InterfaceGenerator::generate_guest_export` for
953-
// where we emit the open bracket corresponding to the `};` we
954-
// emit here.
955-
//
956-
// The async-specific `Instruction`s will probably need to be
957-
// refactored anyway once we start implementing support for
958-
// other languages besides Rust.
959-
uwriteln!(
960-
self.src,
961-
"\
962-
{result}
963-
}};
964-
let result = {async_support}::first_poll({result}, move |{params}| {{
965-
"
966-
);
928+
if constructor_type.is_some() {
929+
self.push_str(")");
930+
}
931+
self.push_str("\n};\n");
967932
}
968933

969-
Instruction::AsyncCallReturn { name, params } => {
934+
Instruction::AsyncTaskReturn { name, params } => {
970935
let func = self.declare_import(name, params, &[]);
971936

972937
uwriteln!(self.src, "{func}({});", operands.join(", "));
973-
self.src.push_str("});\n");
974938
}
975939

976940
Instruction::Flush { amt } => {

0 commit comments

Comments
 (0)