diff --git a/crates/c/src/lib.rs b/crates/c/src/lib.rs index 254a41e6a..3767e50cf 100644 --- a/crates/c/src/lib.rs +++ b/crates/c/src/lib.rs @@ -3685,7 +3685,12 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.src.push_str(");\n"); } - Instruction::CallInterface { func, async_ } => { + Instruction::CallInterface { + func, + async_, + params_block, + } => { + let block = params_block.map(|_| operands.pop().unwrap()); let mut args = String::new(); for (i, (op, (byref, _))) in operands.iter().zip(&self.sig.params).enumerate() { if i > 0 { @@ -3719,6 +3724,13 @@ impl Bindgen for FunctionBindgen<'_, '_> { "{snake}_callback_code_t {ret} = {}({args});", self.sig.name, ); + // The arguments arrived in a block of memory. The user's + // function has returned, every argument was read out of + // the block before it ran, and nothing points into the + // block, so free it here. + if let Some(ptr) = &block { + uwriteln!(self.src, "free({ptr});"); + } uwriteln!(self.src, "return {ret};"); if func.result.is_some() { results.push("ret".to_string()); diff --git a/crates/core/src/abi.rs b/crates/core/src/abi.rs index 73238fc93..61d6e9019 100644 --- a/crates/core/src/abi.rs +++ b/crates/core/src/abi.rs @@ -567,10 +567,17 @@ def_instruction! { /// Note that this will be used for async functions, and `async_` /// indicates whether the function should be invoked in an async /// fashion. + /// + /// `params_block`: for an async export whose parameters arrived in a + /// block of memory, the size and alignment of that block. The block + /// pointer is the last operand. The generator frees the block after + /// it has evaluated every lifted argument and before the task can + /// wait. CallInterface { func: &'a Function, async_: bool, - } : [func.params.len()] => [usize::from(func.result.is_some())], + params_block: Option<(ArchitectureSize, Alignment)>, + } : [func.params.len() + usize::from(params_block.is_some())] => [usize::from(func.result.is_some())], /// Returns `amt` values on the stack. This is always the last /// instruction. @@ -1242,8 +1249,47 @@ impl<'a, B: Bindgen> Generator<'a, B> { } } + // This was dynamically allocated by the caller (or async start + // function) so after it's been read by the guest we need to + // deallocate it. + let export_params_block = if sig.indirect_params + && matches!( + variant, + AbiVariant::GuestExport + | AbiVariant::GuestExportAsync + | AbiVariant::GuestExportAsyncStackful + ) { + Some( + self.bindgen + .sizes() + .record(func.params.iter().map(|param| ¶m.ty)), + ) + } else { + None + }; + + // A sync export frees the block after the call, below. An + // async export cannot free it there: the code after the call + // may sit in another function, or never run when the task is + // cancelled. So the call gets the block pointer as its last + // operand, and the generator frees the block itself. + let params_block = if async_ { + export_params_block + .as_ref() + .map(|info| (info.size, info.align)) + } else { + None + }; + if params_block.is_some() { + self.emit(&Instruction::GetArg { nth: 0 }); + } + // ... and that allows us to call the interface types function - self.emit(&Instruction::CallInterface { func, async_ }); + self.emit(&Instruction::CallInterface { + func, + async_, + params_block, + }); // The return value of an async function is *not* the result of the function // itself or a pointer but rather a status code. @@ -1269,18 +1315,8 @@ impl<'a, B: Bindgen> Generator<'a, B> { (_is_async @ false, _) => (sig.retptr, None), }; - // This was dynamically allocated by the caller (or async start - // function) so after it's been read by the guest we need to - // deallocate it. - if let AbiVariant::GuestExport - | AbiVariant::GuestExportAsync - | AbiVariant::GuestExportAsyncStackful = variant - { - if sig.indirect_params && !async_ { - let ElementInfo { size, align } = self - .bindgen - .sizes() - .record(func.params.iter().map(|param| ¶m.ty)); + if !async_ { + if let Some(ElementInfo { size, align }) = export_params_block { self.emit(&Instruction::GetArg { nth: 0 }); self.emit(&Instruction::GuestDeallocate { size, align }); } diff --git a/crates/csharp/src/function.rs b/crates/csharp/src/function.rs index 76a1c8604..687ea817b 100644 --- a/crates/csharp/src/function.rs +++ b/crates/csharp/src/function.rs @@ -1176,7 +1176,9 @@ impl Bindgen for FunctionBindgen<'_, '_> { } } - Instruction::CallInterface { func, .. } => { + Instruction::CallInterface { + func, params_block, .. + } => { let module = self.interface_gen.name; let func_name = self.func_name.to_upper_camel_case(); let interface_name = CSharp::get_class_name_from_qualified_name(module).1; @@ -1186,19 +1188,28 @@ impl Bindgen for FunctionBindgen<'_, '_> { .unwrap() .to_upper_camel_case(); - let mut oper = String::new(); + // The pointer to the block of parameters comes last; it is not an argument. + let block = params_block.map(|_| operands.pop().unwrap()); - for (i, param) in operands.iter().enumerate() { - if i == 0 && matches!(self.kind, FunctionKind::Method(_)) { - continue; - } + let kind = self.kind; + let join_arguments = |operands: &[String]| { + let mut oper = String::new(); + + for (i, param) in operands.iter().enumerate() { + if i == 0 && matches!(kind, FunctionKind::Method(_)) { + continue; + } - oper.push_str(&format!("({param})")); + oper.push_str(&format!("({param})")); - if i < operands.len() && operands.len() != i + 1 { - oper.push_str(", "); + if i < operands.len() && operands.len() != i + 1 { + oper.push_str(", "); + } } - } + + oper + }; + let mut oper = join_arguments(operands); let is_async = InterfaceGenerator::is_async(self.kind); if is_async && self.interface_gen.direction == Direction::Export { @@ -1207,9 +1218,18 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.interface_gen.csharp_gen.needs_async_support = true; let async_func_name = format!("{}Async", self.func_name.to_upper_camel_case()); + // The arguments are evaluated before the async method starts, and the lifted + // values are managed copies, so the block can be freed right after the call. + let free = match &block { + Some(ptr) => format!( + "\nglobal::System.Runtime.InteropServices.NativeMemory.Free((void*){ptr});" + ), + None => String::new(), + }; + uwriteln!( self.src, - r#"var task = {async_func_name}({oper}); + r#"var task = {async_func_name}({oper});{free} if (task.IsCompletedSuccessfully) {{ return (int)CallbackCode.Exit; @@ -1237,6 +1257,42 @@ impl Bindgen for FunctionBindgen<'_, '_> { "# ); + // The async method takes the lifted values as its parameters. An operand that + // is not a plain name, such as a record built in place, gets a fresh parameter + // name, and the call inside the async method passes that name. + let mut params = Vec::new(); + for (i, p) in func.params.iter().enumerate() { + let mut param_type = + self.interface_gen.type_name_with_qualifier(&p.ty, true); + + // Resource types need the Impl class to be distinguised. + match p.ty { + Type::Id(type_id) => { + let id = dealias(self.interface_gen.resolve, type_id); + + let kind = &self.interface_gen.resolve.types[id].kind; + match kind { + TypeDefKind::Handle(handle) => { + let (Handle::Own(ty) | Handle::Borrow(ty)) = handle; + param_type = self.interface_gen.csharp_gen.all_resources + [&ty] + .export_impl_name(); + } + _ => {} + } + } + _ => {} + } + + let mut name = strip_lift(&operands[i]); + if !is_identifier(&name) { + name = self.locals.tmp("arg"); + operands[i] = name.clone(); + } + params.push(format!("{param_type} {name}")); + } + oper = join_arguments(operands); + // Start the Async function uwriteln!( self.src, @@ -1245,37 +1301,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { var cleanups = new global::System.Collections.Generic.List(); "#, - func.params - .iter() - .enumerate() - .map(|(i, p)| { - let mut param_type = - self.interface_gen.type_name_with_qualifier(&p.ty, false); - - // Resource types need the Impl class to be distinguised. - match p.ty { - Type::Id(type_id) => { - let id = dealias(self.interface_gen.resolve, type_id); - - let kind = &self.interface_gen.resolve.types[id].kind; - match kind { - TypeDefKind::Handle(handle) => { - let (Handle::Own(ty) | Handle::Borrow(ty)) = handle; - param_type = - self.interface_gen.csharp_gen.all_resources - [&ty] - .export_impl_name(); - } - _ => {} - } - } - _ => {} - } - - format!("{} {}", param_type, strip_lift(&operands[i])) - }) - .collect::>() - .join(", ") + params.join(", ") ); self.needs_cleanup = true; } @@ -2047,6 +2073,11 @@ pub fn strip_lift(lifted_param: &String) -> String { out.into_owned() } +fn is_identifier(s: &str) -> bool { + s.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') + && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') +} + /// Dereference any number `TypeDefKind::Type` aliases to retrieve the target type. pub fn dealias(resolve: &Resolve, mut id: TypeId) -> TypeId { loop { diff --git a/crates/d/src/lib.rs b/crates/d/src/lib.rs index 71d18391b..15880fc1c 100644 --- a/crates/d/src/lib.rs +++ b/crates/d/src/lib.rs @@ -3422,7 +3422,7 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> { self.push_str(&format!("deallocate.purge();\n")); } } - abi::Instruction::CallInterface { func, async_ } => { + abi::Instruction::CallInterface { func, async_, .. } => { if *async_ { todo!("CallInterface async"); } diff --git a/crates/go/src/lib.rs b/crates/go/src/lib.rs index f9c779e33..2c9ac0b7f 100644 --- a/crates/go/src/lib.rs +++ b/crates/go/src/lib.rs @@ -2010,7 +2010,18 @@ for index := 0; index < int({length}); index++ {{ ); results.push(result); } - Instruction::CallInterface { func, .. } => { + Instruction::CallInterface { + func, params_block, .. + } => { + // The block was allocated by this component's `cabi_realloc` + // in memory the Go runtime manages; `witRuntime.Unpin()` + // below releases it, so there is nothing to free here. + // `indirect_params` sets `unpin_params`, so that call is + // always emitted when a block exists. + if params_block.is_some() { + operands.pop(); + } + if self.unpin_params { self.imports.insert(remote_pkg("runtime")); uwriteln!(self.src, "witRuntime.Unpin()"); diff --git a/crates/moonbit/src/async/ev.mbt b/crates/moonbit/src/async/ev.mbt index ea41df22e..950acc198 100644 --- a/crates/moonbit/src/async/ev.mbt +++ b/crates/moonbit/src/async/ev.mbt @@ -64,6 +64,10 @@ fn finish_waitableset( guard task_state.lane.blocking == 0 && task_state.lane.run_later.is_empty() else { panic() } + // `pop_front` leaves each vacated slot of the run queue pointing at the + // coroutine it held, and every coroutine points back at this lane. Replace + // the buffer so that cycle does not keep the finished task alive. + task_state.lane.run_later.shrink_to_fit() drop_task_wakeup(task_state) guard task_state.subscribers.is_empty() else { panic() } component_tasks.remove(waitable_set) diff --git a/crates/moonbit/src/async_support.rs b/crates/moonbit/src/async_support.rs index 11a1bc572..599637891 100644 --- a/crates/moonbit/src/async_support.rs +++ b/crates/moonbit/src/async_support.rs @@ -2541,6 +2541,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { &mut self, func: &Function, operands: &[String], + params_block: Option<&str>, results: &mut Vec, ) { let name = self.interface_gen.world_gen.pkg_resolver.func_call( @@ -2550,7 +2551,27 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { ); let task_return_name = format!("{}_task_return", func.name.to_moonbit_ident()); - let mut args = operands.to_vec(); + let mut args = match params_block { + Some(ptr) => { + let args = operands + .iter() + .map(|op| { + let arg = self.locals.tmp("arg"); + uwriteln!(self.src, "let {arg} = {op}"); + arg + }) + .collect::>(); + // The call below runs to completion before the next line, so a + // free after it would never run for a cancelled call. Free the + // block here, once every argument has been read out of it. + if !self.preserve_guest_allocations { + self.use_ffi(ffi::FREE); + uwriteln!(self.src, "mbt_ffi_free({ptr})"); + } + args + } + None => operands.to_vec(), + }; if matches!(self.interface_gen.direction, Direction::Export) { args.push(background_group_name(func)); } diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 9c3142b89..ed66df842 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -2343,9 +2343,14 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.commit_sync_import_arguments(sig, &call_operands); } - Instruction::CallInterface { func, async_ } => { + Instruction::CallInterface { + func, + async_, + params_block, + } => { if *async_ { - self.emit_async_call_interface(func, operands, results); + let block = params_block.map(|_| operands.pop().unwrap()); + self.emit_async_call_interface(func, operands, block.as_deref(), results); return; } @@ -3172,6 +3177,7 @@ fn print_docs(src: &mut String, docs: &Docs) { #[cfg(test)] mod tests { use super::*; + use wit_bindgen_core::wit_parser::WorldItem; fn try_generate_with_opts(wit: &str, world: &str, opts: Opts) -> Result { let mut resolve = Resolve::default(); @@ -3312,6 +3318,93 @@ mod tests { assert!(ffi.contains("extern \"wasm\" fn mbt_ffi_malloc"), "{ffi}"); } + // 18 flat values: the arguments of `many-arguments` arrive in a block of + // the callee's memory, which the callee must free. + const ASYNC_EXPORT_INDIRECT_PARAMS: &str = r#" + package my:test; + + interface i { + many-arguments: async func( + a: string, + b: string, + c: string, + d: string, + e: string, + f: string, + g: string, + h: string, + i: string, + ); + } + + world test { export i; } + "#; + + /// Lifts the arguments of `func` and calls it the way an async export + /// wrapper does, with guest allocations preserved, and returns the code. + fn async_export_body_preserving_allocations(wit: &str, world: &str, func: &str) -> String { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str("test.wit", wit).unwrap(); + let world = resolve.select_world(&[pkg], Some(world)).unwrap(); + let mut generator = MoonBit { + opts: Opts { + gen_dir: "gen".into(), + ..Opts::default() + }, + ..MoonBit::default() + }; + generator.preprocess(&resolve, world).unwrap(); + let (key, id) = resolve.worlds[world] + .exports + .iter() + .find_map(|(key, item)| match item { + WorldItem::Interface { id, .. } => Some((key.clone(), *id)), + _ => None, + }) + .unwrap(); + let func = &resolve.interfaces[id].functions[func]; + let mut r#gen = generator.interface(&resolve, "i", Direction::Export, Some(&key)); + let mut bindgen = FunctionBindgen::new(&mut r#gen, Box::new(["p0".to_string()])); + bindgen.preserve_guest_allocations = true; + abi::call( + &resolve, + AbiVariant::GuestExportAsync, + LiftLower::LiftArgsLowerResults, + func, + &mut bindgen, + true, + ); + bindgen.src + } + + #[test] + fn async_export_indirect_params_frees_block() { + let files = generate(ASYNC_EXPORT_INDIRECT_PARAMS, "test"); + let ffi = file(&files, "gen/interface/my/test/i/ffi.mbt"); + assert!(ffi.contains("extern \"wasm\" fn mbt_ffi_free"), "{ffi}"); + + let start = ffi.find("pub fn wasmExportManyArguments(").unwrap(); + let wrapper = &ffi[start..]; + let wrapper = &wrapper[..wrapper.find("///|").unwrap()]; + // Every argument is bound before the block is freed, and the call + // comes after the free. + assert_eq!(wrapper.matches("let arg").count(), 9, "{wrapper}"); + assert_eq!(wrapper.matches("mbt_ffi_free(").count(), 1, "{wrapper}"); + let last_argument = wrapper.rfind("let arg").unwrap(); + let free = wrapper.find("mbt_ffi_free(p0)").expect(wrapper); + let call = wrapper.find("many_arguments(").unwrap(); + assert!(last_argument < free && free < call, "{wrapper}"); + + let preserved = async_export_body_preserving_allocations( + ASYNC_EXPORT_INDIRECT_PARAMS, + "test", + "many-arguments", + ); + assert_eq!(preserved.matches("let arg").count(), 9, "{preserved}"); + assert!(preserved.contains("many_arguments("), "{preserved}"); + assert!(!preserved.contains("mbt_ffi_free("), "{preserved}"); + } + #[test] fn endpoint_payload_lowering_reserves_destination_pointer() { let files = generate( diff --git a/crates/rust/src/bindgen.rs b/crates/rust/src/bindgen.rs index f639b8abb..86f0afb2d 100644 --- a/crates/rust/src/bindgen.rs +++ b/crates/rust/src/bindgen.rs @@ -51,6 +51,15 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { } } + fn deallocate(&mut self, ptr: &str, size: ArchitectureSize, align: Alignment) { + let dealloc = self.r#gen.path_to_cabi_dealloc(); + self.push_str(&format!( + "{dealloc}({ptr}, {size}, {align});\n", + size = size.format_term(POINTER_SIZE_EXPRESSION, true), + align = align.format(POINTER_SIZE_EXPRESSION) + )); + } + fn cleanup(&mut self, cleanup_value: &str) { if self.block_storage.len() > 0 { self.needs_cleanup_list = true; @@ -940,7 +949,12 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.push_str(");\n"); } - Instruction::CallInterface { func, async_ } => { + Instruction::CallInterface { + func, + async_, + params_block, + } => { + let block = params_block.map(|info| (operands.pop().unwrap(), info)); let prev_src = mem::replace(self.src.as_mut_string(), String::new()); self.let_results(usize::from(func.result.is_some()), results); @@ -969,7 +983,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.src.push_str(&decl); } self.push_str(&prev_src); - let constructor_type = match &func.kind { + let (mut call, prefix, suffix) = match &func.kind { FunctionKind::Freestanding | FunctionKind::AsyncFreestanding | FunctionKind::Getter @@ -978,19 +992,21 @@ impl Bindgen for FunctionBindgen<'_, '_> { | FunctionKind::Static(_) | FunctionKind::StaticGetter(_) | FunctionKind::AsyncMethod(_) - | FunctionKind::AsyncStatic(_) => { - self.push_str(&format!("T_::{}", to_rust_ident(func.item_name()))); - None - } + | FunctionKind::AsyncStatic(_) => ( + format!("T_::{}", to_rust_ident(func.item_name())), + String::new(), + String::new(), + ), FunctionKind::Setter | FunctionKind::MethodSetter(_) - | FunctionKind::StaticSetter(_) => { - self.push_str(&format!( + | FunctionKind::StaticSetter(_) => ( + format!( "T_::{}", to_rust_ident(&format!("set-{}", func.item_name())) - )); - None - } + ), + String::new(), + String::new(), + ), FunctionKind::Constructor(ty) => { let return_type = classify_constructor_return_type(resolve, *ty, &func.result); @@ -1001,24 +1017,26 @@ impl Bindgen for FunctionBindgen<'_, '_> { .to_upper_camel_case(); match return_type { - ConstructorReturnType::Self_ => { - self.push_str(&format!("{ty}::new(T_::new")); - } - ConstructorReturnType::Result { .. } => { - self.push_str(&format!("T_::new")); - } + ConstructorReturnType::Self_ => ( + "T_::new".to_string(), + format!("{ty}::new("), + ")".to_string(), + ), + ConstructorReturnType::Result { .. } => ( + "T_::new".to_string(), + String::new(), + format!(".map({ty}::new)"), + ), } - - Some((ty, return_type)) } }; - self.push_str("("); + call.push_str("("); for (i, operand) in operands.iter().enumerate() { if i > 0 { - self.push_str(", "); + call.push_str(", "); } - self.push_str(operand); + call.push_str(operand); // Automatically convert `Borrow<'_, AResource>` to // `&Self` since traits have `&self` as their @@ -1032,19 +1050,27 @@ impl Bindgen for FunctionBindgen<'_, '_> { | FunctionKind::MethodSetter(_) ) { - self.push_str(".get()") + call.push_str(".get()") } } - self.push_str(")"); - if *async_ { - self.push_str(".await"); - } - match constructor_type { - Some((_, ConstructorReturnType::Self_)) => self.push_str(")"), - Some((ty, ConstructorReturnType::Result { .. })) => { - self.push_str(&format!(".map({ty}::new)")) + call.push_str(")"); + let await_ = if *async_ { ".await" } else { "" }; + match block { + // The arguments of this async export arrived in a block of + // memory. Creating the future reads every argument out of + // that block, so free it before the first `.await`. Then a + // call that is cancelled while it waits frees it too. Core + // announces the block only for async exports today, and + // this arm frees it for any call that carries one. + Some((ptr, (size, align))) => { + let tmp = self.tmp(); + uwriteln!(self.src, "let call{tmp} = {call};"); + self.deallocate(&ptr, size, align); + self.push_str(&format!("{prefix}call{tmp}{await_}{suffix}")); + } + None => { + self.push_str(&format!("{prefix}{call}{await_}{suffix}")); } - None => {} } self.push_str("\n};\n"); } @@ -1257,13 +1283,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { Instruction::Malloc { .. } => unimplemented!(), Instruction::GuestDeallocate { size, align } => { - let dealloc = self.r#gen.path_to_cabi_dealloc(); - self.push_str(&format!( - "{dealloc}({op}, {size}, {align});\n", - op = operands[0], - size = size.format_term(POINTER_SIZE_EXPRESSION, true), - align = align.format(POINTER_SIZE_EXPRESSION) - )); + self.deallocate(&operands[0], *size, *align); } Instruction::GuestDeallocateString => { diff --git a/crates/test/src/go.rs b/crates/test/src/go.rs index 535b6918c..f5973f9df 100644 --- a/crates/test/src/go.rs +++ b/crates/test/src/go.rs @@ -53,7 +53,8 @@ impl LanguageMethods for Go { let pkg_mode = args.iter().any(|a| a == "--pkg-name"); if !pkg_mode { return name.starts_with("async-trait-function.wit") - || name.starts_with("issue-1598.wit"); + || name.starts_with("issue-1598.wit") + || name.starts_with("async-many-arguments.wit"); } } diff --git a/tests/codegen/async-many-arguments.wit b/tests/codegen/async-many-arguments.wit new file mode 100644 index 000000000..5cf51f114 --- /dev/null +++ b/tests/codegen/async-many-arguments.wit @@ -0,0 +1,67 @@ +//@ async = true + +// An async export with more than 16 flat parameter values gets its arguments +// in one block of memory, which the callee frees once it has read them. +// `direct-values` stays under that limit, so it has no block. + +package my:test; + +interface i { + record big-record { + f0: u32, f1: u32, f2: u32, f3: u32, f4: u32, f5: u32, f6: u32, f7: u32, f8: u32, + f9: u32, f10: u32, f11: u32, f12: u32, f13: u32, f14: u32, f15: u32, f16: u32, + } + + // 18 flat values: the arguments arrive in a block. + many-arguments: async func( + a: string, + b: string, + c: string, + d: string, + e: string, + f: string, + g: string, + h: string, + i: string, + ); + + // 15 flat values: direct parameters, no block to free. (16 would be direct + // too, but Go cannot export an async function with 16 flat values.) + direct-values: async func(a: string, b: string, c: string, d: string, + e: string, f: string, g: string, h: u32); + + // 17 flat values in one parameter. + one-big-record: async func(r: big-record); + + // 18 flat values: nine lists, each read out of the block. + many-lists: async func( + a: list, + b: list, + c: list, + d: list, + e: list, + f: list, + g: list, + h: list, + i: list, + ); + + resource r { + // 19 flat values counting `self`: a method whose arguments arrive in a block. + many: async func( + a: string, + b: string, + c: string, + d: string, + e: string, + f: string, + g: string, + h: string, + i: string, + ); + } +} + +world test { + export i; +} diff --git a/tests/runtime/async-many-arguments/runner.rs b/tests/runtime/async-many-arguments/runner.rs new file mode 100644 index 000000000..fa222c936 --- /dev/null +++ b/tests/runtime/async-many-arguments/runner.rs @@ -0,0 +1,135 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +include!(env!("BINDINGS")); + +use crate::my::test::i::*; +use futures::task::noop_waker_ref; +use std::future::Future; +use std::task::Context; + +struct Component; + +export!(Component); + +const N: usize = 2000; + +macro_rules! check_no_leak { + ($name:expr, $body:block) => {{ + println!("{}", $name); + for _ in 0..N $body + let before = heap_mark(); + for _ in 0..N $body + assert_eq!(heap_mark(), before, "{}", $name); + }}; +} + +impl Guest for Component { + async fn run() { + check_no_leak!("finished calls free their arguments", { + many_arguments( + "a".into(), + "b".into(), + "c".into(), + "d".into(), + "e".into(), + "f".into(), + "g".into(), + "h".into(), + "i".into(), + ) + .await; + }); + + let mut k = 0; + check_no_leak!("payloads of options and results arrive intact", { + if k % 2 == 0 { + many_arguments_cases( + Some(42), + Err("error".into()), + "c".into(), + "d".into(), + "e".into(), + "f".into(), + "g".into(), + "h".into(), + ) + .await; + } else { + many_arguments_cases( + None, + Ok(7), + "c".into(), + "d".into(), + "e".into(), + "f".into(), + "g".into(), + "h".into(), + ) + .await; + } + k += 1; + }); + + check_no_leak!("a record in one parameter", { + one_big_record(BigRecord { + f0: 0, + f1: 1, + f2: 2, + f3: 3, + f4: 4, + f5: 5, + f6: 6, + f7: 7, + f8: 8, + f9: 9, + f10: 10, + f11: 11, + f12: 12, + f13: 13, + f14: 14, + f15: 15, + f16: 16, + }) + .await; + }); + + check_no_leak!("direct parameters need no block", { + direct_values( + "a".into(), + "b".into(), + "c".into(), + "d".into(), + "e".into(), + "f".into(), + "g".into(), + 8, + ) + .await; + }); + + if supports_cancel() { + check_no_leak!("cancelled calls free their arguments", { + let (tx, rx) = wit_future::new(|| unreachable!()); + let mut call = Box::pin(many_arguments_pending( + rx, + "a".into(), + "b".into(), + "c".into(), + "d".into(), + "e".into(), + "f".into(), + "g".into(), + "h".into(), + )); + assert!(call + .as_mut() + .poll(&mut Context::from_waker(noop_waker_ref())) + .is_pending()); + drop(call); + tx.write(()).await.unwrap_err(); + }); + } else { + println!("cancelled calls: this component cannot cancel, skipped"); + } + } +} diff --git a/tests/runtime/async-many-arguments/test.c b/tests/runtime/async-many-arguments/test.c new file mode 100644 index 000000000..33d35721b --- /dev/null +++ b/tests/runtime/async-many-arguments/test.c @@ -0,0 +1,180 @@ +//@ args = '--rename my:test/i=test' + +#include +#include +#include +#include + +// Checks that `s` holds the one letter `c`, then frees it: the callee owns +// the bytes of every string it receives. +static void expect_letter(test_string_t *s, char c) { + assert(s->len == 1); + assert(s->ptr[0] == (uint8_t) c); + test_string_free(s); +} + +test_callback_code_t exports_test_many_arguments( + test_string_t *a, test_string_t *b, test_string_t *c, test_string_t *d, + test_string_t *e, test_string_t *f, test_string_t *g, test_string_t *h, + test_string_t *i) { + expect_letter(a, 'a'); + expect_letter(b, 'b'); + expect_letter(c, 'c'); + expect_letter(d, 'd'); + expect_letter(e, 'e'); + expect_letter(f, 'f'); + expect_letter(g, 'g'); + expect_letter(h, 'h'); + expect_letter(i, 'i'); + exports_test_many_arguments_return(); + return TEST_CALLBACK_CODE_EXIT; +} + +test_callback_code_t exports_test_many_arguments_callback(test_event_t *event) { + assert(0); +} + +struct my_task { + test_waitable_set_t set; + exports_test_future_void_t future; +}; + +#ifdef __wasm_libcall_thread_context__ +static _Thread_local struct my_task *current_task = NULL; + +static void set_task(struct my_task *task) { + current_task = task; +} + +static struct my_task *get_task(void) { + return current_task; +} +#else +static void set_task(struct my_task *task) { + test_context_set_0(task); +} + +static struct my_task *get_task(void) { + return (struct my_task*) test_context_get_0(); +} +#endif + +test_callback_code_t exports_test_many_arguments_pending( + exports_test_future_void_t x, test_string_t *a, test_string_t *b, + test_string_t *c, test_string_t *d, test_string_t *e, test_string_t *f, + test_string_t *g, test_string_t *h) { + expect_letter(a, 'a'); + expect_letter(b, 'b'); + expect_letter(c, 'c'); + expect_letter(d, 'd'); + expect_letter(e, 'e'); + expect_letter(f, 'f'); + expect_letter(g, 'g'); + expect_letter(h, 'h'); + + struct my_task *task = (struct my_task*) malloc(sizeof(struct my_task)); + assert(task != NULL); + test_waitable_status_t status = exports_test_future_void_read(x); + assert(status == TEST_WAITABLE_STATUS_BLOCKED); + task->future = x; + task->set = test_waitable_set_new(); + test_waitable_join(task->future, task->set); + + assert(get_task() == NULL); + set_task(task); + return TEST_CALLBACK_CODE_WAIT(task->set); +} + +test_callback_code_t exports_test_many_arguments_pending_callback(test_event_t *event) { + struct my_task *task = get_task(); + set_task(NULL); + test_waitable_join(task->future, 0); + if (event->event == TEST_EVENT_CANCEL) { + assert(event->waitable == 0); + assert(event->code == 0); + + test_waitable_status_t status = exports_test_future_void_cancel_read(task->future); + assert(TEST_WAITABLE_STATE(status) == TEST_WAITABLE_CANCELLED); + assert(TEST_WAITABLE_COUNT(status) == 0); + test_task_cancel(); + } else { + assert(event->event == TEST_EVENT_FUTURE_READ); + assert(event->waitable == task->future); + assert(TEST_WAITABLE_STATE(event->code) == TEST_WAITABLE_COMPLETED); + assert(TEST_WAITABLE_COUNT(event->code) == 0); + exports_test_many_arguments_pending_return(); + } + + exports_test_future_void_drop_readable(task->future); + test_waitable_set_drop(task->set); + + free(task); + + return TEST_CALLBACK_CODE_EXIT; +} + +test_callback_code_t exports_test_many_arguments_cases( + uint32_t *maybe_a, exports_test_result_u32_string_t *b, test_string_t *c, + test_string_t *d, test_string_t *e, test_string_t *f, test_string_t *g, + test_string_t *h) { + bool some_and_err = maybe_a != NULL && *maybe_a == 42 && b->is_err && + b->val.err.len == 5 && + memcmp(b->val.err.ptr, "error", 5) == 0; + bool none_and_ok = maybe_a == NULL && !b->is_err && b->val.ok == 7; + assert(some_and_err || none_and_ok); + // Frees the error string when there is one. + exports_test_result_u32_string_free(b); + expect_letter(c, 'c'); + expect_letter(d, 'd'); + expect_letter(e, 'e'); + expect_letter(f, 'f'); + expect_letter(g, 'g'); + expect_letter(h, 'h'); + exports_test_many_arguments_cases_return(); + return TEST_CALLBACK_CODE_EXIT; +} + +test_callback_code_t exports_test_many_arguments_cases_callback(test_event_t *event) { + assert(0); +} + +test_callback_code_t exports_test_one_big_record(exports_test_big_record_t *r) { + assert(r->f0 == 0); + assert(r->f8 == 8); + assert(r->f16 == 16); + exports_test_one_big_record_return(); + return TEST_CALLBACK_CODE_EXIT; +} + +test_callback_code_t exports_test_one_big_record_callback(test_event_t *event) { + assert(0); +} + +test_callback_code_t exports_test_direct_values( + test_string_t *a, test_string_t *b, test_string_t *c, test_string_t *d, + test_string_t *e, test_string_t *f, test_string_t *g, uint32_t h) { + expect_letter(a, 'a'); + expect_letter(b, 'b'); + expect_letter(c, 'c'); + expect_letter(d, 'd'); + expect_letter(e, 'e'); + expect_letter(f, 'f'); + expect_letter(g, 'g'); + assert(h == 8); + exports_test_direct_values_return(); + return TEST_CALLBACK_CODE_EXIT; +} + +test_callback_code_t exports_test_direct_values_callback(test_event_t *event) { + assert(0); +} + +// Memory size in pages. Without the leak it stops growing once the warm-up +// calls are done. +uint32_t exports_test_heap_mark(void) { + return __builtin_wasm_memory_size(0); +} + +bool exports_test_supports_cancel(void) { + return true; +} diff --git a/tests/runtime/async-many-arguments/test.cs b/tests/runtime/async-many-arguments/test.cs new file mode 100644 index 000000000..0ce1ad0ba --- /dev/null +++ b/tests/runtime/async-many-arguments/test.cs @@ -0,0 +1,111 @@ +using System.Runtime.InteropServices; + +namespace TestWorld.wit.Exports.my.test +{ + public class IExportsImpl : IIExports + { + static void Expect(string actual, string expected) + { + if (actual != expected) + { + throw new Exception($"expected \"{expected}\", got \"{actual}\""); + } + } + + public static async Task ManyArguments( + string a, string b, string c, string d, string e, string f, string g, string h, + string i) + { + Expect(a, "a"); + Expect(b, "b"); + Expect(c, "c"); + Expect(d, "d"); + Expect(e, "e"); + Expect(f, "f"); + Expect(g, "g"); + Expect(h, "h"); + Expect(i, "i"); + await Task.CompletedTask; + } + + public static async Task ManyArgumentsPending( + FutureReader x, string a, string b, string c, string d, string e, string f, + string g, string h) + { + Expect(a, "a"); + Expect(b, "b"); + Expect(c, "c"); + Expect(d, "d"); + Expect(e, "e"); + Expect(f, "f"); + Expect(g, "g"); + Expect(h, "h"); + // The caller drops this call while it waits here. + await x.Read(); + } + + public static async Task ManyArgumentsCases( + uint? a, Result b, string c, string d, string e, string f, + string g, string h) + { + bool someAndErr = a == 42 && b.IsErr && b.AsErr == "error"; + bool noneAndOk = a == null && b.IsOk && b.AsOk == 7; + if (!someAndErr && !noneAndOk) + { + throw new Exception("unexpected option or result"); + } + Expect(c, "c"); + Expect(d, "d"); + Expect(e, "e"); + Expect(f, "f"); + Expect(g, "g"); + Expect(h, "h"); + await Task.CompletedTask; + } + + public static async Task OneBigRecord(IIExports.BigRecord r) + { + if (r.f0 != 0 || r.f8 != 8 || r.f16 != 16) + { + throw new Exception("unexpected record"); + } + await Task.CompletedTask; + } + + public static async Task DirectValues( + string a, string b, string c, string d, string e, string f, string g, uint h) + { + Expect(a, "a"); + Expect(b, "b"); + Expect(c, "c"); + Expect(d, "d"); + Expect(e, "e"); + Expect(f, "f"); + Expect(g, "g"); + if (h != 8) + { + throw new Exception($"expected 8, got {h}"); + } + await Task.CompletedTask; + } + + // The end of the memory that malloc hands out. A block that is never + // freed makes it grow; once the component has warmed up it stays put. + [DllImport("*")] + static extern nint sbrk(nint increment); + + public static uint HeapMark() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + return (uint)sbrk(0); + } + + // The C# async support does not handle a cancelled export yet. + public static bool SupportsCancel() + { + return false; + } + } +} diff --git a/tests/runtime/async-many-arguments/test.go b/tests/runtime/async-many-arguments/test.go new file mode 100644 index 000000000..9b65d8da2 --- /dev/null +++ b/tests/runtime/async-many-arguments/test.go @@ -0,0 +1,166 @@ +package export_my_test_i + +import ( + "runtime" + "wit_component/my_test_i" + + . "go.bytecodealliance.org/pkg/wit/types" +) + +func ManyArguments(a, b, c, d, e, f, g, h, i string) { + if a != "a" { + panic("a") + } + if b != "b" { + panic("b") + } + if c != "c" { + panic("c") + } + if d != "d" { + panic("d") + } + if e != "e" { + panic("e") + } + if f != "f" { + panic("f") + } + if g != "g" { + panic("g") + } + if h != "h" { + panic("h") + } + if i != "i" { + panic("i") + } +} + +func ManyArgumentsPending(x *FutureReader[Unit], a, b, c, d, e, f, g, h string) { + if a != "a" { + panic("a") + } + if b != "b" { + panic("b") + } + if c != "c" { + panic("c") + } + if d != "d" { + panic("d") + } + if e != "e" { + panic("e") + } + if f != "f" { + panic("f") + } + if g != "g" { + panic("g") + } + if h != "h" { + panic("h") + } + defer x.Drop() + x.Read() +} + +func ManyArgumentsCases(a Option[uint32], b Result[uint32, string], c, d, e, f, g, h string) { + // Either (Some(42), Err("error")) or (None, Ok(7)). + switch a.Tag() { + case OptionSome: + if a.Some() != 42 { + panic("a") + } + if b.Tag() != ResultErr || b.Err() != "error" { + panic("b") + } + case OptionNone: + if b.Tag() != ResultOk || b.Ok() != 7 { + panic("b") + } + default: + panic("a") + } + if c != "c" { + panic("c") + } + if d != "d" { + panic("d") + } + if e != "e" { + panic("e") + } + if f != "f" { + panic("f") + } + if g != "g" { + panic("g") + } + if h != "h" { + panic("h") + } +} + +func OneBigRecord(r my_test_i.BigRecord) { + if r.F0 != 0 { + panic("f0") + } + if r.F8 != 8 { + panic("f8") + } + if r.F16 != 16 { + panic("f16") + } +} + +func DirectValues(a, b, c, d, e, f, g string, h uint32) { + if a != "a" { + panic("a") + } + if b != "b" { + panic("b") + } + if c != "c" { + panic("c") + } + if d != "d" { + panic("d") + } + if e != "e" { + panic("e") + } + if f != "f" { + panic("f") + } + if g != "g" { + panic("g") + } + if h != 8 { + panic("h") + } +} + +// The first GC starts a mark worker, which takes the goroutine finished calls +// reuse, and every GC drops the runtime's cached Pinner, which its finalizer +// keeps on the heap one GC longer. Do both here, before any call, so the first +// reading holds what every later one does. +func init() { + var p runtime.Pinner + p.Pin(new(byte)) + p.Unpin() + runtime.GC() +} + +func HeapMark() uint32 { + runtime.GC() + var m runtime.MemStats + runtime.ReadMemStats(&m) + return uint32(m.HeapAlloc) +} + +// The Go async runtime has no case for a cancelled export yet. +func SupportsCancel() bool { + return false +} diff --git a/tests/runtime/async-many-arguments/test.mbt b/tests/runtime/async-many-arguments/test.mbt new file mode 100644 index 000000000..8f16881fb --- /dev/null +++ b/tests/runtime/async-many-arguments/test.mbt @@ -0,0 +1,102 @@ +//@ [lang] +//@ path = 'gen/interface/my/test/i/stub.mbt' + +///| +pub async fn many_arguments( + a : String, + b : String, + c : String, + d : String, + e : String, + f : String, + g : String, + h : String, + i : String, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + assert_eq([a, b, c, d, e, f, g, h, i], [ + "a", "b", "c", "d", "e", "f", "g", "h", "i", + ]) +} + +///| +pub async fn many_arguments_pending( + x : @async-core.Future[Unit], + a : String, + b : String, + c : String, + d : String, + e : String, + f : String, + g : String, + h : String, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + assert_eq([a, b, c, d, e, f, g, h], ["a", "b", "c", "d", "e", "f", "g", "h"]) + x.get() +} + +///| +pub async fn many_arguments_cases( + a : UInt?, + b : Result[UInt, String], + c : String, + d : String, + e : String, + f : String, + g : String, + h : String, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + // The caller sends either `(Some(42), Err("error"))` or `(None, Ok(7))`. + match a { + Some(_) => { + assert_eq(a, Some(42)) + assert_eq(b, Err("error")) + } + None => assert_eq(b, Ok(7)) + } + assert_eq([c, d, e, f, g, h], ["c", "d", "e", "f", "g", "h"]) +} + +///| +pub async fn one_big_record( + r : BigRecord, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + assert_eq(r.f0, 0) + assert_eq(r.f8, 8) + assert_eq(r.f16, 16) +} + +///| +pub async fn direct_values( + a : String, + b : String, + c : String, + d : String, + e : String, + f : String, + g : String, + h : UInt, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + assert_eq([a, b, c, d, e, f, g], ["a", "b", "c", "d", "e", "f", "g"]) + assert_eq(h, 8U) +} + +///| +extern "wasm" fn memory_size() -> Int = + #|(func (result i32) memory.size) + +///| +/// Memory size in pages. Without the leak it stops growing once the warm-up +/// calls are done. +pub fn heap_mark() -> UInt { + memory_size().reinterpret_as_uint() +} + +///| +pub fn supports_cancel() -> Bool { + true +} diff --git a/tests/runtime/async-many-arguments/test.rs b/tests/runtime/async-many-arguments/test.rs new file mode 100644 index 000000000..3c2b4e771 --- /dev/null +++ b/tests/runtime/async-many-arguments/test.rs @@ -0,0 +1,97 @@ +use wit_bindgen::FutureReader; + +include!(env!("BINDINGS")); + +struct Component; + +export!(Component); + +#[path = "../lists/alloc.rs"] +mod alloc; + +use crate::exports::my::test::i::BigRecord; + +impl crate::exports::my::test::i::Guest for Component { + async fn many_arguments( + a: String, + b: String, + c: String, + d: String, + e: String, + f: String, + g: String, + h: String, + i: String, + ) { + assert_eq!( + [a, b, c, d, e, f, g, h, i], + ["a", "b", "c", "d", "e", "f", "g", "h", "i"] + ); + } + + async fn many_arguments_pending( + x: FutureReader<()>, + a: String, + b: String, + c: String, + d: String, + e: String, + f: String, + g: String, + h: String, + ) { + assert_eq!( + [a, b, c, d, e, f, g, h], + ["a", "b", "c", "d", "e", "f", "g", "h"] + ); + x.await + } + + async fn many_arguments_cases( + a: Option, + b: Result, + c: String, + d: String, + e: String, + f: String, + g: String, + h: String, + ) { + assert!( + matches!((a, &b), (Some(42), Err(e)) if e == "error") + || matches!((a, &b), (None, Ok(7))) + ); + assert_eq!([c, d, e, f, g, h], ["c", "d", "e", "f", "g", "h"]); + } + + async fn one_big_record(r: BigRecord) { + assert_eq!(r.f0, 0); + assert_eq!(r.f8, 8); + assert_eq!(r.f16, 16); + } + + async fn direct_values( + a: String, + b: String, + c: String, + d: String, + e: String, + f: String, + g: String, + h: u32, + ) { + assert_eq!( + [a, b, c, d, e, f, g], + ["a", "b", "c", "d", "e", "f", "g"] + ); + assert_eq!(h, 8); + } + + fn heap_mark() -> u32 { + alloc::get().try_into().unwrap() + } + + fn supports_cancel() -> bool { + true + } +} diff --git a/tests/runtime/async-many-arguments/test.wit b/tests/runtime/async-many-arguments/test.wit new file mode 100644 index 000000000..abd5c6dc3 --- /dev/null +++ b/tests/runtime/async-many-arguments/test.wit @@ -0,0 +1,75 @@ +//@ async = true +package my:test; + +interface i { + record big-record { + f0: u32, f1: u32, f2: u32, f3: u32, f4: u32, f5: u32, f6: u32, f7: u32, f8: u32, + f9: u32, f10: u32, f11: u32, f12: u32, f13: u32, f14: u32, f15: u32, f16: u32, + } + + // 18 flat values: the arguments arrive in a block that the callee must free. + many-arguments: async func( + a: string, + b: string, + c: string, + d: string, + e: string, + f: string, + g: string, + h: string, + i: string, + ); + + // 17 flat values; the call waits on `x`, so the caller can cancel it. + many-arguments-pending: async func( + x: future, + a: string, + b: string, + c: string, + d: string, + e: string, + f: string, + g: string, + h: string, + ); + + // 17 flat values; option and result read their payload only when present. + many-arguments-cases: async func( + a: option, + b: result, + c: string, + d: string, + e: string, + f: string, + g: string, + h: string, + ); + + // 17 flat values in one parameter. + one-big-record: async func(r: big-record); + + // 15 flat values: direct parameters, no block. (16 would be direct too, but Go + // cannot export an async function with 16 flat values.) + direct-values: async func(a: string, b: string, c: string, d: string, + e: string, f: string, g: string, h: u32); + + // A number that grows when the parameter block leaks and stays the same + // when it does not, once the component has warmed up. Exact live bytes in + // Rust, a memory-size reading in other languages. + heap-mark: func() -> u32; + + // Whether this component can handle a call that is cancelled while it + // waits. A component that cannot do that returns false, and the runner + // skips that case for it. + supports-cancel: func() -> bool; +} + +world test { + export i; +} + +world runner { + import i; + + export run: async func(); +}