Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
32 changes: 21 additions & 11 deletions crates/csharp/src/AsyncSupport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,10 @@ internal LiftingTaskCompletionSource(TaskCompletionSource<int> innerTaskCompleti
{
SetCanceled();
}
throw new NotImplementedException("LiftingTaskCompletionSource unexpected task status " + t.Status);
else
{
throw new NotImplementedException("LiftingTaskCompletionSource unexpected task status " + t.Status);
}
}, TaskContinuationOptions.ExecuteSynchronously);
}
}
Expand All @@ -306,7 +309,9 @@ internal LiftingTaskCompletionSource(TaskCompletionSource<int> 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 <paramref name="count"/> values from canonical ABI memory at <paramref name="buffer"/>
/// into <paramref name="resultBuffer"/>.
public delegate Array Lift(IntPtr buffer, Array resultBuffer, int count);
public delegate void Lower(object payload, List<Action> cleanups);
public delegate uint CancelRead(int handle);
public delegate uint CancelWrite(int handle);
Expand Down Expand Up @@ -475,8 +480,12 @@ internal int TakeHandle()

protected unsafe IntPtr GetBuffer<T>(int length, T[]? userBuffer, IVTable vTable, List<Action> 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.
Expand All @@ -495,23 +504,23 @@ protected unsafe IntPtr GetBuffer<T>(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<T>(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;
Expand Down Expand Up @@ -625,7 +634,7 @@ public ComponentTask<T> 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)
{
Expand Down Expand Up @@ -712,7 +721,8 @@ public ComponentTask<int> 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)
{
Expand Down
16 changes: 15 additions & 1 deletion crates/csharp/src/csproj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ impl CSProjectLLVMBuilder {
<ItemGroup>
<CustomLinkerArg Include=\"-Wl,--component-type,{camel}_component_type.wit\" />
</ItemGroup>

<!-- wasi-sdk 34's wasm-ld puts the stack first by default, which requires the data to start
above the stack, but the ILCompiler sets a global base of 1024. Raising the global base
to the stack size links, but breaks the runtime's exception unwinding, so put the stack
after the data instead. -->
<ItemGroup>
<CustomLinkerArg Include=\"-Wl,--no-stack-first\" />
</ItemGroup>
"
);

Expand All @@ -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") {
Expand All @@ -117,7 +131,7 @@ impl CSProjectLLVMBuilder {
r#"
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*" />
<PackageReference Include="runtime.{os}-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*" />
<PackageReference Include="runtime.{os}-{arch}.Microsoft.DotNet.ILCompiler.LLVM" Version="10.0.0-*" />
</ItemGroup>"#),
);

Expand Down
119 changes: 51 additions & 68 deletions crates/csharp/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub(crate) struct FunctionBindgen<'a, 'b> {
parameter_type: ParameterType,
pub(crate) result_type: Option<Type>,
pub(crate) resource_type_name: Option<String>,
/// The function's futures and streams in signature order; see `future_or_stream_index`.
pub(crate) futures_and_streams: Vec<TypeId>,
}

impl<'a, 'b> FunctionBindgen<'a, 'b> {
Expand Down Expand Up @@ -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<Type>)],
Expand Down Expand Up @@ -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};");
}
Expand Down Expand Up @@ -358,17 +371,15 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> {
}
let head = oks.concat();
let tail = oks.iter().map(|_| ")").collect::<Vec<_>>().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");
Expand All @@ -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<'_, '_> {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}}
Expand All @@ -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<global::System.Action>();

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) => {
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading
Loading