diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c3465c6a4..8d8ccf5de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,8 +79,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] # moonbit removed from language matrix for now - causing CI failures - # csharp removed from language matrix for now - causing CI failures - lang: [c, rust, cpp, go, d] + lang: [c, rust, csharp, cpp, go, d] exclude: # For now csharp doesn't work on macos, so exclude it from testing. - os: macos-latest @@ -102,7 +101,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.x' + dotnet-version: '10.x' if: matrix.lang == 'csharp' # As of this writing async tests require [a patched build of diff --git a/crates/csharp/src/AsyncSupport.cs b/crates/csharp/src/AsyncSupport.cs index 37c617511..def30c7da 100644 --- a/crates/csharp/src/AsyncSupport.cs +++ b/crates/csharp/src/AsyncSupport.cs @@ -279,7 +279,10 @@ internal LiftingTaskCompletionSource(TaskCompletionSource innerTaskCompleti { SetCanceled(); } - throw new NotImplementedException("LiftingTaskCompletionSource unexpected task status " + t.Status); + else + { + throw new NotImplementedException("LiftingTaskCompletionSource unexpected task status " + t.Status); + } }, TaskContinuationOptions.ExecuteSynchronously); } } @@ -306,7 +309,9 @@ internal LiftingTaskCompletionSource(TaskCompletionSource innerTaskCompleti public delegate uint StreamWrite(int handle, IntPtr buffer, uint length); public delegate uint StreamRead(int handle, IntPtr buffer, uint length); -public delegate Array Lift(IntPtr buffer, Array? resultBuffer); +/// Lifts the first values from canonical ABI memory at +/// into . +public delegate Array Lift(IntPtr buffer, Array resultBuffer, int count); public delegate void Lower(object payload, List cleanups); public delegate uint CancelRead(int handle); public delegate uint CancelWrite(int handle); @@ -475,8 +480,12 @@ internal int TakeHandle() protected unsafe IntPtr GetBuffer(int length, T[]? userBuffer, IVTable vTable, List cleanups) { - // For primitive, blittable types, TODO: this probably does not align 100% with the component ABI? - if (typeof(T).IsPrimitive || typeof(T).IsValueType) + // Primitives have the same layout in C# and the canonical ABI, so their array can be pinned + // and used directly. Any other type (records, variants, results, strings, ...) has a + // different managed layout, and Result<,> and friends hold references so cannot be pinned: + // they go through native memory and the vtable's Lift/Lower, which the generator emits for + // every non-primitive payload. + if (typeof(T).IsPrimitive) { T[] buffer; // For Streams, the user passes the buffer, so use that. @@ -495,23 +504,23 @@ protected unsafe IntPtr GetBuffer(int length, T[]? userBuffer, IVTable vTable else { System.Diagnostics.Debug.Assert(vTable.Size > 0, $"Did not compute size for {typeof(T)}."); - IntPtr bufferPtr = (IntPtr)global::System.Runtime.InteropServices.NativeMemory.AlignedAlloc(vTable.Size, vTable.Align); + IntPtr bufferPtr = (IntPtr)global::System.Runtime.InteropServices.NativeMemory.AlignedAlloc(vTable.Size * (uint)length, vTable.Align); cleanups.Add(() => global::System.Runtime.InteropServices.NativeMemory.Free((void*)bufferPtr)); return bufferPtr; } } + // Lifts one future value. Primitives were read into the pinned array GetBuffer returned, so copy + // that value; anything else goes through the vtable's Lift. protected unsafe T[] LiftBuffer(IntPtr buffer, T[] resultBuffer, Lift? liftFunc) { - // For primitive, blittable types - if (typeof(T).IsPrimitive || typeof(T).IsValueType) + if (typeof(T).IsPrimitive) { - // TODO array length > 1 resultBuffer[0] = *(T*)buffer; } else { - liftFunc(buffer, resultBuffer); + liftFunc!(buffer, resultBuffer, 1); } return resultBuffer; @@ -625,7 +634,7 @@ public ComponentTask Read() { try { - readTask.SetResult(((T[])VTable.Lift(buf, new T[1]))[0]); + readTask.SetResult(LiftBuffer(buf, new T[1], VTable.Lift)[0]); } catch(Exception e) { @@ -712,7 +721,8 @@ public ComponentTask Read(T[] resultBuffer) { if (it.IsCompletedSuccessfully) { - VTable.Lift(buf, resultBuffer); + // Primitives were read straight into resultBuffer, and have no Lift. + VTable.Lift?.Invoke(buf, resultBuffer, it.Result); } else if (!it.IsCanceled) { diff --git a/crates/csharp/src/csproj.rs b/crates/csharp/src/csproj.rs index 76b8c5d57..dbcb4de82 100644 --- a/crates/csharp/src/csproj.rs +++ b/crates/csharp/src/csproj.rs @@ -96,6 +96,14 @@ impl CSProjectLLVMBuilder { + + + + + " ); @@ -105,6 +113,12 @@ impl CSProjectLLVMBuilder { "linux" => std::env::consts::OS, other => todo!("OS {} not supported", other), }; + // The ILCompiler runs on the host. Windows on Arm uses the x64 package (see the + // `_hostArchitecture` workaround in the test runner). + let arch = match (os, std::env::consts::ARCH) { + ("linux", "aarch64") => "arm64", + _ => "x64", + }; // Share nuget packages between codegen and runtime tests. let packages_path = if self.dir.to_str().unwrap().contains("codegen") { @@ -117,7 +131,7 @@ impl CSProjectLLVMBuilder { r#" - + "#), ); diff --git a/crates/csharp/src/function.rs b/crates/csharp/src/function.rs index 9de5852cf..76a1c8604 100644 --- a/crates/csharp/src/function.rs +++ b/crates/csharp/src/function.rs @@ -35,6 +35,8 @@ pub(crate) struct FunctionBindgen<'a, 'b> { parameter_type: ParameterType, pub(crate) result_type: Option, pub(crate) resource_type_name: Option, + /// The function's futures and streams in signature order; see `future_or_stream_index`. + pub(crate) futures_and_streams: Vec, } impl<'a, 'b> FunctionBindgen<'a, 'b> { @@ -73,9 +75,20 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { parameter_type: parameter_type, result_type: result_type, resource_type_name: None, + futures_and_streams: Vec::new(), } } + /// Index of a future or stream type among the function's futures and streams, which the + /// canonical ABI numbers together. Payloads nested inside another future or stream are not in + /// that list and use 0. + fn future_or_stream_index(&self, ty: TypeId) -> usize { + self.futures_and_streams + .iter() + .position(|t| *t == ty) + .unwrap_or(0) + } + fn lower_variant( &mut self, cases: &[(&str, Option)], @@ -315,7 +328,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { let is_async = InterfaceGenerator::is_async(&func.kind); if is_async && !use_await { - uwriteln!(self.src, "Task<{ty}> {ret};"); + uwriteln!(self.src, "global::System.Threading.Tasks.Task<{ty}> {ret};"); } else { uwriteln!(self.src, "{ty} {ret};"); } @@ -358,17 +371,15 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { } let head = oks.concat(); let tail = oks.iter().map(|_| ")").collect::>().concat(); + // Await the export itself; the Result wrapping its value is not awaitable. + let await_ = if use_await { "await " } else { "" }; let val = if payload_is_void { - uwriteln!(self.src, "{target}.{func_name}({oper});"); + uwriteln!(self.src, "{await_}{target}.{func_name}({oper});"); "new None()".to_owned() } else { - format!("{target}.{func_name}({oper})") + format!("({await_}{target}.{func_name}({oper}))") }; - uwriteln!( - self.src, - "{ret} = {}{head}{val}{tail};", - if use_await { "await " } else { "" } - ); + uwriteln!(self.src, "{ret} = {head}{val}{tail};"); if !self.results.is_empty() { self.interface_gen.csharp_gen.needs_wit_exception = true; let cases = cases.join("\n"); @@ -390,46 +401,19 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { ret } - pub fn emit_allocation_for_type(&mut self, results: &[WasmType]) -> String { + /// Allocates native memory for an async import to write its result into, sized and aligned + /// for the result type in the canonical ABI. The caller frees it once the call completes. + pub fn emit_allocation_for_type(&mut self, ty: &Type) -> String { let address = self.locals.tmp("address"); - let buffer_size = self.get_size_for_type(results); - let align = self.get_align_for_type(results); + // TODO: Wasm64 + let size = self.interface_gen.csharp_gen.sizes.size(ty).size_wasm32(); + let align = self.interface_gen.csharp_gen.sizes.align(ty).align_wasm32(); uwriteln!( self.src, - "void* {address} = global::System.Runtime.InteropServices.NativeMemory.AlignedAlloc({buffer_size}, {align});" + "void* {address} = global::System.Runtime.InteropServices.NativeMemory.AlignedAlloc({size}, {align});" ); - - // TODO: Store the address somewhere so we can free it when the task completes. address } - - fn get_size_for_type(&self, results: &[WasmType]) -> usize { - match results { - [WasmType::I32] => 4, - [WasmType::I64] => 8, - [WasmType::F32] => 4, - [WasmType::F64] => 8, - [WasmType::Pointer, WasmType::Length] => 4, // TODO: Wasm64 - [WasmType::PointerOrI64, WasmType::Length] => 8, - _ => { - todo!("other types not yet supported"); - } - } - } - - fn get_align_for_type(&self, results: &[WasmType]) -> usize { - match results { - [WasmType::I32] => 4, - [WasmType::I64] => 8, - [WasmType::F32] => 4, - [WasmType::F64] => 8, - [WasmType::Pointer, WasmType::Length] => 4, // TODO: Wasm64 - [WasmType::PointerOrI64, WasmType::Length] => 8, - _ => { - todo!("other types not yet supported"); - } - } - } } impl Bindgen for FunctionBindgen<'_, '_> { @@ -1166,26 +1150,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { let func_name = self.func_name.to_upper_camel_case(); let operands = operands.join(", "); - let (_namespace, interface_name) = - &CSharp::get_class_name_from_qualified_name(self.interface_gen.name); - let mut interop_name = format!( - "{}ImportsInterop", - interface_name - .strip_prefix("I") - .unwrap() - .strip_suffix(if self.interface_gen.direction == Direction::Import { - "Imports" - } else { - "Exports" - }) - .unwrap() - .to_upper_camel_case() - ); - - if self.interface_gen.is_world && self.interface_gen.direction == Direction::Import - { - interop_name = format!("Imports.{interop_name}"); - } + let interop_name = self.interface_gen.interop_class_name(); let resource_type_name = match self.kind { FunctionKind::Method(resource_type_id) @@ -1250,8 +1215,22 @@ impl Bindgen for FunctionBindgen<'_, '_> { return (int)CallbackCode.Exit; }} + if (task.IsFaulted) + {{ + global::System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw(task.Exception!.InnerException!); + }} + + // After this returns nothing observes the task, so a later failure must not be lost. + task.ContinueWith( + t => global::System.Environment.FailFast("Async export {async_func_name} failed", t.Exception), + global::System.Threading.Tasks.TaskContinuationOptions.OnlyOnFaulted | global::System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously); + // TODO: Defer dropping borrowed resources until a result is returned. ContextTask* contextTaskPtr = AsyncSupport.ContextGet(); + if (contextTaskPtr == null) + {{ + throw new global::System.InvalidOperationException("Async export {async_func_name} is waiting on something other than a component-model waitable, such as Task.Yield or Task.Delay; that is not supported yet."); + }} return (int)CallbackCode.Wait | (int)(contextTaskPtr->WaitableSetHandle << 4); }} @@ -1261,7 +1240,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { // Start the Async function uwriteln!( self.src, - r#"public static async Task {async_func_name}({}) + r#"public static async global::System.Threading.Tasks.Task {async_func_name}({}) {{ var cleanups = new global::System.Collections.Generic.List(); @@ -1368,7 +1347,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { {name}TaskReturn({ret_param}); - }}, TaskContinuationOptions.ExecuteSynchronously); + }}, global::System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously); // TODO: Defer dropping borrowed resources until a result is returned. ContextTask* contextTaskPtr = AsyncSupport.ContextGet(); @@ -1633,8 +1612,8 @@ impl Bindgen for FunctionBindgen<'_, '_> { results.extend(operands.iter().take(*amt).cloned()); } - Instruction::FutureLower { payload, ty: _ } - | Instruction::StreamLower { payload, ty: _ } => { + Instruction::FutureLower { payload, ty } | Instruction::StreamLower { payload, ty } => { + let index = self.future_or_stream_index(*ty); let op = &operands[0]; let (generic_type_name, generic_type_name_with_qualifier) = match payload { Some(generic_type) => { @@ -1653,6 +1632,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { Instruction::FutureLower { .. } => { self.interface_gen.add_future( self.func_name, + index, &generic_type_name, &generic_type_name_with_qualifier, **payload, @@ -1661,6 +1641,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { _ => { self.interface_gen.add_stream( self.func_name, + index, &generic_type_name, &generic_type_name_with_qualifier, **payload, @@ -1693,8 +1674,8 @@ impl Bindgen for FunctionBindgen<'_, '_> { } } - Instruction::FutureLift { payload, ty: _ } - | Instruction::StreamLift { payload, ty: _ } => { + Instruction::FutureLift { payload, ty } | Instruction::StreamLift { payload, ty } => { + let index = self.future_or_stream_index(*ty); let generic_type_name_with_qualifier = match payload { Some(generic_type) => &self .interface_gen @@ -1740,6 +1721,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { Instruction::FutureLift { .. } => { self.interface_gen.add_future( self.func_name, + index, &generic_type_name, &generic_type_name_with_qualifier, **payload, @@ -1748,6 +1730,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { _ => { self.interface_gen.add_stream( self.func_name, + index, &generic_type_name, &generic_type_name_with_qualifier, **payload, diff --git a/crates/csharp/src/interface.rs b/crates/csharp/src/interface.rs index ef8ade04c..594d50f06 100644 --- a/crates/csharp/src/interface.rs +++ b/crates/csharp/src/interface.rs @@ -46,6 +46,9 @@ impl InterfaceTypeAndFragments { #[derive(Clone)] pub(crate) struct FutureInfo { pub name: String, + /// Position among the function's futures and streams together, as numbered in the + /// `[future-*-N]` / `[stream-*-N]` intrinsic import names. + pub index: usize, pub generic_type_name: String, pub qualified_generic_type_name: String, pub ty: Option, @@ -68,6 +71,31 @@ pub(crate) struct InterfaceGenerator<'a> { } impl InterfaceGenerator<'_> { + /// Name of the class holding this interface's `[DllImport]` declarations, relative to the + /// interface's namespace: `FooImportsInterop` for `IFooImports`. + pub(crate) fn interop_class_name(&self) -> String { + let (_namespace, interface_name) = &CSharp::get_class_name_from_qualified_name(self.name); + let interop_name = format!( + "{}ImportsInterop", + interface_name + .strip_prefix("I") + .unwrap() + .strip_suffix(if self.direction == Direction::Import { + "Imports" + } else { + "Exports" + }) + .unwrap() + .to_upper_camel_case() + ); + + if self.is_world && self.direction == Direction::Import { + format!("Imports.{interop_name}") + } else { + interop_name + } + } + pub fn is_async(kind: &FunctionKind) -> bool { matches!( kind, @@ -244,7 +272,6 @@ impl InterfaceGenerator<'_> { (self.streams.iter().cloned().collect(), ", uint length") }; - let mut index = 0; let mut size = 0; let mut align = 0; for future in futures_or_streams { @@ -269,6 +296,7 @@ impl InterfaceGenerator<'_> { let mut lift_func = "null".to_string(); let mut lower_func = "null".to_string(); let future_name = &future.name; + let index = future.index; let generic_type_name = &future.generic_type_name; let upper_camel_future_type = generic_type_name.to_upper_camel_case(); @@ -396,17 +424,23 @@ impl InterfaceGenerator<'_> { if lift_func != "null" && let Some(payload_type) = canonical_payload { - let (lift, result) = self.lift_from_memory("buffer", &payload_type); + let (lift, result) = + self.lift_from_memory("elementAddress", &payload_type, None); + // Values are laid out like a list: `size` is already a multiple of the + // alignment, so it is also the stride. uwrite!( self.csharp_interop_src, r#" - public static unsafe Array {future_stream_name}Lift{upper_camel_future_type}(IntPtr buffer, Array resultBuffer) + public static unsafe Array {future_stream_name}Lift{upper_camel_future_type}(IntPtr buffer, Array resultBuffer, int count) {{ - {lift} + for (int i = 0; i < count; i++) + {{ + IntPtr elementAddress = buffer + i * {size}; + {lift} - // TODO: length > 1 - resultBuffer.SetValue({result}, 0); + resultBuffer.SetValue({result}, i); + }} return resultBuffer; }} "# @@ -432,7 +466,7 @@ impl InterfaceGenerator<'_> { uwrite!( self.csharp_interop_src, r#" - public static unsafe void {future_stream_name}Lower{upper_camel_future_type}(object toLower, List cleanups) + public static unsafe void {future_stream_name}Lower{upper_camel_future_type}(object toLower, global::System.Collections.Generic.List cleanups) {{ var ptr = InteropReturnArea.returnArea.AddressOfReturnArea(); var typedToLower = ({qualified_generic_type_name})toLower; @@ -465,7 +499,6 @@ impl InterfaceGenerator<'_> { } generated_future_types.insert(canonical_payload); - index = index + 1; } self.csharp_gen.needs_async_support = true; @@ -668,9 +701,10 @@ impl InterfaceGenerator<'_> { let asyncified_type = match &func.kind { FunctionKind::AsyncFreestanding | FunctionKind::AsyncStatic(_) - | FunctionKind::AsyncMethod(_) => match func.result { - None => "Task".to_string(), - Some(_ty) => format!("Task<{base_type}>"), + // A `result` without payloads lowers to `void` too, which `Task<>` cannot wrap. + | FunctionKind::AsyncMethod(_) => match base_type.as_str() { + "void" => "global::System.Threading.Tasks.Task".to_string(), + _ => format!("global::System.Threading.Tasks.Task<{base_type}>"), }, _ => base_type, }; @@ -687,6 +721,7 @@ impl InterfaceGenerator<'_> { results: &[TypeId], parameter_type: ParameterType, ) -> (String, String) { + let interop_name = self.interop_class_name(); let mut bindgen = FunctionBindgen::new( self, func.item_name(), @@ -706,6 +741,7 @@ impl InterfaceGenerator<'_> { parameter_type, func.result, ); + bindgen.futures_and_streams = func.find_futures_and_streams(bindgen.interface_gen.resolve); let async_ = InterfaceGenerator::is_async(&func.kind); let mut src: String; @@ -721,12 +757,7 @@ impl InterfaceGenerator<'_> { let async_status_var = "result"; // String::new(); let requires_async_return_buffer_param = func.result.is_some(); let async_return_buffer = if requires_async_return_buffer_param { - let buffer = bindgen.emit_allocation_for_type(&sig.results); - uwriteln!( - bindgen.src, - "//TODO: store somewhere with the TaskCompletionSource, possibly in the state, using Task.AsyncState to retrieve it later." - ); - Some(buffer) + Some(bindgen.emit_allocation_for_type(&func.result.unwrap())) } else { None }; @@ -755,7 +786,7 @@ impl InterfaceGenerator<'_> { (mem::take(&mut bindgen.src), wasm_params) }; let name = func.name.to_upper_camel_case(); - let raw_name = format!("IImportsInterop.{name}WasmInterop.wasmImport{name}"); + let raw_name = format!("{interop_name}.{name}WasmInterop.wasmImport{name}"); let mut wasm_param_refs: Vec<&str> = wasm_params.iter().map(|v| v.as_str()).collect(); @@ -783,7 +814,7 @@ var {async_status_var} = {raw_name}({wasm_params}); let bindgen_src = bindgen.src; if let Some(buffer) = async_return_buffer { let ty = bindgen.result_type.expect("expected a result type"); - let (lift_expr, res) = self.lift_from_memory("address", &ty); + let (lift_expr, res) = self.lift_from_memory("address", &ty, Some(func)); uwriteln!(src, "{}", bindgen_src); let return_type = self.type_name_with_qualifier(&ty, true); @@ -800,10 +831,11 @@ var {async_status_var} = {raw_name}({wasm_params}); var task = AsyncSupport.TaskFromStatus<{return_type}>({async_status_var}, {});", lift_func ); + // The callee writes the result into the buffer when it returns, which may be after + // this method has returned, so free it only once the task has completed (and lifted). uwriteln!( src, - "global::System.Runtime.InteropServices.NativeMemory.Free({});", - buffer + "task.ContinueWith(_ => global::System.Runtime.InteropServices.NativeMemory.AlignedFree({buffer}), global::System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously);" ); uwriteln!(src, "return task;"); } else { @@ -881,6 +913,8 @@ var {async_status_var} = {raw_name}({wasm_params}); func.result, ); + bindgen.futures_and_streams = func.find_futures_and_streams(bindgen.interface_gen.resolve); + let async_ = matches!( func.kind, FunctionKind::AsyncFreestanding @@ -1013,6 +1047,9 @@ var {async_status_var} = {raw_name}({wasm_params}); func.result, ); + bindgen.futures_and_streams = + func.find_futures_and_streams(bindgen.interface_gen.resolve); + abi::post_return(bindgen.interface_gen.resolve, func, &mut bindgen); let src = bindgen.src; @@ -1607,12 +1644,14 @@ var {async_status_var} = {raw_name}({wasm_params}); pub(crate) fn add_future( &mut self, func_name: &str, + index: usize, generic_type_name: &str, qualified_generic_type_name: &str, ty: Option, ) { self.futures.push(FutureInfo { name: func_name.to_string(), + index, generic_type_name: generic_type_name.to_string(), qualified_generic_type_name: qualified_generic_type_name.to_string(), ty: ty, @@ -1622,30 +1661,43 @@ var {async_status_var} = {raw_name}({wasm_params}); pub(crate) fn add_stream( &mut self, func_name: &str, + index: usize, generic_type_name: &str, qualified_generic_type_name: &str, ty: Option, ) { self.streams.push(FutureInfo { name: func_name.to_string(), + index, generic_type_name: generic_type_name.to_string(), qualified_generic_type_name: qualified_generic_type_name.to_string(), ty: ty, }); } - fn lift_from_memory(&mut self, address: &str, ty: &Type) -> (String, String) { + fn lift_from_memory( + &mut self, + address: &str, + ty: &Type, + func: Option<&Function>, + ) -> (String, String) { let boxed: Box<[String]> = Vec::new().into_boxed_slice(); + // Futures and streams in the lifted value are named after, and numbered within, the + // function they belong to. + let futures_and_streams = func + .map(|func| func.find_futures_and_streams(self.resolve)) + .unwrap_or_default(); let mut f = FunctionBindgen::new( self, - "", + func.map_or("", |func| func.item_name()), &FunctionKind::AsyncFreestanding, boxed, vec![], ParameterType::ABI, None, ); + f.futures_and_streams = futures_and_streams; let result = abi::lift_from_memory(f.interface_gen.resolve, &mut f, address.into(), ty); (f.src, result) @@ -1996,9 +2048,10 @@ fn needs_ptr(ty: &Type) -> bool { } } -// Handles the tag being the same name as the variant, which would cause a method with the same name as the type in C# which is not valid. +// Names the factory method for a variant case. A method can't share its name with the variant type, +// or with the `Tag` field and `Tags` class every variant has, so those cases get a suffix. pub fn variant_new_func_name(variant_name: &String, tag: &String) -> String { - if *tag == *variant_name { + if *tag == *variant_name || tag == "Tag" || tag == "Tags" { format!("{tag}_") // Underscores are not valid in wit identifiers so this should be safe. } else { tag.clone() diff --git a/crates/csharp/src/world_generator.rs b/crates/csharp/src/world_generator.rs index 1a8d9b39a..23340ff7e 100644 --- a/crates/csharp/src/world_generator.rs +++ b/crates/csharp/src/world_generator.rs @@ -434,6 +434,7 @@ impl WorldGenerator for CSharp { using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; + using System.Collections.Generic; " ); uwrite!( diff --git a/tests/runtime/async-export-result/runner.rs b/tests/runtime/async-export-result/runner.rs new file mode 100644 index 000000000..a95540f13 --- /dev/null +++ b/tests/runtime/async-export-result/runner.rs @@ -0,0 +1,18 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +include!(env!("BINDINGS")); + +use crate::my::test::i::*; + +struct Component; + +export!(Component); + +impl Guest for Component { + async fn run() { + assert_eq!(unit_ok().await, Ok(())); + assert_eq!(unit_err().await, Err("unit".to_string())); + assert_eq!(halve(8).await, Ok(4)); + assert_eq!(halve(7).await, Err("odd".to_string())); + } +} diff --git a/tests/runtime/async-export-result/test.cs b/tests/runtime/async-export-result/test.cs new file mode 100644 index 000000000..5fe8f5415 --- /dev/null +++ b/tests/runtime/async-export-result/test.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; + +namespace TestWorld.wit.Exports.my.test +{ + public class IExportsImpl : IIExports + { + public static async Task UnitOk() + { + await Task.CompletedTask; + } + + public static async Task UnitErr() + { + await Task.CompletedTask; + throw new WitException("unit", 0); + } + + public static async Task Halve(uint x) + { + await Task.CompletedTask; + if (x % 2 != 0) + { + throw new WitException("odd", 0); + } + return x / 2; + } + } +} diff --git a/tests/runtime/async-export-result/test.rs b/tests/runtime/async-export-result/test.rs new file mode 100644 index 000000000..e98de2e2f --- /dev/null +++ b/tests/runtime/async-export-result/test.rs @@ -0,0 +1,22 @@ +include!(env!("BINDINGS")); + +struct Component; + +export!(Component); + +impl crate::exports::my::test::i::Guest for Component { + async fn unit_ok() -> Result<(), ()> { + Ok(()) + } + + async fn unit_err() -> Result<(), String> { + Err("unit".to_string()) + } + + async fn halve(x: u32) -> Result { + if x % 2 != 0 { + return Err("odd".to_string()); + } + Ok(x / 2) + } +} diff --git a/tests/runtime/async-export-result/test.wit b/tests/runtime/async-export-result/test.wit new file mode 100644 index 000000000..6b7900e2e --- /dev/null +++ b/tests/runtime/async-export-result/test.wit @@ -0,0 +1,19 @@ +//@ async = true +package my:test; + +interface i { + // `result` without payloads, which the C# generator once declared as `Task`. + unit-ok: async func() -> result; + unit-err: async func() -> result<_, string>; + halve: async func(x: u32) -> result; +} + +world test { + export i; +} + +world runner { + import i; + + export run: async func(); +} diff --git a/tests/runtime/async-import-results/runner.cs b/tests/runtime/async-import-results/runner.cs new file mode 100644 index 000000000..ae4220c6e --- /dev/null +++ b/tests/runtime/async-import-results/runner.cs @@ -0,0 +1,42 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using RunnerWorld; +using RunnerWorld.wit.Imports.my.test; + +public class RunnerWorldExportsImpl +{ + public static async Task Run() + { + var echoed = await IIImports.Echo("hi"); + Debug.Assert(echoed == "hi!"); + + var p = await IIImports.MakePoint(3, 4); + Debug.Assert(p.x == 3 && p.y == 4 && p.label == "(3, 4)"); + + // Read with room for every element, so one read can lift several values. + string[] all = ["a", "bb", "ccc"]; + var stream = await IIImports.Words(new List(all)); + var received = new List(); + var buffer = new string[all.Length]; + while (received.Count < all.Length) + { + var read = await stream.Read(buffer); + Debug.Assert(read > 0); + for (var i = 0; i < read; i++) + { + received.Add(buffer[i]); + } + } + Debug.Assert(string.Join(",", received) == "a,bb,ccc"); + stream.Dispose(); + } + + public static int RunCallback() + { + throw new NotImplementedException(); + } +} diff --git a/tests/runtime/async-import-results/runner.rs b/tests/runtime/async-import-results/runner.rs new file mode 100644 index 000000000..8b053c0ab --- /dev/null +++ b/tests/runtime/async-import-results/runner.rs @@ -0,0 +1,22 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +include!(env!("BINDINGS")); + +use crate::my::test::i::*; + +struct Component; + +export!(Component); + +impl Guest for Component { + async fn run() { + assert_eq!(echo("hi".to_string()).await, "hi!"); + + let p = make_point(3, 4).await; + assert_eq!((p.x, p.y, p.label.as_str()), (3, 4, "(3, 4)")); + + let all = vec!["a".to_string(), "bb".to_string(), "ccc".to_string()]; + let rx = words(all.clone()).await; + assert_eq!(rx.collect().await, all); + } +} diff --git a/tests/runtime/async-import-results/test.rs b/tests/runtime/async-import-results/test.rs new file mode 100644 index 000000000..f08eaf909 --- /dev/null +++ b/tests/runtime/async-import-results/test.rs @@ -0,0 +1,34 @@ +use wit_bindgen::StreamReader; + +include!(env!("BINDINGS")); + +use crate::exports::my::test::i::Point; + +struct Component; + +export!(Component); + +impl crate::exports::my::test::i::Guest for Component { + async fn echo(s: String) -> String { + wit_bindgen::yield_async().await; + s + "!" + } + + async fn make_point(x: u32, y: u32) -> Point { + wit_bindgen::yield_async().await; + Point { + x, + y, + label: format!("({x}, {y})"), + } + } + + async fn words(all: Vec) -> StreamReader { + let (mut tx, rx) = wit_stream::new(); + wit_bindgen::spawn_local(async move { + let remaining = tx.write_all(all).await; + assert!(remaining.is_empty()); + }); + rx + } +} diff --git a/tests/runtime/async-import-results/test.wit b/tests/runtime/async-import-results/test.wit new file mode 100644 index 000000000..cce78febb --- /dev/null +++ b/tests/runtime/async-import-results/test.wit @@ -0,0 +1,28 @@ +//@ async = true +package my:test; + +interface i { + record point { + x: u32, + y: u32, + label: string, + } + + // Each returns after yielding, so the caller sees the call pending and reads the result from + // its return buffer later. + echo: async func(s: string) -> string; + make-point: async func(x: u32, y: u32) -> point; + + // Writes the strings it was given to the returned stream. + words: async func(all: list) -> stream; +} + +world test { + export i; +} + +world runner { + import i; + + export run: async func(); +} diff --git a/tests/runtime/stream-param-future-result/runner.cs b/tests/runtime/stream-param-future-result/runner.cs new file mode 100644 index 000000000..ad382dea9 --- /dev/null +++ b/tests/runtime/stream-param-future-result/runner.cs @@ -0,0 +1,34 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using RunnerWorld; +using RunnerWorld.wit.Imports.my.test; + +public class RunnerWorldExportsImpl +{ + public static async Task Run() + { + var (rx, tx) = IIImports.StreamNewByte(); + var result = await IIImports.Consume(rx); + var written = await tx.Write([1, 2, 3]); + Debug.Assert(written == 3); + tx.Dispose(); + var sum = await result.Read(); + Debug.Assert(sum.IsOk && sum.AsOk == 6); + + (rx, tx) = IIImports.StreamNewByte(); + result = await IIImports.Consume(rx); + written = await tx.Write([0]); + Debug.Assert(written == 1); + tx.Dispose(); + var zero = await result.Read(); + Debug.Assert(zero.IsErr && zero.AsErr == "zero"); + } + + public static int RunCallback() + { + throw new NotImplementedException(); + } +} diff --git a/tests/runtime/stream-param-future-result/runner.rs b/tests/runtime/stream-param-future-result/runner.rs new file mode 100644 index 000000000..f8ca96cbf --- /dev/null +++ b/tests/runtime/stream-param-future-result/runner.rs @@ -0,0 +1,28 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +include!(env!("BINDINGS")); + +use crate::my::test::i::*; +use wit_bindgen::StreamResult; + +struct Component; + +export!(Component); + +impl Guest for Component { + async fn run() { + let (mut tx, rx) = wit_stream::new(); + let result = consume(rx).await; + let (written, _) = tx.write(vec![1, 2, 3]).await; + assert_eq!(written, StreamResult::Complete(3)); + drop(tx); + assert_eq!(result.await, Ok(6)); + + let (mut tx, rx) = wit_stream::new(); + let result = consume(rx).await; + let (written, _) = tx.write(vec![0]).await; + assert_eq!(written, StreamResult::Complete(1)); + drop(tx); + assert_eq!(result.await, Err("zero".to_string())); + } +} diff --git a/tests/runtime/stream-param-future-result/test.rs b/tests/runtime/stream-param-future-result/test.rs new file mode 100644 index 000000000..b4c6dd3d5 --- /dev/null +++ b/tests/runtime/stream-param-future-result/test.rs @@ -0,0 +1,23 @@ +use wit_bindgen::{FutureReader, StreamReader}; + +include!(env!("BINDINGS")); + +struct Component; + +export!(Component); + +impl crate::exports::my::test::i::Guest for Component { + async fn consume(data: StreamReader) -> FutureReader> { + let (tx, rx) = wit_future::new(|| unreachable!()); + wit_bindgen::spawn_local(async move { + let bytes = data.collect().await; + let result = if bytes.contains(&0) { + Err("zero".to_string()) + } else { + Ok(bytes.iter().map(|b| u32::from(*b)).sum()) + }; + tx.write(result).await.unwrap(); + }); + rx + } +} diff --git a/tests/runtime/stream-param-future-result/test.wit b/tests/runtime/stream-param-future-result/test.wit new file mode 100644 index 000000000..2d55c9fb2 --- /dev/null +++ b/tests/runtime/stream-param-future-result/test.wit @@ -0,0 +1,22 @@ +//@ async = true +package my:test; + +interface i { + // Like `wasi:cli/stdout.write-via-stream`: a stream parameter and a future result. It is async + // so the export can keep reading after it returns the future. + // Futures and streams share one index space in the canonical ABI, so the stream is 0 and the + // future is 1 in this function's intrinsic import names. + // + // Sums the bytes read from `data` once it closes, or fails if one of them is zero. + consume: async func(data: stream) -> future>; +} + +world test { + export i; +} + +world runner { + import i; + + export run: async func(); +}