diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3ded1423..1eb12d4b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -55,7 +55,7 @@ jobs:
- name: Run examples
shell: bash
env:
- EXAMPLES: externref funcref global hello memory table consumefuel
+ EXAMPLES: externref funcref global hello memory table consumefuel component
run: |
for e in $EXAMPLES; do cd examples/$e && dotnet run -c ${{ matrix.config }} && cd ../..; done
- name: Create package
diff --git a/.gitignore b/.gitignore
index 09fc0eab..c3f91418 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ bin/
obj/
BenchmarkDotNet.Artifacts/
+/Wasmtime.sln.DotSettings.user
diff --git a/Examples.sln b/Examples.sln
index b0d31c21..9867cb99 100644
--- a/Examples.sln
+++ b/Examples.sln
@@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "consumefuel", "examples\con
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "storedata", "examples\storedata\storedata.csproj", "{E8749BAF-9D8B-4CF9-ACFF-490E86D52A20}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "component", "examples\component\component.csproj", "{35CE1A43-6F41-4747-82DB-6BFEE10297EC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -63,6 +65,10 @@ Global
{E8749BAF-9D8B-4CF9-ACFF-490E86D52A20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8749BAF-9D8B-4CF9-ACFF-490E86D52A20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8749BAF-9D8B-4CF9-ACFF-490E86D52A20}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35CE1A43-6F41-4747-82DB-6BFEE10297EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35CE1A43-6F41-4747-82DB-6BFEE10297EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35CE1A43-6F41-4747-82DB-6BFEE10297EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35CE1A43-6F41-4747-82DB-6BFEE10297EC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/examples/component/Program.cs b/examples/component/Program.cs
new file mode 100644
index 00000000..db7beb2e
--- /dev/null
+++ b/examples/component/Program.cs
@@ -0,0 +1,27 @@
+using System;
+using System.IO;
+using Wasmtime;
+using Wasmtime.Components;
+
+using var engine = new Engine(new Config().WithComponentModel(true));
+using var component = Component.FromTextFile(engine, Path.Combine(AppContext.BaseDirectory, "greeter.wat"));
+using var linker = new ComponentLinker(engine);
+using var store = new Store(engine);
+
+using (var root = linker.Root())
+using (var host = root.AddInstance("host"))
+{
+ host.DefineFunction("name", (arguments, results) =>
+ results[0] = ComponentValue.String("WebAssembly"));
+}
+
+var instance = linker.Instantiate(store, component);
+
+var greet = instance.GetFunction("greet");
+if (greet is null)
+{
+ Console.WriteLine("error: greet export is missing");
+ return;
+}
+
+Console.WriteLine(greet.Call(ComponentValue.String("Wasmtime"))!.AsString());
diff --git a/examples/component/component.csproj b/examples/component/component.csproj
new file mode 100644
index 00000000..8bbbbb19
--- /dev/null
+++ b/examples/component/component.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net9.0
+ enable
+
+
+
+
+
+
+
+
diff --git a/examples/component/greeter.wat b/examples/component/greeter.wat
new file mode 100644
index 00000000..2f613b44
--- /dev/null
+++ b/examples/component/greeter.wat
@@ -0,0 +1,86 @@
+;; A component that combines a caller-provided string with a host-provided name.
+;;
+;; import name: func() -> string
+;; export greet: func(string) -> string
+;;
+;; The core module needs an exported memory and realloc so that strings can cross
+;; the component boundary.
+(component
+ (import "host" (instance $h
+ (export "name" (func (result string)))))
+ (alias export $h "name" (func $name))
+
+ (core module $libc
+ (memory (export "memory") 1)
+ (global $bump (mut i32) (i32.const 1024))
+ (func (export "realloc") (param $old i32) (param $oldsz i32) (param $align i32) (param $n i32) (result i32)
+ (local $r i32)
+ (local.set $r (global.get $bump))
+ (global.set $bump
+ (i32.and (i32.add (i32.add (global.get $bump) (local.get $n)) (i32.const 7)) (i32.const -8)))
+ (local.get $r))
+ )
+ (core instance $libc_i (instantiate $libc))
+
+ (core func $name_core
+ (canon lower (func $name)
+ (memory $libc_i "memory")
+ (realloc (func $libc_i "realloc"))
+ string-encoding=utf8))
+
+ (core module $m
+ (import "libc" "memory" (memory 1))
+ (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32)))
+ (import "host" "name" (func $name (param i32)))
+
+ (func (export "greet") (param $ptr i32) (param $len i32) (result i32)
+ (local $host_result i32)
+ (local $host_ptr i32)
+ (local $host_len i32)
+ (local $out i32)
+ (local $suffix i32)
+ (local $ret i32)
+
+ ;; Ask the host for its name, then build "hello, " ++ name ++ " from " ++ host name.
+ (local.set $host_result (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8)))
+ (call $name (local.get $host_result))
+ (local.set $host_ptr (i32.load (local.get $host_result)))
+ (local.set $host_len (i32.load offset=4 (local.get $host_result)))
+
+ (local.set $out
+ (call $realloc (i32.const 0) (i32.const 0) (i32.const 1)
+ (i32.add (i32.const 13) (i32.add (local.get $len) (local.get $host_len)))))
+ (i32.store8 (local.get $out) (i32.const 104))
+ (i32.store8 offset=1 (local.get $out) (i32.const 101))
+ (i32.store8 offset=2 (local.get $out) (i32.const 108))
+ (i32.store8 offset=3 (local.get $out) (i32.const 108))
+ (i32.store8 offset=4 (local.get $out) (i32.const 111))
+ (i32.store8 offset=5 (local.get $out) (i32.const 44))
+ (i32.store8 offset=6 (local.get $out) (i32.const 32))
+ (memory.copy (i32.add (local.get $out) (i32.const 7)) (local.get $ptr) (local.get $len))
+
+ (local.set $suffix (i32.add (i32.add (local.get $out) (i32.const 7)) (local.get $len)))
+ (i32.store8 (local.get $suffix) (i32.const 32))
+ (i32.store8 offset=1 (local.get $suffix) (i32.const 102))
+ (i32.store8 offset=2 (local.get $suffix) (i32.const 114))
+ (i32.store8 offset=3 (local.get $suffix) (i32.const 111))
+ (i32.store8 offset=4 (local.get $suffix) (i32.const 109))
+ (i32.store8 offset=5 (local.get $suffix) (i32.const 32))
+ (memory.copy (i32.add (local.get $suffix) (i32.const 6)) (local.get $host_ptr) (local.get $host_len))
+
+ (local.set $ret (call $realloc (i32.const 0) (i32.const 0) (i32.const 4) (i32.const 8)))
+ (i32.store (local.get $ret) (local.get $out))
+ (i32.store offset=4 (local.get $ret)
+ (i32.add (i32.const 13) (i32.add (local.get $len) (local.get $host_len))))
+ (local.get $ret))
+ )
+ (core instance $i (instantiate $m
+ (with "libc" (instance $libc_i))
+ (with "host" (instance (export "name" (func $name_core))))))
+
+ (func (export "greet") (param "name" string) (result string)
+ (canon lift (core func $i "greet")
+ (memory $libc_i "memory")
+ (realloc (func $libc_i "realloc"))
+ string-encoding=utf8))
+)
diff --git a/src/Components/Component.cs b/src/Components/Component.cs
new file mode 100644
index 00000000..1a5b745a
--- /dev/null
+++ b/src/Components/Component.cs
@@ -0,0 +1,285 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using Microsoft.Win32.SafeHandles;
+
+namespace Wasmtime.Components;
+
+///
+/// Representation of a component in the component model.
+///
+public class Component
+ : IDisposable
+{
+ private readonly Handle handle;
+
+ internal Handle NativeHandle
+ {
+ get
+ {
+ if (handle.IsInvalid || handle.IsClosed)
+ {
+ throw new ObjectDisposedException(typeof(Module).FullName);
+ }
+
+ return handle;
+ }
+ }
+
+ internal Component(IntPtr handle)
+ {
+ this.handle = new Handle(handle);
+ }
+
+ ///
+ public void Dispose()
+ {
+ handle.Dispose();
+ }
+
+ ///
+ /// Creates a given bytes.
+ ///
+ /// The engine to use for the Component.
+ /// The bytes of the Component.
+ /// Returns a new .
+ public static Component FromBytes(Engine engine, ReadOnlySpan bytes)
+ {
+ if (engine is null)
+ {
+ throw new ArgumentNullException(nameof(engine));
+ }
+
+ unsafe
+ {
+ fixed (byte* ptr = bytes)
+ {
+ var error = Native.wasmtime_component_new(engine.NativeHandle, ptr, (UIntPtr)bytes.Length, out var handle);
+ if (error != IntPtr.Zero)
+ {
+ throw new WasmtimeException($"WebAssembly component is not valid: {WasmtimeException.FromOwnedError(error).Message}");
+ }
+
+ return new Component(handle);
+ }
+ }
+ }
+
+ ///
+ /// Creates a based on a WebAssembly text format representation.
+ ///
+ /// The engine to use for the component.
+ /// The WebAssembly text format representation of the component.
+ /// Returns a new .
+ public static Component FromText(Engine engine, string text)
+ {
+ if (engine is null)
+ {
+ throw new ArgumentNullException(nameof(engine));
+ }
+
+ if (text is null)
+ {
+ throw new ArgumentNullException(nameof(text));
+ }
+
+ unsafe
+ {
+ var textBytes = Encoding.UTF8.GetBytes(text);
+ fixed (byte* ptr = textBytes)
+ {
+ var error = Module.Native.wasmtime_wat2wasm(ptr, (nuint)textBytes.Length, out var componentBytes);
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+
+ using (componentBytes)
+ {
+ return FromBytes(engine, new ReadOnlySpan(componentBytes.data, checked((int)componentBytes.size)));
+ }
+ }
+ }
+ }
+
+ ///
+ /// Creates a from a file in the WebAssembly text format.
+ ///
+ /// The engine to use for the component.
+ /// The path to the file.
+ /// Returns a new .
+ /// Thrown if an argument is null.
+ public static Component FromTextFile(Engine engine, string path)
+ {
+ if (path is null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ return FromText(engine, File.ReadAllText(path));
+ }
+
+ ///
+ /// This function serializes compiled component artifacts as blob data.
+ ///
+ /// If the conversion is successful, the serialized compiled component.
+ public byte[] Serialize()
+ {
+ var error = Native.wasmtime_component_serialize(NativeHandle, out var bytes);
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+
+ using (bytes)
+ return bytes.ToArray();
+ }
+
+ ///
+ /// Deserializes a previously serialized component from a span of bytes.
+ ///
+ /// The engine to use to deserialize the component.
+ /// The previously serialized component bytes.
+ /// Returns the that was previously serialized.
+ /// The passed bytes must come from a previous call to .
+ public static Component Deserialize(Engine engine, ReadOnlySpan bytes)
+ {
+ if (engine is null)
+ {
+ throw new ArgumentNullException(nameof(engine));
+ }
+
+ unsafe
+ {
+ fixed (byte* ptr = bytes)
+ {
+ var error = Native.wasmtime_component_deserialize(engine.NativeHandle, ptr, (UIntPtr)bytes.Length, out var handle);
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+
+ return new Component(handle);
+ }
+ }
+ }
+
+ ///
+ /// Deserializes a previously serialized component from a file.
+ ///
+ /// The engine to deserialize the component with.
+ /// The path to the previously serialized component.
+ /// Returns the that was previously serialized.
+ /// The file's contents must come from a previous call to .
+ public static Component DeserializeFile(Engine engine, string path)
+ {
+ if (engine is null)
+ {
+ throw new ArgumentNullException(nameof(engine));
+ }
+
+ var error = Native.wasmtime_component_deserialize_file(engine.NativeHandle, path, out var handle);
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+
+ return new Component(handle);
+ }
+
+ ///
+ /// Looks up an export of this component by name.
+ ///
+ /// The name of the export.
+ /// The export index, or null if there is no such export.
+ /// Thrown if is null.
+ public ComponentExport? GetExport(string name)
+ {
+ return GetExport(name, null);
+ }
+
+ ///
+ /// Looks up an export of this component by name, within an exported instance.
+ ///
+ /// The name of the export.
+ /// The instance export to look within, or null for the root.
+ /// The export index, or null if there is no such export.
+ /// Thrown if is null.
+ public ComponentExport? GetExport(string name, ComponentExport? instance_export_index)
+ {
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ // The parent is optional, and a null SafeHandle cannot be marshalled, so pass it as a raw pointer.
+ var parentHandle = instance_export_index?.NativeHandle;
+ var parentHandleAddedRef = false;
+ var nameBytes = Encoding.UTF8.GetBytes(name);
+
+ try
+ {
+ parentHandle?.DangerousAddRef(ref parentHandleAddedRef);
+ var parent = parentHandle?.DangerousGetHandle() ?? IntPtr.Zero;
+
+ unsafe
+ {
+ fixed (byte* namePtr = nameBytes)
+ {
+ var ret = Native.wasmtime_component_get_export_index(
+ NativeHandle, parent, namePtr, (nuint)nameBytes.Length);
+
+ return ret == IntPtr.Zero ? null : new ComponentExport(ret);
+ }
+ }
+ }
+ finally
+ {
+ if (parentHandleAddedRef)
+ {
+ parentHandle!.DangerousRelease();
+ }
+ }
+ }
+
+ internal class Handle
+ : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public Handle(IntPtr handle)
+ : base(true)
+ {
+ SetHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ Native.wasmtime_component_delete(handle);
+ return true;
+ }
+ }
+
+ internal static class Native
+ {
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_component_new(Engine.Handle engine, byte* bytes, nuint size, out IntPtr handle);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_delete(IntPtr handle);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_serialize(Handle component, out ByteArray ret);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_component_deserialize(Engine.Handle engine, byte* bytes, nuint size, out IntPtr handle);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_deserialize_file(Engine.Handle engine, string path, out IntPtr handle);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_component_get_export_index(Handle component, IntPtr instance_export_index, byte* name, nuint name_len);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_wat2wasm(byte* text, nuint len, out ByteArray bytes);
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentExport.cs b/src/Components/ComponentExport.cs
new file mode 100644
index 00000000..ea276d6c
--- /dev/null
+++ b/src/Components/ComponentExport.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace Wasmtime.Components;
+
+///
+/// The index of an export within a or ,
+/// used to look the export up without repeating a name search.
+///
+public class ComponentExport
+ : IDisposable
+{
+ private readonly Handle handle;
+
+ internal Handle NativeHandle
+ {
+ get
+ {
+ if (handle.IsInvalid || handle.IsClosed)
+ {
+ throw new ObjectDisposedException(typeof(Module).FullName);
+ }
+
+ return handle;
+ }
+ }
+
+ internal ComponentExport(IntPtr handle)
+ {
+ this.handle = new Handle(handle);
+ }
+
+ ///
+ public void Dispose()
+ {
+ handle.Dispose();
+ }
+
+ internal class Handle
+ : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public Handle(IntPtr handle)
+ : base(true)
+ {
+ SetHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ Native.wasmtime_component_export_index_delete(handle);
+ return true;
+ }
+ }
+
+ internal static class Native
+ {
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_export_index_delete(IntPtr /* wasmtime_component_export_index_t* */ export_index);
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentFunction.cs b/src/Components/ComponentFunction.cs
new file mode 100644
index 00000000..e60709c8
--- /dev/null
+++ b/src/Components/ComponentFunction.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace Wasmtime.Components;
+
+///
+/// An exported function of a .
+///
+public class ComponentFunction
+{
+ /// Size of wasmtime_component_valtype_t: a 1-byte kind padded to 8, plus an 8-byte union.
+ private const int ValueTypeSize = 16;
+
+ private readonly Store store;
+ private readonly Native.Func func;
+
+ internal ComponentFunction(Store store, Native.Func func)
+ {
+ this.store = store;
+ this.func = func;
+
+ var type = Native.wasmtime_component_func_type(in func, store.Context.handle);
+ GC.KeepAlive(store);
+
+ try
+ {
+ ParameterCount = (int)Native.wasmtime_component_func_type_param_count(type);
+ HasResult = ReadHasResult(type);
+ }
+ finally
+ {
+ Native.wasmtime_component_func_type_delete(type);
+ }
+ }
+
+ ///
+ /// Gets the number of parameters this function takes.
+ ///
+ public int ParameterCount { get; }
+
+ ///
+ /// Gets whether this function returns a value.
+ ///
+ ///
+ /// A component function has at most one result. Note that a WIT function declared to return
+ /// result<_, E> still has one result here, even though bindings generators
+ /// usually surface it as returning nothing.
+ ///
+ public bool HasResult { get; }
+
+ ///
+ /// Invokes the function.
+ ///
+ /// The arguments, which must match .
+ /// The result, or null if the function does not return one.
+ /// Thrown if is null.
+ /// Thrown if the wrong number of arguments is given.
+ /// Thrown if the function traps or fails.
+ public ComponentValue? Call(params ComponentValue[] arguments)
+ {
+ return Call((IReadOnlyList)arguments);
+ }
+
+ ///
+ /// Invokes the function.
+ ///
+ /// The arguments, which must match .
+ /// The result, or null if the function does not return one.
+ /// Thrown if is null.
+ /// Thrown if the wrong number of arguments is given.
+ /// Thrown if the function traps or fails.
+ ///
+ /// A trap leaves the whole unusable for further component calls, which
+ /// then fail with "cannot enter component instance". This applies to every instance in the
+ /// store, not just this one, so a store cannot be reused after a trap.
+ ///
+ public ComponentValue? Call(IReadOnlyList arguments)
+ {
+ if (arguments is null)
+ {
+ throw new ArgumentNullException(nameof(arguments));
+ }
+
+ var argumentCount = arguments.Count;
+ if (argumentCount != ParameterCount)
+ {
+ throw new ArgumentException(
+ $"The function takes {ParameterCount} argument(s) but {argumentCount} were given.",
+ nameof(arguments));
+ }
+
+ var resultCount = HasResult ? 1 : 0;
+
+ using (var scope = new ComponentValueMarshaller.AllocationScope())
+ {
+ var argumentBuffer = IntPtr.Zero;
+ if (argumentCount > 0)
+ {
+ argumentBuffer = scope.Allocate(argumentCount * ComponentValueMarshaller.ValueSize);
+ for (var i = 0; i < argumentCount; i++)
+ {
+ if (arguments[i] is null)
+ {
+ throw new ArgumentException($"Argument {i} is null.", nameof(arguments));
+ }
+
+ ComponentValueMarshaller.Write(
+ arguments[i],
+ argumentBuffer + (i * ComponentValueMarshaller.ValueSize),
+ scope);
+ }
+ }
+
+ // Wasmtime allocates the contents of the results, so the buffer must start zeroed and
+ // must only be deleted if the call actually wrote to it.
+ var resultBuffer = resultCount == 0
+ ? IntPtr.Zero
+ : scope.Allocate(resultCount * ComponentValueMarshaller.ValueSize);
+
+ var error = Native.wasmtime_component_func_call(
+ in func,
+ store.Context.handle,
+ argumentBuffer,
+ (nuint)argumentCount,
+ resultBuffer,
+ (nuint)resultCount);
+
+ GC.KeepAlive(store);
+
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+
+ if (resultCount == 0)
+ {
+ return null;
+ }
+
+ try
+ {
+ return ComponentValueMarshaller.Read(resultBuffer);
+ }
+ finally
+ {
+ ComponentValueNative.wasmtime_component_val_delete(resultBuffer);
+ }
+ }
+ }
+
+ private static bool ReadHasResult(IntPtr type)
+ {
+ var buffer = Marshal.AllocHGlobal(ValueTypeSize);
+ try
+ {
+ unsafe
+ {
+ new Span((void*)buffer, ValueTypeSize).Clear();
+ }
+
+ if (!Native.wasmtime_component_func_type_result(type, buffer))
+ {
+ return false;
+ }
+
+ Native.wasmtime_component_valtype_delete(buffer);
+ return true;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ internal static class Native
+ {
+ ///
+ /// Mirrors wasmtime_component_func_t. The first two fields sit in an anonymous
+ /// struct, so the trailing field lands at offset 16 and the whole thing is 24 bytes.
+ ///
+ [StructLayout(LayoutKind.Explicit, Size = 24)]
+ internal struct Func
+ {
+ [FieldOffset(0)]
+ public ulong StoreId;
+
+ [FieldOffset(8)]
+ public uint Private1;
+
+ [FieldOffset(16)]
+ public uint Private2;
+ }
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_func_call(
+ in Func func,
+ IntPtr context,
+ IntPtr args,
+ nuint args_size,
+ IntPtr results,
+ nuint results_size);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_func_type(in Func func, IntPtr context);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_func_type_delete(IntPtr ty);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern nuint wasmtime_component_func_type_param_count(IntPtr ty);
+
+ [DllImport(Engine.LibraryName)]
+ [return: MarshalAs(UnmanagedType.I1)]
+ public static extern bool wasmtime_component_func_type_result(IntPtr ty, IntPtr type_ret);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_valtype_delete(IntPtr ptr);
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentInstance.cs b/src/Components/ComponentInstance.cs
new file mode 100644
index 00000000..37a28d7d
--- /dev/null
+++ b/src/Components/ComponentInstance.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Wasmtime.Components;
+
+///
+/// An instantiated .
+///
+///
+/// Instances are identified by an integer within their store and have no destructor, so this
+/// type is not disposable. Passing an instance to a different than the one
+/// it was created in may abort the process.
+///
+public class ComponentInstance
+{
+ internal readonly Native.Instance instance;
+ internal readonly Store store;
+
+ internal ComponentInstance(Store store, Native.Instance instance)
+ {
+ this.store = store;
+ this.instance = instance;
+ }
+
+ ///
+ /// Looks up an export of this instance by name.
+ ///
+ /// The name of the export.
+ /// The instance export to look within, or null for the root.
+ /// The export index, or null if there is no such export.
+ /// Thrown if is null.
+ public ComponentExport? GetExport(string name, ComponentExport? parent = null)
+ {
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var nameBytes = Encoding.UTF8.GetBytes(name);
+
+ // The parent is optional, and a null SafeHandle cannot be marshalled, so pass it as a raw pointer.
+ var parentSafeHandle = parent?.NativeHandle;
+ var parentHandleAddedRef = false;
+
+ try
+ {
+ parentSafeHandle?.DangerousAddRef(ref parentHandleAddedRef);
+ var parentHandle = parentSafeHandle?.DangerousGetHandle() ?? IntPtr.Zero;
+
+ unsafe
+ {
+ fixed (byte* namePtr = nameBytes)
+ {
+ var index = Native.wasmtime_component_instance_get_export_index(
+ in instance,
+ store.Context.handle,
+ parentHandle,
+ namePtr,
+ (nuint)nameBytes.Length);
+
+ GC.KeepAlive(store);
+
+ return index == IntPtr.Zero ? null : new ComponentExport(index);
+ }
+ }
+ }
+ finally
+ {
+ if (parentHandleAddedRef)
+ {
+ parentSafeHandle!.DangerousRelease();
+ }
+ }
+ }
+
+ ///
+ /// Gets an exported function by its export index.
+ ///
+ /// The export index of the function.
+ /// The function, or null if the export is not a function.
+ /// Thrown if is null.
+ public ComponentFunction? GetFunction(ComponentExport export)
+ {
+ if (export is null)
+ {
+ throw new ArgumentNullException(nameof(export));
+ }
+
+ var found = Native.wasmtime_component_instance_get_func(
+ in instance, store.Context.handle, export.NativeHandle, out var func);
+
+ GC.KeepAlive(store);
+
+ return found ? new ComponentFunction(store, func) : null;
+ }
+
+ ///
+ /// Gets an exported function from the root of this instance.
+ ///
+ /// The name of the function.
+ /// The function, or null if there is no such exported function.
+ /// Thrown if is null.
+ public ComponentFunction? GetFunction(string name)
+ {
+ using var export = GetExport(name);
+ return export is null ? null : GetFunction(export);
+ }
+
+ ///
+ /// Gets a function exported by one of this instance's exported interfaces.
+ ///
+ /// The name of the interface, such as my:pkg/iface@1.0.0.
+ /// The name of the function within the interface.
+ /// The function, or null if there is no such exported function.
+ /// Thrown if an argument is null.
+ public ComponentFunction? GetFunction(string interfaceName, string name)
+ {
+ if (interfaceName is null)
+ {
+ throw new ArgumentNullException(nameof(interfaceName));
+ }
+
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ using var instanceExport = GetExport(interfaceName);
+ if (instanceExport is null)
+ {
+ return null;
+ }
+
+ using var export = GetExport(name, instanceExport);
+ return export is null ? null : GetFunction(export);
+ }
+
+ internal static class Native
+ {
+ /// Mirrors wasmtime_component_instance_t.
+ [StructLayout(LayoutKind.Explicit, Size = 16)]
+ internal struct Instance
+ {
+ [FieldOffset(0)]
+ public ulong StoreId;
+
+ [FieldOffset(8)]
+ public uint Private;
+ }
+
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_component_instance_get_export_index(
+ in Instance instance,
+ IntPtr context,
+ IntPtr instance_export_index,
+ byte* name,
+ nuint name_len);
+
+ [DllImport(Engine.LibraryName)]
+ [return: MarshalAs(UnmanagedType.I1)]
+ public static extern bool wasmtime_component_instance_get_func(
+ in Instance instance,
+ IntPtr context,
+ ComponentExport.Handle export_index,
+ out ComponentFunction.Native.Func func_out);
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentLinker.cs b/src/Components/ComponentLinker.cs
new file mode 100644
index 00000000..604fde6f
--- /dev/null
+++ b/src/Components/ComponentLinker.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace Wasmtime.Components;
+
+///
+/// Used to instantiate a , providing the host functions and other
+/// definitions that satisfy its imports.
+///
+public class ComponentLinker
+ : IDisposable
+{
+ private readonly Handle handle;
+ private ComponentLinkerInstance? root;
+
+ ///
+ /// Creates a new for the given engine.
+ ///
+ /// The engine to create the linker for.
+ /// Thrown if is null.
+ public ComponentLinker(Engine engine)
+ {
+ if (engine is null)
+ {
+ throw new ArgumentNullException(nameof(engine));
+ }
+
+ handle = new Handle(Native.wasmtime_component_linker_new(engine.NativeHandle));
+ }
+
+ internal ComponentLinker(IntPtr handle)
+ {
+ this.handle = new Handle(handle);
+ }
+
+ internal Handle NativeHandle
+ {
+ get
+ {
+ if (handle.IsInvalid || handle.IsClosed)
+ {
+ throw new ObjectDisposedException(typeof(ComponentLinker).FullName);
+ }
+
+ if (root is not null)
+ {
+ throw new InvalidOperationException(
+ "The linker cannot be used while the linker instance returned by Root() is still in use.");
+ }
+
+ return handle;
+ }
+ }
+
+ ///
+ /// Sets whether later definitions are allowed to shadow previous ones.
+ ///
+ /// True to allow shadowing.
+ public bool AllowShadowing
+ {
+ set
+ {
+ Native.wasmtime_component_linker_allow_shadowing(NativeHandle, value);
+ }
+ }
+
+ ///
+ /// Returns the root instance of this linker, used to define names into the root namespace.
+ ///
+ /// The root instance. The linker cannot be used again until this is disposed.
+ public ComponentLinkerInstance Root()
+ {
+ var current = NativeHandle;
+ root = new ComponentLinkerInstance(
+ Native.wasmtime_component_linker_root(current),
+ () => root = null);
+
+ return root;
+ }
+
+ ///
+ /// Adds all WASI Preview 2 interfaces to this linker.
+ ///
+ ///
+ /// The store used for instantiation must have a WASI configuration set with
+ /// .
+ ///
+ public void AddWasiPreview2()
+ {
+ var error = Native.wasmtime_component_linker_add_wasip2(NativeHandle);
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+ }
+
+ ///
+ /// Defines every import of the given component that is not already defined as a function
+ /// which traps when called.
+ ///
+ /// The component whose imports should be satisfied.
+ /// Thrown if is null.
+ public void DefineUnknownImportsAsTraps(Component component)
+ {
+ if (component is null)
+ {
+ throw new ArgumentNullException(nameof(component));
+ }
+
+ var error = Native.wasmtime_component_linker_define_unknown_imports_as_traps(
+ NativeHandle, component.NativeHandle);
+
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+ }
+
+ ///
+ /// Instantiates a component in the given store.
+ ///
+ /// The store to instantiate the component in.
+ /// The component to instantiate.
+ /// The instantiated component.
+ /// Thrown if an argument is null.
+ /// Thrown if an import could not be satisfied.
+ public ComponentInstance Instantiate(Store store, Component component)
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ if (component is null)
+ {
+ throw new ArgumentNullException(nameof(component));
+ }
+
+ var error = Native.wasmtime_component_linker_instantiate(
+ NativeHandle, store.Context.handle, component.NativeHandle, out var instance);
+
+ GC.KeepAlive(store);
+
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+
+ return new ComponentInstance(store, instance);
+ }
+
+ ///
+ public void Dispose()
+ {
+ root?.Dispose();
+ handle.Dispose();
+ }
+
+ internal class Handle
+ : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public Handle(IntPtr handle)
+ : base(true)
+ {
+ SetHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ Native.wasmtime_component_linker_delete(handle);
+ return true;
+ }
+ }
+
+ internal static class Native
+ {
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_linker_new(Engine.Handle engine);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_linker_allow_shadowing(
+ Handle linker, [MarshalAs(UnmanagedType.I1)] bool allow);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_linker_root(Handle linker);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_linker_instantiate(
+ Handle linker,
+ IntPtr context,
+ Component.Handle component,
+ out ComponentInstance.Native.Instance instance_out);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_linker_define_unknown_imports_as_traps(
+ Handle linker, Component.Handle component);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_linker_add_wasip2(Handle linker);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_linker_delete(IntPtr /* wasmtime_component_linker_t* */ linker);
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentLinkerInstance.cs b/src/Components/ComponentLinkerInstance.cs
new file mode 100644
index 00000000..3b4d6092
--- /dev/null
+++ b/src/Components/ComponentLinkerInstance.cs
@@ -0,0 +1,325 @@
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Text;
+using Microsoft.Win32.SafeHandles;
+
+namespace Wasmtime.Components;
+
+///
+/// A callback implementing a component function defined by the host.
+///
+/// The arguments passed by the guest.
+///
+/// The results to return to the guest. Every element must be assigned.
+///
+public delegate void ComponentFunctionCallback(
+ ReadOnlySpan arguments,
+ Span results);
+
+///
+/// An instance being defined within a , used to define names into
+/// a namespace.
+///
+///
+/// Obtaining one of these acquires exclusive access to whatever it came from: neither the owning
+/// linker nor a parent instance may be used until this is disposed. This type enforces that by
+/// throwing rather than allowing the undefined behaviour the C API warns about.
+///
+public sealed class ComponentLinkerInstance : IDisposable
+{
+ private readonly Handle handle;
+ private readonly Action onDisposed;
+ private ComponentLinkerInstance? child;
+ private bool disposed;
+
+ internal ComponentLinkerInstance(IntPtr handle, Action onDisposed)
+ {
+ this.handle = new Handle(handle);
+ this.onDisposed = onDisposed;
+ }
+
+ internal Handle NativeHandle
+ {
+ get
+ {
+ if (handle.IsInvalid || handle.IsClosed)
+ {
+ throw new ObjectDisposedException(typeof(ComponentLinkerInstance).FullName);
+ }
+
+ if (child is not null)
+ {
+ throw new InvalidOperationException(
+ "This linker instance cannot be used while a nested instance created from it is still in use.");
+ }
+
+ return handle;
+ }
+ }
+
+ ///
+ /// Defines a nested instance within this instance.
+ ///
+ /// The name of the nested instance.
+ /// The nested instance, which must be disposed before this one is used again.
+ /// Thrown if is null.
+ public ComponentLinkerInstance AddInstance(string name)
+ {
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var current = NativeHandle;
+ var nameBytes = Encoding.UTF8.GetBytes(name);
+
+ unsafe
+ {
+ fixed (byte* namePtr = nameBytes)
+ {
+ var error = Native.wasmtime_component_linker_instance_add_instance(
+ current, namePtr, (nuint)nameBytes.Length, out var nested);
+
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+
+ child = new ComponentLinkerInstance(nested, () => child = null);
+ return child;
+ }
+ }
+ }
+
+ ///
+ /// Defines a core WebAssembly module within this instance.
+ ///
+ /// The name to define the module as.
+ /// The module.
+ /// Thrown if an argument is null.
+ public void AddModule(string name, Module module)
+ {
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (module is null)
+ {
+ throw new ArgumentNullException(nameof(module));
+ }
+
+ var current = NativeHandle;
+ var nameBytes = Encoding.UTF8.GetBytes(name);
+
+ unsafe
+ {
+ fixed (byte* namePtr = nameBytes)
+ {
+ var error = Native.wasmtime_component_linker_instance_add_module(
+ current, namePtr, (nuint)nameBytes.Length, module.NativeHandle);
+
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Defines a function within this instance.
+ ///
+ /// The name of the function.
+ /// The implementation of the function.
+ /// Thrown if an argument is null.
+ public void DefineFunction(string name, ComponentFunctionCallback callback)
+ {
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (callback is null)
+ {
+ throw new ArgumentNullException(nameof(callback));
+ }
+
+ var current = NativeHandle;
+ var nameBytes = Encoding.UTF8.GetBytes(name);
+
+ Native.ComponentFuncCallback trampoline = (env, context, type, args, nargs, results, nresults) =>
+ Invoke(callback, name, args, (int)nargs, results, (int)nresults);
+
+ unsafe
+ {
+ fixed (byte* namePtr = nameBytes)
+ {
+ var error = Native.wasmtime_component_linker_instance_add_func(
+ current,
+ namePtr,
+ (nuint)nameBytes.Length,
+ trampoline,
+ GCHandle.ToIntPtr(GCHandle.Alloc(trampoline)),
+ Function.Finalizer);
+
+ if (error != IntPtr.Zero)
+ {
+ throw WasmtimeException.FromOwnedError(error);
+ }
+ }
+ }
+ }
+
+ private static IntPtr Invoke(
+ ComponentFunctionCallback callback,
+ string name,
+ IntPtr args,
+ int argumentCount,
+ IntPtr results,
+ int resultCount)
+ {
+ ComponentValue[]? arguments = null;
+ ComponentValue[]? produced = null;
+
+ try
+ {
+ arguments = argumentCount == 0
+ ? Array.Empty()
+ : ArrayPool.Shared.Rent(argumentCount);
+ for (var i = 0; i < argumentCount; i++)
+ {
+ arguments[i] = ComponentValueMarshaller.Read(args + (i * ComponentValueMarshaller.ValueSize));
+ }
+
+ produced = resultCount == 0
+ ? Array.Empty()
+ : ArrayPool.Shared.Rent(resultCount);
+ Array.Clear(produced, 0, resultCount);
+ callback(arguments.AsSpan(0, argumentCount), produced.AsSpan(0, resultCount));
+
+ for (var i = 0; i < resultCount; i++)
+ {
+ if (produced[i] is null)
+ {
+ throw new InvalidOperationException(
+ $"The callback for component function '{name}' did not assign result {i}.");
+ }
+
+ ComponentValueMarshaller.WriteOwned(
+ produced[i],
+ results + (i * ComponentValueMarshaller.ValueSize));
+ }
+
+ return IntPtr.Zero;
+ }
+ catch (Exception ex)
+ {
+ return HandleCallbackException(ex);
+ }
+ finally
+ {
+ if (arguments is { Length: > 0 })
+ {
+ ArrayPool.Shared.Return(arguments, clearArray: true);
+ }
+
+ if (produced is { Length: > 0 })
+ {
+ ArrayPool.Shared.Return(produced, clearArray: true);
+ }
+ }
+ }
+
+ ///
+ /// Converts a managed exception into an owned wasmtime_error_t, since an exception
+ /// must never propagate across the native-to-managed transition.
+ ///
+ private static IntPtr HandleCallbackException(Exception ex)
+ {
+ try
+ {
+ Function.CallbackErrorCause = ex is WasmtimeException wasmtimeException
+ ? wasmtimeException.InnerException
+ : ex;
+
+ return Native.wasmtime_error_new(ex.Message);
+ }
+ catch (Exception separateException)
+ {
+ // See Function.HandleCallbackException: unwinding through native frames is undefined
+ // behaviour, so failing fast is the only safe option left.
+ Environment.FailFast(separateException.Message, separateException);
+ throw;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ disposed = true;
+
+ // A nested instance borrows from this one, so it has to go first.
+ child?.Dispose();
+ handle.Dispose();
+ onDisposed();
+ }
+
+ internal class Handle
+ : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public Handle(IntPtr handle)
+ : base(true)
+ {
+ SetHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ Native.wasmtime_component_linker_instance_delete(handle);
+ return true;
+ }
+ }
+
+ internal static class Native
+ {
+ public delegate IntPtr ComponentFuncCallback(
+ IntPtr env,
+ IntPtr context,
+ IntPtr type,
+ IntPtr args,
+ nuint nargs,
+ IntPtr results,
+ nuint nresults);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_component_linker_instance_add_instance(
+ Handle linker_instance, byte* name, nuint name_len, out IntPtr linker_instance_out);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_component_linker_instance_add_module(
+ Handle linker_instance, byte* name, nuint name_len, Module.Handle module);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern unsafe IntPtr wasmtime_component_linker_instance_add_func(
+ Handle linker_instance,
+ byte* name,
+ nuint name_len,
+ ComponentFuncCallback callback,
+ IntPtr data,
+ Function.Native.Finalizer? finalizer);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_linker_instance_delete(
+ IntPtr /* wasmtime_component_linker_instance_t* */ linker_instance);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_error_new([MarshalAs(Extensions.LPUTF8Str)] string message);
+ }
+}
diff --git a/src/Components/ComponentValue.cs b/src/Components/ComponentValue.cs
new file mode 100644
index 00000000..ed4e1bb9
--- /dev/null
+++ b/src/Components/ComponentValue.cs
@@ -0,0 +1,499 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.InteropServices;
+
+namespace Wasmtime.Components;
+
+///
+/// The type discriminant of a , mirroring
+/// wasmtime_component_valkind_t.
+///
+public enum ComponentValueKind : byte
+{
+ /// A bool.
+ Bool = 0,
+ /// A signed 8-bit integer.
+ S8 = 1,
+ /// An unsigned 8-bit integer.
+ U8 = 2,
+ /// A signed 16-bit integer.
+ S16 = 3,
+ /// An unsigned 16-bit integer.
+ U16 = 4,
+ /// A signed 32-bit integer.
+ S32 = 5,
+ /// An unsigned 32-bit integer.
+ U32 = 6,
+ /// A signed 64-bit integer.
+ S64 = 7,
+ /// An unsigned 64-bit integer.
+ U64 = 8,
+ /// A 32-bit IEEE-754 float.
+ F32 = 9,
+ /// A 64-bit IEEE-754 float.
+ F64 = 10,
+ /// A Unicode scalar value. Not yet supported by this binding.
+ Char = 11,
+ /// A UTF-8 string.
+ String = 12,
+ /// A homogeneous list.
+ List = 13,
+ /// A record of named fields.
+ Record = 14,
+ /// A tuple of positional values.
+ Tuple = 15,
+ /// A variant. Not yet supported by this binding.
+ Variant = 16,
+ /// An enumeration, identified by case name.
+ Enum = 17,
+ /// An optional value.
+ Option = 18,
+ /// A result value.
+ Result = 19,
+ /// A set of flags. Not yet supported by this binding.
+ Flags = 20,
+ /// A resource handle. Not yet supported by this binding.
+ Resource = 21,
+ /// A map. Not yet supported by this binding.
+ Map = 22,
+}
+
+///
+/// A value which a component function can consume or produce, mirroring
+/// wasmtime_component_val_t.
+///
+///
+/// Create values with the static factory methods and read them back with the typed accessors,
+/// each of which throws if the value has a different . The
+/// , ,
+/// , and
+/// kinds are not yet supported.
+///
+public sealed class ComponentValue
+{
+ private ComponentValue(ComponentValueKind kind)
+ {
+ Kind = kind;
+ }
+
+ ///
+ /// Gets the type discriminant of this value.
+ ///
+ public ComponentValueKind Kind { get; }
+
+ /// Backing store for the integral kinds and for .
+ internal long Integer { get; private set; }
+
+ /// Backing store for the floating-point kinds.
+ internal double Real { get; private set; }
+
+ /// String contents, or the case name of an enum.
+ internal string? Text { get; private set; }
+
+ /// Elements of a list or tuple.
+ internal IReadOnlyList Items { get; private set; } = Array.Empty();
+
+ /// Fields of a record, in declaration order.
+ internal IReadOnlyList> Fields { get; private set; } =
+ Array.Empty>();
+
+ /// True for some options and ok results.
+ internal bool Flag { get; private set; }
+
+ ///
+ /// Gets the payload of an or
+ /// , or null when there is none.
+ ///
+ public ComponentValue? Payload { get; private set; }
+
+ ///
+ /// Gets whether this carries a value.
+ ///
+ public bool IsSome => Expect(ComponentValueKind.Option, Flag);
+
+ ///
+ /// Gets whether this is the ok case.
+ ///
+ public bool IsOk => Expect(ComponentValueKind.Result, Flag);
+
+ /// Creates a bool value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue Bool(bool value) =>
+ new ComponentValue(ComponentValueKind.Bool) { Integer = value ? 1 : 0 };
+
+ /// Creates a signed 8-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue S8(sbyte value) =>
+ new ComponentValue(ComponentValueKind.S8) { Integer = value };
+
+ /// Creates an unsigned 8-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue U8(byte value) =>
+ new ComponentValue(ComponentValueKind.U8) { Integer = value };
+
+ /// Creates a signed 16-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue S16(short value) =>
+ new ComponentValue(ComponentValueKind.S16) { Integer = value };
+
+ /// Creates an unsigned 16-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue U16(ushort value) =>
+ new ComponentValue(ComponentValueKind.U16) { Integer = value };
+
+ /// Creates a signed 32-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue S32(int value) =>
+ new ComponentValue(ComponentValueKind.S32) { Integer = value };
+
+ /// Creates an unsigned 32-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue U32(uint value) =>
+ new ComponentValue(ComponentValueKind.U32) { Integer = value };
+
+ /// Creates a signed 64-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue S64(long value) =>
+ new ComponentValue(ComponentValueKind.S64) { Integer = value };
+
+ /// Creates an unsigned 64-bit integer value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue U64(ulong value) =>
+ new ComponentValue(ComponentValueKind.U64) { Integer = unchecked((long)value) };
+
+ /// Creates a 32-bit float value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue F32(float value) =>
+ new ComponentValue(ComponentValueKind.F32) { Real = value };
+
+ /// Creates a 64-bit float value.
+ /// The value.
+ /// The component value.
+ public static ComponentValue F64(double value) =>
+ new ComponentValue(ComponentValueKind.F64) { Real = value };
+
+ /// Creates a string value.
+ /// The value.
+ /// The component value.
+ /// Thrown if is null.
+ public static ComponentValue String(string value)
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ return new ComponentValue(ComponentValueKind.String) { Text = value };
+ }
+
+ /// Creates an enumeration value.
+ /// The name of the enum case, as declared in WIT.
+ /// The component value.
+ /// Thrown if is null.
+ /// Component model enums travel by case name rather than by ordinal.
+ public static ComponentValue Enum(string caseName)
+ {
+ if (caseName is null)
+ {
+ throw new ArgumentNullException(nameof(caseName));
+ }
+
+ return new ComponentValue(ComponentValueKind.Enum) { Text = caseName };
+ }
+
+ /// Creates a list value.
+ /// The elements of the list, which are copied.
+ /// The component value.
+ /// Thrown if is null.
+ public static ComponentValue List(IReadOnlyList items)
+ {
+ if (items is null)
+ {
+ throw new ArgumentNullException(nameof(items));
+ }
+
+ return new ComponentValue(ComponentValueKind.List) { Items = Copy(items) };
+ }
+
+ /// Creates a tuple value.
+ /// The elements of the tuple, which are copied.
+ /// The component value.
+ /// Thrown if is null.
+ public static ComponentValue Tuple(IReadOnlyList items)
+ {
+ if (items is null)
+ {
+ throw new ArgumentNullException(nameof(items));
+ }
+
+ return new ComponentValue(ComponentValueKind.Tuple) { Items = Copy(items) };
+ }
+
+ /// Creates a record value.
+ /// The fields of the record in declaration order, which are copied.
+ /// The component value.
+ /// Thrown if is null.
+ public static ComponentValue Record(IReadOnlyList> fields)
+ {
+ if (fields is null)
+ {
+ throw new ArgumentNullException(nameof(fields));
+ }
+
+ return new ComponentValue(ComponentValueKind.Record) { Fields = Copy(fields) };
+ }
+
+ // The Owned* factories take ownership instead of copying, for lists nothing else references.
+ internal static ComponentValue OwnedList(List items) =>
+ new ComponentValue(ComponentValueKind.List) { Items = items };
+
+ internal static ComponentValue OwnedTuple(List items) =>
+ new ComponentValue(ComponentValueKind.Tuple) { Items = items };
+
+ internal static ComponentValue OwnedRecord(List> fields) =>
+ new ComponentValue(ComponentValueKind.Record) { Fields = fields };
+
+ private static T[] Copy(IReadOnlyList source)
+ {
+ if (source.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ var copy = new T[source.Count];
+ for (var i = 0; i < copy.Length; i++)
+ {
+ copy[i] = source[i];
+ }
+
+ return copy;
+ }
+
+ /// Creates an option value carrying a payload.
+ /// The payload.
+ /// The component value.
+ /// Thrown if is null.
+ public static ComponentValue Some(ComponentValue value)
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ return new ComponentValue(ComponentValueKind.Option) { Payload = value, Flag = true };
+ }
+
+ /// Creates an empty option value.
+ /// The component value.
+ public static ComponentValue None() =>
+ new ComponentValue(ComponentValueKind.Option) { Flag = false };
+
+ /// Creates the ok case of a result.
+ /// The payload, or null for a result with no ok type.
+ /// The component value.
+ public static ComponentValue Ok(ComponentValue? value = null) =>
+ new ComponentValue(ComponentValueKind.Result) { Payload = value, Flag = true };
+
+ /// Creates the err case of a result.
+ /// The payload, or null for a result with no err type.
+ /// The component value.
+ public static ComponentValue Err(ComponentValue? value = null) =>
+ new ComponentValue(ComponentValueKind.Result) { Payload = value, Flag = false };
+
+ /// Gets the value as a bool.
+ /// The value.
+ public bool AsBool() => Expect(ComponentValueKind.Bool, Integer != 0);
+
+ /// Gets the value as a signed 8-bit integer.
+ /// The value.
+ public sbyte AsS8() => Expect(ComponentValueKind.S8, unchecked((sbyte)Integer));
+
+ /// Gets the value as an unsigned 8-bit integer.
+ /// The value.
+ public byte AsU8() => Expect(ComponentValueKind.U8, unchecked((byte)Integer));
+
+ /// Gets the value as a signed 16-bit integer.
+ /// The value.
+ public short AsS16() => Expect(ComponentValueKind.S16, unchecked((short)Integer));
+
+ /// Gets the value as an unsigned 16-bit integer.
+ /// The value.
+ public ushort AsU16() => Expect(ComponentValueKind.U16, unchecked((ushort)Integer));
+
+ /// Gets the value as a signed 32-bit integer.
+ /// The value.
+ public int AsS32() => Expect(ComponentValueKind.S32, unchecked((int)Integer));
+
+ /// Gets the value as an unsigned 32-bit integer.
+ /// The value.
+ public uint AsU32() => Expect(ComponentValueKind.U32, unchecked((uint)Integer));
+
+ /// Gets the value as a signed 64-bit integer.
+ /// The value.
+ public long AsS64() => Expect(ComponentValueKind.S64, Integer);
+
+ /// Gets the value as an unsigned 64-bit integer.
+ /// The value.
+ public ulong AsU64() => Expect(ComponentValueKind.U64, unchecked((ulong)Integer));
+
+ /// Gets the value as a 32-bit float.
+ /// The value.
+ public float AsF32() => Expect(ComponentValueKind.F32, (float)Real);
+
+ /// Gets the value as a 64-bit float.
+ /// The value.
+ public double AsF64() => Expect(ComponentValueKind.F64, Real);
+
+ /// Gets the value as a string.
+ /// The value.
+ public string AsString() => Expect(ComponentValueKind.String, Text ?? string.Empty);
+
+ /// Gets the case name of an enumeration value.
+ /// The case name.
+ public string AsEnum() => Expect(ComponentValueKind.Enum, Text ?? string.Empty);
+
+ /// Gets the elements of a list.
+ /// The elements.
+ public IReadOnlyList AsList() => Expect(ComponentValueKind.List, Items);
+
+ /// Gets the elements of a tuple.
+ /// The elements.
+ public IReadOnlyList AsTuple() => Expect(ComponentValueKind.Tuple, Items);
+
+ /// Gets the fields of a record.
+ /// The fields, in declaration order.
+ public IReadOnlyList> AsRecord() =>
+ Expect(ComponentValueKind.Record, Fields);
+
+ ///
+ /// Gets the value of a record field by name.
+ ///
+ /// The field name.
+ /// The field's value.
+ ///
+ /// Thrown if this is not a record, or if it has no such field.
+ ///
+ public ComponentValue Field(string name)
+ {
+ if (!TryGetField(name, out var value))
+ {
+ throw new InvalidOperationException($"Record has no field '{name}'.");
+ }
+
+ return value!;
+ }
+
+ ///
+ /// Gets the value of a record field by name, if present.
+ ///
+ /// The field name.
+ /// The field's value, or null if there is no such field.
+ /// True if the field was found.
+ /// Thrown if this is not a record.
+ public bool TryGetField(string name, out ComponentValue? value)
+ {
+ foreach (var field in AsRecord())
+ {
+ if (field.Key == name)
+ {
+ value = field.Value;
+ return true;
+ }
+ }
+
+ value = null;
+ return false;
+ }
+
+ private T Expect(ComponentValueKind expected, T value)
+ {
+ if (Kind != expected)
+ {
+ throw new InvalidOperationException($"Expected component value kind {expected}, got {Kind}.");
+ }
+
+ return value;
+ }
+
+ ///
+ public override string ToString()
+ {
+ switch (Kind)
+ {
+ case ComponentValueKind.Bool:
+ return (Integer != 0).ToString(CultureInfo.InvariantCulture);
+ case ComponentValueKind.F32:
+ case ComponentValueKind.F64:
+ return Real.ToString(CultureInfo.InvariantCulture);
+ case ComponentValueKind.U64:
+ return unchecked((ulong)Integer).ToString(CultureInfo.InvariantCulture);
+ case ComponentValueKind.String:
+ return $"\"{Text}\"";
+ case ComponentValueKind.Enum:
+ return Text ?? string.Empty;
+ case ComponentValueKind.List:
+ case ComponentValueKind.Tuple:
+ return $"[{string.Join(", ", Items)}]";
+ case ComponentValueKind.Record:
+ var fields = new string[Fields.Count];
+ for (var i = 0; i < Fields.Count; i++)
+ {
+ fields[i] = $"{Fields[i].Key}: {Fields[i].Value}";
+ }
+
+ return $"{{{string.Join(", ", fields)}}}";
+ case ComponentValueKind.Option:
+ return Flag ? $"some({Payload})" : "none";
+ case ComponentValueKind.Result:
+ return Flag ? $"ok({Payload})" : $"err({Payload})";
+ default:
+ return Integer.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+}
+
+internal static class ComponentValueNative
+{
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasm_byte_vec_new_uninitialized(IntPtr value, nuint size);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_vallist_new_uninit(IntPtr value, nuint size);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_valrecord_new_uninit(IntPtr value, nuint size);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_valtuple_new_uninit(IntPtr value, nuint size);
+
+ [DllImport(Engine.LibraryName)]
+ public static extern IntPtr wasmtime_component_val_new(IntPtr value);
+
+ ///
+ /// Performs a deep copy of into . The contents
+ /// of are owned by Wasmtime and must be released with
+ /// .
+ ///
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_val_clone(
+ IntPtr /* const wasmtime_component_val_t* */ src,
+ IntPtr /* wasmtime_component_val_t* */ dst);
+
+ ///
+ /// Deallocates the memory owned by , but not the storage of
+ /// itself. Only valid for embedder-owned storage.
+ ///
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_val_delete(IntPtr /* wasmtime_component_val_t* */ value);
+}
\ No newline at end of file
diff --git a/src/Components/ComponentValueMarshaller.cs b/src/Components/ComponentValueMarshaller.cs
new file mode 100644
index 00000000..c132928b
--- /dev/null
+++ b/src/Components/ComponentValueMarshaller.cs
@@ -0,0 +1,580 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Wasmtime.Components;
+
+///
+/// Converts between and the native wasmtime_component_val_t
+/// layout.
+///
+///
+///
+/// Ownership is asymmetric. Arguments are declared const by the C API, so Wasmtime never
+/// takes them: everything written here is allocated by us and released by the caller's
+/// . Results are the opposite, being allocated by Wasmtime's own
+/// allocator, so their contents must be released with
+/// wasmtime_component_val_delete rather than freed directly. Mixing the two allocators
+/// corrupts the heap.
+///
+///
+/// Argument values are written into caller-owned storage. Host results use Wasmtime's own
+/// constructors so their nested allocations have the correct owner.
+///
+///
+internal static class ComponentValueMarshaller
+{
+ /// Size of wasmtime_component_val_t: a 1-byte kind padded to 8, plus a 24-byte union.
+ public const int ValueSize = 32;
+
+ /// Offset of the of union within wasmtime_component_val_t.
+ public const int ValuePayloadOffset = 8;
+
+ /// Size of wasmtime_component_valrecord_entry_t: a 16-byte name plus a 32-byte value.
+ public const int RecordEntrySize = 48;
+
+ /// Offset of the value within wasmtime_component_valrecord_entry_t.
+ public const int RecordEntryValueOffset = 16;
+
+ /// Offset of the data pointer within a vec or a wasm_name_t.
+ private const int VectorDataOffset = 8;
+
+ ///
+ /// Tracks the native allocations made while writing arguments, so they can be released
+ /// together once the call has returned.
+ ///
+ public sealed class AllocationScope : IDisposable
+ {
+ private readonly List allocations = new List();
+ private bool disposed;
+
+ ///
+ /// Allocates zeroed native memory whose lifetime is bound to this scope.
+ ///
+ /// The number of bytes to allocate.
+ /// A pointer to the allocation.
+ public IntPtr Allocate(int bytes)
+ {
+ if (disposed)
+ {
+ throw new ObjectDisposedException(nameof(AllocationScope));
+ }
+
+ var pointer = Marshal.AllocHGlobal(bytes);
+ allocations.Add(pointer);
+
+ unsafe
+ {
+ new Span((void*)pointer, bytes).Clear();
+ }
+
+ return pointer;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (disposed)
+ {
+ return;
+ }
+
+ disposed = true;
+ FreeAllocations();
+ }
+
+ private void FreeAllocations()
+ {
+ foreach (var pointer in allocations)
+ {
+ Marshal.FreeHGlobal(pointer);
+ }
+
+ allocations.Clear();
+ }
+ }
+
+ ///
+ /// Writes a value into caller-owned native storage.
+ ///
+ /// The value to write.
+ /// A pointer to bytes of storage.
+ /// The scope owning any secondary allocations the value needs.
+ public static void Write(ComponentValue value, IntPtr destination, AllocationScope scope)
+ {
+ if (WritePrimitive(value, destination))
+ {
+ return;
+ }
+
+ Marshal.WriteByte(destination, (byte)value.Kind);
+ var payload = destination + ValuePayloadOffset;
+
+ switch (value.Kind)
+ {
+ case ComponentValueKind.String:
+ case ComponentValueKind.Enum:
+ WriteName(value.Text ?? string.Empty, payload, scope);
+ break;
+
+ case ComponentValueKind.List:
+ case ComponentValueKind.Tuple:
+ WriteVector(value.Items, payload, scope);
+ break;
+
+ case ComponentValueKind.Record:
+ WriteRecord(value.Fields, payload, scope);
+ break;
+
+ case ComponentValueKind.Option:
+ Marshal.WriteIntPtr(payload, value.Flag ? WriteBoxed(value.Payload!, scope) : IntPtr.Zero);
+ break;
+
+ case ComponentValueKind.Result:
+ Marshal.WriteByte(payload, (byte)(value.Flag ? 1 : 0));
+ Marshal.WriteIntPtr(
+ payload + VectorDataOffset,
+ value.Payload is null ? IntPtr.Zero : WriteBoxed(value.Payload, scope));
+ break;
+
+ default:
+ throw new NotSupportedException(
+ $"Writing component values of kind {value.Kind} is not supported.");
+ }
+ }
+
+ ///
+ /// Writes a value into storage whose contents Wasmtime will free, as required for the
+ /// results of a host-defined function.
+ ///
+ /// The value to write.
+ /// A pointer to bytes of storage.
+ ///
+ /// Heap-backed values are constructed with Wasmtime's allocator so Wasmtime can safely
+ /// deallocate the result after the host callback returns.
+ ///
+ public static void WriteOwned(ComponentValue value, IntPtr destination)
+ {
+ ClearValue(destination);
+ try
+ {
+ if (!WritePrimitive(value, destination))
+ {
+ WriteOwnedHeapValue(value, destination);
+ }
+ }
+ catch
+ {
+ ComponentValueNative.wasmtime_component_val_delete(destination);
+ ClearValue(destination);
+ throw;
+ }
+ }
+
+ private static bool WritePrimitive(ComponentValue value, IntPtr destination)
+ {
+ var payload = destination + ValuePayloadOffset;
+
+ switch (value.Kind)
+ {
+ case ComponentValueKind.Bool:
+ Marshal.WriteByte(payload, (byte)(value.Integer != 0 ? 1 : 0));
+ break;
+
+ case ComponentValueKind.S8:
+ case ComponentValueKind.U8:
+ Marshal.WriteByte(payload, unchecked((byte)value.Integer));
+ break;
+
+ case ComponentValueKind.S16:
+ case ComponentValueKind.U16:
+ Marshal.WriteInt16(payload, unchecked((short)value.Integer));
+ break;
+
+ case ComponentValueKind.S32:
+ case ComponentValueKind.U32:
+ Marshal.WriteInt32(payload, unchecked((int)value.Integer));
+ break;
+
+ case ComponentValueKind.F32:
+ Marshal.WriteInt32(payload, Extensions.SingleToInt32Bits((float)value.Real));
+ break;
+
+ case ComponentValueKind.S64:
+ case ComponentValueKind.U64:
+ Marshal.WriteInt64(payload, value.Integer);
+ break;
+
+ case ComponentValueKind.F64:
+ Marshal.WriteInt64(payload, BitConverter.DoubleToInt64Bits(value.Real));
+ break;
+
+ default:
+ return false;
+ }
+
+ Marshal.WriteByte(destination, (byte)value.Kind);
+ return true;
+ }
+
+ private static void WriteOwnedHeapValue(ComponentValue value, IntPtr destination)
+ {
+ var payload = destination + ValuePayloadOffset;
+
+ switch (value.Kind)
+ {
+ case ComponentValueKind.String:
+ case ComponentValueKind.Enum:
+ Marshal.WriteByte(destination, (byte)value.Kind);
+ WriteOwnedName(value.Text ?? string.Empty, payload);
+ break;
+
+ case ComponentValueKind.List:
+ case ComponentValueKind.Tuple:
+ WriteOwnedVector(value.Items, destination, value.Kind);
+ break;
+
+ case ComponentValueKind.Record:
+ WriteOwnedRecord(value.Fields, destination);
+ break;
+
+ case ComponentValueKind.Option:
+ Marshal.WriteByte(destination, (byte)value.Kind);
+ if (value.Flag)
+ {
+ Marshal.WriteIntPtr(payload, WriteOwnedBoxed(value.Payload!));
+ }
+
+ break;
+
+ case ComponentValueKind.Result:
+ Marshal.WriteByte(destination, (byte)value.Kind);
+ Marshal.WriteByte(payload, (byte)(value.Flag ? 1 : 0));
+ if (value.Payload is not null)
+ {
+ Marshal.WriteIntPtr(payload + VectorDataOffset, WriteOwnedBoxed(value.Payload));
+ }
+
+ break;
+
+ default:
+ throw new NotSupportedException(
+ $"Writing component values of kind {value.Kind} is not supported.");
+ }
+ }
+
+ private static void WriteOwnedName(string text, IntPtr destination)
+ {
+ var byteCount = Encoding.UTF8.GetByteCount(text);
+ ComponentValueNative.wasm_byte_vec_new_uninitialized(destination, (nuint)byteCount);
+
+ var data = Marshal.ReadIntPtr(destination + VectorDataOffset);
+ unsafe
+ {
+ fixed (char* textPtr = text)
+ {
+ Encoding.UTF8.GetBytes(textPtr, text.Length, (byte*)data, byteCount);
+ }
+ }
+ }
+
+ private static void WriteOwnedVector(
+ IReadOnlyList items,
+ IntPtr destination,
+ ComponentValueKind kind)
+ {
+ var count = items.Count;
+ checked
+ {
+ _ = count * ValueSize;
+ }
+
+ Marshal.WriteByte(destination, (byte)kind);
+ var vector = destination + ValuePayloadOffset;
+ if (kind == ComponentValueKind.List)
+ {
+ ComponentValueNative.wasmtime_component_vallist_new_uninit(vector, (nuint)count);
+ }
+ else
+ {
+ ComponentValueNative.wasmtime_component_valtuple_new_uninit(vector, (nuint)count);
+ }
+
+ var element = Marshal.ReadIntPtr(vector + VectorDataOffset);
+ for (var i = 0; i < count; i++)
+ {
+ ClearValue(element + (i * ValueSize));
+ }
+
+ for (var i = 0; i < count; i++)
+ {
+ WriteOwned(items[i], element + (i * ValueSize));
+ }
+ }
+
+ private static void WriteOwnedRecord(
+ IReadOnlyList> fields,
+ IntPtr destination)
+ {
+ var count = fields.Count;
+ checked
+ {
+ _ = count * RecordEntrySize;
+ }
+
+ Marshal.WriteByte(destination, (byte)ComponentValueKind.Record);
+ var vector = destination + ValuePayloadOffset;
+ ComponentValueNative.wasmtime_component_valrecord_new_uninit(vector, (nuint)count);
+
+ var entry = Marshal.ReadIntPtr(vector + VectorDataOffset);
+ for (var i = 0; i < count; i++)
+ {
+ unsafe
+ {
+ new Span((void*)(entry + (i * RecordEntrySize)), RecordEntrySize).Clear();
+ }
+ }
+
+ for (var i = 0; i < count; i++)
+ {
+ var currentEntry = entry + (i * RecordEntrySize);
+ WriteOwnedName(fields[i].Key, currentEntry);
+ WriteOwned(fields[i].Value, currentEntry + RecordEntryValueOffset);
+ }
+ }
+
+ private static IntPtr WriteOwnedBoxed(ComponentValue value)
+ {
+ unsafe
+ {
+ var boxed = stackalloc byte[ValueSize];
+ var pointer = (IntPtr)boxed;
+ ClearValue(pointer);
+
+ var initialized = false;
+ try
+ {
+ WriteOwned(value, pointer);
+ initialized = true;
+
+ var owned = ComponentValueNative.wasmtime_component_val_new(pointer);
+ initialized = false;
+ return owned;
+ }
+ finally
+ {
+ if (initialized)
+ {
+ ComponentValueNative.wasmtime_component_val_delete(pointer);
+ }
+ }
+ }
+ }
+
+ private static void ClearValue(IntPtr value)
+ {
+ unsafe
+ {
+ new Span((void*)value, ValueSize).Clear();
+ }
+ }
+
+ ///
+ /// Reads a value from native storage.
+ ///
+ /// A pointer to a wasmtime_component_val_t.
+ /// The value that was read.
+ public static ComponentValue Read(IntPtr source)
+ {
+ var kind = (ComponentValueKind)Marshal.ReadByte(source);
+ var payload = source + ValuePayloadOffset;
+
+ switch (kind)
+ {
+ case ComponentValueKind.Bool:
+ return ComponentValue.Bool(Marshal.ReadByte(payload) != 0);
+
+ case ComponentValueKind.S8:
+ return ComponentValue.S8(unchecked((sbyte)Marshal.ReadByte(payload)));
+
+ case ComponentValueKind.U8:
+ return ComponentValue.U8(Marshal.ReadByte(payload));
+
+ case ComponentValueKind.S16:
+ return ComponentValue.S16(Marshal.ReadInt16(payload));
+
+ case ComponentValueKind.U16:
+ return ComponentValue.U16(unchecked((ushort)Marshal.ReadInt16(payload)));
+
+ case ComponentValueKind.S32:
+ return ComponentValue.S32(Marshal.ReadInt32(payload));
+
+ case ComponentValueKind.U32:
+ return ComponentValue.U32(unchecked((uint)Marshal.ReadInt32(payload)));
+
+ case ComponentValueKind.S64:
+ return ComponentValue.S64(Marshal.ReadInt64(payload));
+
+ case ComponentValueKind.U64:
+ return ComponentValue.U64(unchecked((ulong)Marshal.ReadInt64(payload)));
+
+ case ComponentValueKind.F32:
+ return ComponentValue.F32(Extensions.Int32BitsToSingle(Marshal.ReadInt32(payload)));
+
+ case ComponentValueKind.F64:
+ return ComponentValue.F64(BitConverter.Int64BitsToDouble(Marshal.ReadInt64(payload)));
+
+ case ComponentValueKind.String:
+ return ComponentValue.String(ReadName(payload));
+
+ case ComponentValueKind.Enum:
+ return ComponentValue.Enum(ReadName(payload));
+
+ case ComponentValueKind.List:
+ return ComponentValue.OwnedList(ReadVector(payload));
+
+ case ComponentValueKind.Tuple:
+ return ComponentValue.OwnedTuple(ReadVector(payload));
+
+ case ComponentValueKind.Record:
+ return ComponentValue.OwnedRecord(ReadRecord(payload));
+
+ case ComponentValueKind.Option:
+ var some = Marshal.ReadIntPtr(payload);
+ return some == IntPtr.Zero ? ComponentValue.None() : ComponentValue.Some(Read(some));
+
+ case ComponentValueKind.Result:
+ var isOk = Marshal.ReadByte(payload) != 0;
+ var inner = Marshal.ReadIntPtr(payload + VectorDataOffset);
+ var result = inner == IntPtr.Zero ? null : Read(inner);
+ return isOk ? ComponentValue.Ok(result) : ComponentValue.Err(result);
+
+ default:
+ throw new NotSupportedException(
+ $"Reading component values of kind {kind} is not supported.");
+ }
+ }
+
+ private static IntPtr WriteBoxed(ComponentValue value, AllocationScope scope)
+ {
+ var boxed = scope.Allocate(ValueSize);
+ Write(value, boxed, scope);
+ return boxed;
+ }
+
+ private static void WriteName(string text, IntPtr destination, AllocationScope scope)
+ {
+ var byteCount = Encoding.UTF8.GetByteCount(text);
+
+ // A zero-length allocation would still need a non-null pointer, so always take at least one byte.
+ var buffer = scope.Allocate(Math.Max(byteCount, 1));
+ unsafe
+ {
+ fixed (char* textPtr = text)
+ {
+ Encoding.UTF8.GetBytes(textPtr, text.Length, (byte*)buffer, byteCount);
+ }
+ }
+
+ Marshal.WriteIntPtr(destination, (IntPtr)byteCount);
+ Marshal.WriteIntPtr(destination + VectorDataOffset, buffer);
+ }
+
+ private static void WriteVector(IReadOnlyList items, IntPtr destination, AllocationScope scope)
+ {
+ // Read once: the buffer size, loop bound and written length must agree even if the list changes.
+ var count = items.Count;
+
+ // checked: an overflowing size would otherwise under-allocate and be written past.
+ var buffer = scope.Allocate(Math.Max(checked(count * ValueSize), 1));
+ var element = buffer;
+
+ for (var i = 0; i < count; i++)
+ {
+ Write(items[i], element, scope);
+ element += ValueSize;
+ }
+
+ Marshal.WriteIntPtr(destination, (IntPtr)count);
+ Marshal.WriteIntPtr(destination + VectorDataOffset, buffer);
+ }
+
+ private static void WriteRecord(
+ IReadOnlyList> fields,
+ IntPtr destination,
+ AllocationScope scope)
+ {
+ var count = fields.Count;
+ var buffer = scope.Allocate(Math.Max(checked(count * RecordEntrySize), 1));
+ var entry = buffer;
+
+ for (var i = 0; i < count; i++)
+ {
+ WriteName(fields[i].Key, entry, scope);
+ Write(fields[i].Value, entry + RecordEntryValueOffset, scope);
+ entry += RecordEntrySize;
+ }
+
+ Marshal.WriteIntPtr(destination, (IntPtr)count);
+ Marshal.WriteIntPtr(destination + VectorDataOffset, buffer);
+ }
+
+ private static string ReadName(IntPtr source)
+ {
+ var size = ReadLength(source);
+ var data = Marshal.ReadIntPtr(source + VectorDataOffset);
+
+ return data == IntPtr.Zero || size == 0 ? string.Empty : Extensions.PtrToStringUTF8(data, size);
+ }
+
+ private static List ReadVector(IntPtr source)
+ {
+ var size = ReadLength(source);
+ var element = Marshal.ReadIntPtr(source + VectorDataOffset);
+ var items = new List(size);
+
+ for (var i = 0; i < size; i++)
+ {
+ items.Add(Read(element));
+ element += ValueSize;
+ }
+
+ return items;
+ }
+
+ private static List> ReadRecord(IntPtr source)
+ {
+ var size = ReadLength(source);
+ var entry = Marshal.ReadIntPtr(source + VectorDataOffset);
+ var fields = new List>(size);
+
+ for (var i = 0; i < size; i++)
+ {
+ fields.Add(new KeyValuePair(
+ ReadName(entry),
+ Read(entry + RecordEntryValueOffset)));
+
+ entry += RecordEntrySize;
+ }
+
+ return fields;
+ }
+
+ ///
+ /// Reads a native size_t length, rejecting values too large to be represented
+ /// managed-side rather than silently truncating them.
+ ///
+ private static int ReadLength(IntPtr source)
+ {
+ // Reinterpreted rather than converted, because size_t is unsigned.
+ var size = unchecked((ulong)Marshal.ReadIntPtr(source).ToInt64());
+ if (size > int.MaxValue)
+ {
+ throw new NotSupportedException(
+ $"A component value with {size} elements exceeds the maximum supported length of {int.MaxValue}.");
+ }
+
+ return (int)size;
+ }
+}
diff --git a/src/Module.cs b/src/Module.cs
index 421558e7..6df18def 100644
--- a/src/Module.cs
+++ b/src/Module.cs
@@ -1,12 +1,15 @@
+using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
-using Microsoft.Win32.SafeHandles;
namespace Wasmtime
{
+ ///
+ /// Equivalent to wasm_byte_vec_t
+ ///
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct ByteArray : IDisposable
{
@@ -18,6 +21,20 @@ public void Dispose()
Native.wasm_byte_vec_delete(this);
}
+ public Span AsSpan()
+ {
+ return new Span(data, checked((int)size));
+ }
+
+ public byte[] ToArray()
+ {
+ var src = AsSpan();
+ var dst = new byte[src.Length];
+ src.CopyTo(dst);
+
+ return dst;
+ }
+
private static class Native
{
[DllImport(Engine.LibraryName)]
@@ -236,22 +253,14 @@ public static Module FromTextStream(Engine engine, string name, Stream stream)
/// Returns the serialized module as an array of bytes.
public byte[] Serialize()
{
- var error = Native.wasmtime_module_serialize(this.handle, out var array);
+ var error = Native.wasmtime_module_serialize(handle, out var bytes);
if (error != IntPtr.Zero)
{
throw WasmtimeException.FromOwnedError(error);
}
- using (array)
- {
- var len = checked((int)array.size);
- var bytes = new byte[len];
- unsafe
- {
- Marshal.Copy((IntPtr)array.data, bytes, 0, len);
- }
- return bytes;
- }
+ using (bytes)
+ return bytes.ToArray();
}
///
diff --git a/src/Wasmtime.csproj b/src/Wasmtime.csproj
index b88a7d93..34702165 100644
--- a/src/Wasmtime.csproj
+++ b/src/Wasmtime.csproj
@@ -157,26 +157,11 @@ The .NET embedding of Wasmtime enables .NET code to instantiate WebAssembly modu
-
+
-
-
+
+
diff --git a/tests/ComponentFixture.cs b/tests/ComponentFixture.cs
new file mode 100644
index 00000000..a9734372
--- /dev/null
+++ b/tests/ComponentFixture.cs
@@ -0,0 +1,47 @@
+using System;
+using System.IO;
+using Wasmtime.Components;
+
+namespace Wasmtime.Tests
+{
+ public abstract class ComponentFixture : IDisposable
+ {
+ public ComponentFixture()
+ {
+ Engine = new Engine(GetEngineConfig());
+
+ Store = new Store(Engine);
+ }
+
+ public virtual Config GetEngineConfig()
+ {
+ return new Config()
+ .WithComponentModel(true);
+ }
+
+ public Component LoadComponent(string fileName) =>
+ Component.FromText(Engine, File.ReadAllText(Path.Combine("Components", fileName)));
+
+ public Store CreateStore() => new Store(Engine);
+
+ public Component Strings() => LoadComponent("strings.wat");
+
+ public void Dispose()
+ {
+ if (!(Store is null))
+ {
+ Store.Dispose();
+ Store = null;
+ }
+
+ if (!(Engine is null))
+ {
+ Engine.Dispose();
+ Engine = null;
+ }
+ }
+
+ public Engine Engine { get; set; }
+ public Store Store { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/ComponentFunctionTests.cs b/tests/ComponentFunctionTests.cs
new file mode 100644
index 00000000..195d4d61
--- /dev/null
+++ b/tests/ComponentFunctionTests.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using FluentAssertions;
+using Wasmtime.Components;
+using Xunit;
+
+namespace Wasmtime.Tests
+{
+ public class ComponentFunctionFixture : ComponentFixture { }
+
+ public sealed class ComponentFunctionTests : IClassFixture, IDisposable
+ {
+ private readonly ComponentFunctionFixture fixture;
+ private readonly ComponentLinker linker;
+ private readonly Store store;
+
+ public ComponentFunctionTests(ComponentFunctionFixture fixture)
+ {
+ this.fixture = fixture;
+ store = new Store(fixture.Engine);
+ linker = new ComponentLinker(fixture.Engine);
+ }
+
+ public void Dispose()
+ {
+ store.Dispose();
+ linker.Dispose();
+ }
+
+ private ComponentInstance InstantiateTinyComponent()
+ {
+ using var component = fixture.LoadComponent("tiny.wat");
+ return linker.Instantiate(store, component);
+ }
+
+ [Fact]
+ public void ItCallsAnExportedFunction()
+ {
+ var instance = InstantiateTinyComponent();
+
+ var add = instance.GetFunction("add");
+
+ add.Should().NotBeNull();
+ add!.Call(ComponentValue.S32(2), ComponentValue.S32(3))!.AsS32().Should().Be(5);
+ }
+
+ [Fact]
+ public void ItReportsTheFunctionSignature()
+ {
+ var instance = InstantiateTinyComponent();
+
+ var add = instance.GetFunction("add")!;
+
+ add.ParameterCount.Should().Be(2);
+ add.HasResult.Should().BeTrue();
+ }
+
+ [Fact]
+ public void ItCallsAFunctionRepeatedly()
+ {
+ var instance = InstantiateTinyComponent();
+ var add = instance.GetFunction("add")!;
+
+ for (var i = 0; i < 100; i++)
+ {
+ add.Call(ComponentValue.S32(i), ComponentValue.S32(i))!.AsS32().Should().Be(i * 2);
+ }
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0)]
+ [InlineData(-1, 1, 0)]
+ [InlineData(int.MaxValue, 1, int.MinValue)]
+ [InlineData(int.MinValue, -1, int.MaxValue)]
+ public void ItRoundTripsArgumentsAndResults(int a, int b, int expected)
+ {
+ var instance = InstantiateTinyComponent();
+ var add = instance.GetFunction("add")!;
+
+ add.Call(ComponentValue.S32(a), ComponentValue.S32(b))!.AsS32().Should().Be(expected);
+ }
+
+ [Fact]
+ public void ItReturnsNullForAnUnknownExport()
+ {
+ var instance = InstantiateTinyComponent();
+
+ instance.GetExport("nope").Should().BeNull();
+ instance.GetFunction("nope").Should().BeNull();
+ instance.GetFunction("no:such/iface", "nope").Should().BeNull();
+ }
+
+ [Fact]
+ public void ItThrowsForTheWrongNumberOfArguments()
+ {
+ var instance = InstantiateTinyComponent();
+ var add = instance.GetFunction("add")!;
+
+ add.Invoking(f => f.Call(ComponentValue.S32(1)))
+ .Should().Throw().WithMessage("*2 argument(s)*1 were given*");
+ add.Invoking(f => f.Call())
+ .Should().Throw();
+ }
+
+ [Fact]
+ public void ItThrowsForNullArguments()
+ {
+ var instance = InstantiateTinyComponent();
+ var add = instance.GetFunction("add")!;
+
+ add.Invoking(f => f.Call((ComponentValue[])null!)).Should().Throw();
+ instance.Invoking(i => i.GetExport(null!)).Should().Throw();
+ instance.Invoking(i => i.GetFunction((ComponentExport)null!)).Should().Throw();
+ instance.Invoking(i => i.GetFunction("no:such/iface", null!)).Should().Throw();
+ }
+
+ [Fact]
+ public void ItUsesOneArgumentCountForMarshalling()
+ {
+ var instance = InstantiateTinyComponent();
+ var add = instance.GetFunction("add")!;
+ var arguments = new ChangingCountList(ComponentValue.S32(2), ComponentValue.S32(3));
+
+ add.Call(arguments)!.AsS32().Should().Be(5);
+ }
+
+ [Fact]
+ public void ItCallsBackIntoTheHost()
+ {
+ var observed = 0;
+
+ using (var root = linker.Root())
+ using (var host = root.AddInstance("host"))
+ {
+ host.DefineFunction("transform", (arguments, results) =>
+ {
+ observed = arguments[0].AsS32();
+ results[0] = ComponentValue.S32(observed * 2);
+ });
+
+ host.DefineFunction("greet", (arguments, results) =>
+ results[0] = ComponentValue.String($"hello, {arguments[0].AsString()}"));
+ }
+
+ using var component = fixture.LoadComponent("host-import.wat");
+ var instance = linker.Instantiate(store, component);
+
+ instance.GetFunction("run")!.Call(ComponentValue.S32(21))!.AsS32().Should().Be(42);
+ observed.Should().Be(21);
+ }
+
+ [Fact]
+ public void ItSurfacesHostExceptionsAsTraps()
+ {
+ using (var root = linker.Root())
+ using (var host = root.AddInstance("host"))
+ {
+ host.DefineFunction("transform", (arguments, results) =>
+ throw new InvalidOperationException("host went bang"));
+ host.DefineFunction("greet", (arguments, results) =>
+ results[0] = ComponentValue.String(string.Empty));
+ }
+
+ using var component = fixture.LoadComponent("host-import.wat");
+ linker.Invoking(l => l.Instantiate(store, component))
+ .Should().Throw()
+ .WithMessage("*host went bang*");
+ }
+
+ [Fact]
+ public void ItThrowsWhenAHostFunctionDoesNotSetItsResult()
+ {
+ using (var root = linker.Root())
+ using (var host = root.AddInstance("host"))
+ {
+ host.DefineFunction("transform", (arguments, results) => { });
+ host.DefineFunction("greet", (arguments, results) =>
+ results[0] = ComponentValue.String(string.Empty));
+ }
+
+ using var component = fixture.LoadComponent("host-import.wat");
+ linker.Invoking(l => l.Instantiate(store, component))
+ .Should().Throw()
+ .WithMessage("*did not assign result*");
+ }
+
+ ///
+ /// A trap poisons its entire Store, not just the instance that trapped. This holds for
+ /// guest-side traps and for host callbacks that throw, so it is pinned here: sharing a
+ /// Store across calls that may trap silently breaks every later call.
+ ///
+ [Fact]
+ public void ATrapPoisonsTheWholeStore()
+ {
+ using var tiny = fixture.LoadComponent("tiny.wat");
+ using var trap = fixture.LoadComponent("trap.wat");
+
+ var add = linker.Instantiate(store, tiny).GetFunction("add")!;
+ add.Call(ComponentValue.S32(1), ComponentValue.S32(1))!.AsS32().Should().Be(2);
+
+ linker.Instantiate(store, trap).GetFunction("boom")!
+ .Invoking(f => f.Call())
+ .Should().Throw();
+
+ add.Invoking(f => f.Call(ComponentValue.S32(1), ComponentValue.S32(1)))
+ .Should().Throw()
+ .WithMessage("*cannot enter component instance*");
+ }
+ private sealed class ChangingCountList : IReadOnlyList
+ {
+ private readonly ComponentValue[] items;
+ private int countReads;
+
+ public ChangingCountList(params ComponentValue[] items)
+ {
+ this.items = items;
+ }
+
+ public int Count => countReads++ == 0 ? items.Length : items.Length + 1;
+
+ public ComponentValue this[int index] => items[index];
+
+ public IEnumerator GetEnumerator() => ((IEnumerable)items).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => items.GetEnumerator();
+ }
+
+ private ComponentInstance InstantiateStrings(ComponentLinker linker, Store store)
+ {
+ using var component = fixture.Strings();
+ return linker.Instantiate(store, component);
+ }
+
+ [Theory]
+ [InlineData("world", "hi world")]
+ [InlineData("", "hi ")]
+ [InlineData("h\u00e9llo", "hi h\u00e9llo")]
+ [InlineData("\u4e16\u754c", "hi \u4e16\u754c")]
+ [InlineData("\ud83c\udf89", "hi \ud83c\udf89")]
+ public void ItPassesAndReturnsStrings(string name, string expected)
+ {
+ using var store = fixture.CreateStore();
+ using var linker = new ComponentLinker(fixture.Engine);
+ var greet = InstantiateStrings(linker, store).GetFunction("greet")!;
+
+ greet.Call(ComponentValue.String(name))!.AsString().Should().Be(expected);
+ }
+
+ ///
+ /// The guest reports the byte length it was given, so this fails if the argument was
+ /// encoded as anything other than UTF-8.
+ ///
+ [Theory]
+ [InlineData("abc", 3)]
+ [InlineData("h\u00e9llo", 6)]
+ [InlineData("\u4e16\u754c", 6)]
+ [InlineData("\ud83c\udf89", 4)]
+ [InlineData("", 0)]
+ public void ItPassesStringsAsUtf8(string name, int expectedByteLength)
+ {
+ using var store = fixture.CreateStore();
+ using var linker = new ComponentLinker(fixture.Engine);
+ var length = InstantiateStrings(linker, store).GetFunction("length")!;
+
+ length.Call(ComponentValue.String(name))!.AsS32().Should().Be(expectedByteLength);
+ }
+
+ [Fact]
+ public void ItReturnsALongStringFromTheGuest()
+ {
+ using var store = fixture.CreateStore();
+ using var linker = new ComponentLinker(fixture.Engine);
+ var greet = InstantiateStrings(linker, store).GetFunction("greet")!;
+
+ var name = new string('x', 10_000);
+
+ greet.Call(ComponentValue.String(name))!.AsString().Should().Be("hi " + name);
+ }
+
+ [Fact]
+ public void ItReturnsAListFromTheGuest()
+ {
+ using var store = fixture.CreateStore();
+ using var linker = new ComponentLinker(fixture.Engine);
+ var numbers = InstantiateStrings(linker, store).GetFunction("numbers")!;
+
+ numbers.ParameterCount.Should().Be(0);
+
+ var result = numbers.Call()!.AsList();
+
+ result.Should().HaveCount(3);
+ result[0].AsS32().Should().Be(10);
+ result[1].AsS32().Should().Be(20);
+ result[2].AsS32().Should().Be(30);
+ }
+ }
+}
diff --git a/tests/ComponentLinkerTests.cs b/tests/ComponentLinkerTests.cs
new file mode 100644
index 00000000..433e7f90
--- /dev/null
+++ b/tests/ComponentLinkerTests.cs
@@ -0,0 +1,191 @@
+using System;
+using FluentAssertions;
+using Wasmtime.Components;
+using Xunit;
+
+namespace Wasmtime.Tests
+{
+ public class ComponentLinkerFixture : ComponentFixture { }
+
+ public sealed class ComponentLinkerTests : IClassFixture
+ {
+ private readonly ComponentLinkerFixture fixture;
+
+ public ComponentLinkerTests(ComponentLinkerFixture fixture)
+ {
+ this.fixture = fixture;
+ }
+
+ [Fact]
+ public void ItThrowsForANullEngine()
+ {
+ FluentActions.Invoking(() => new ComponentLinker(null!))
+ .Should().Throw();
+ }
+
+ [Fact]
+ public void ItInstantiatesAComponentWithoutImports()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+ using var component = fixture.LoadComponent("tiny.wat");
+
+ var instance = linker.Instantiate(fixture.Store, component);
+
+ instance.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ItThrowsWhenAnImportIsNotDefined()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+ using var component = fixture.LoadComponent("host-import-declarations.wat");
+
+ linker.Invoking(l => l.Instantiate(fixture.Store, component))
+ .Should().Throw();
+ }
+
+ [Fact]
+ public void ItInstantiatesWithHostFunctionsDefined()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+ using var component = fixture.LoadComponent("host-import.wat");
+
+ var receivedInput = 0;
+ DefineHost(linker, value => receivedInput = value);
+
+ linker.Instantiate(fixture.Store, component).Should().NotBeNull();
+ receivedInput.Should().Be(21);
+ }
+
+ [Fact]
+ public void ItSatisfiesUnknownImportsWithTraps()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+ using var component = fixture.LoadComponent("host-import-declarations.wat");
+
+ linker.DefineUnknownImportsAsTraps(component);
+
+ linker.Instantiate(fixture.Store, component).Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ItThrowsWhenUsingTheLinkerWhileTheRootIsAlive()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+ using var component = fixture.LoadComponent("tiny.wat");
+
+ var root = linker.Root();
+
+ linker.Invoking(l => l.Instantiate(fixture.Store, component))
+ .Should().Throw();
+ linker.Invoking(l => l.Root())
+ .Should().Throw();
+
+ root.Dispose();
+
+ linker.Instantiate(fixture.Store, component).Should().NotBeNull();
+ }
+
+ [Fact]
+ public void ItThrowsWhenUsingAnInstanceWhileItsNestedInstanceIsAlive()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+ using var root = linker.Root();
+
+ var nested = root.AddInstance("host");
+
+ root.Invoking(r => r.AddInstance("other"))
+ .Should().Throw();
+
+ nested.Dispose();
+
+ root.Invoking(r => r.AddInstance("other")).Should().NotThrow();
+ }
+
+ [Fact]
+ public void ItDisposesNestedInstancesWithTheirParent()
+ {
+ var linker = new ComponentLinker(fixture.Engine);
+ var root = linker.Root();
+ var nested = root.AddInstance("host");
+
+ linker.Dispose();
+
+ nested.Invoking(n => n.DefineFunction("f", (args, results) => { }))
+ .Should().Throw();
+ }
+
+ [Fact]
+ public void ItThrowsForDuplicateDefinitionsUnlessShadowingIsAllowed()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+
+ using (var root = linker.Root())
+ {
+ using var host = root.AddInstance("host");
+ host.DefineFunction("transform", (args, results) => results[0] = ComponentValue.S32(0));
+
+ host.Invoking(h => h.DefineFunction("transform", (args, results) => results[0] = ComponentValue.S32(0)))
+ .Should().Throw();
+ }
+
+ linker.AllowShadowing = true;
+
+ using (var root = linker.Root())
+ {
+ using var host = root.AddInstance("host");
+ host.Invoking(h => h.DefineFunction("transform", (args, results) => results[0] = ComponentValue.S32(0)))
+ .Should().NotThrow();
+ }
+ }
+
+ [Fact]
+ public void ItThrowsForNullArguments()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+
+ linker.Invoking(l => l.Instantiate(null!, fixture.LoadComponent("tiny.wat"))).Should().Throw();
+ linker.Invoking(l => l.Instantiate(fixture.Store, null!)).Should().Throw();
+ linker.Invoking(l => l.DefineUnknownImportsAsTraps(null!)).Should().Throw();
+
+ using var root = linker.Root();
+ root.Invoking(r => r.AddInstance(null!)).Should().Throw();
+ root.Invoking(r => r.DefineFunction(null!, (args, results) => { })).Should().Throw();
+ root.Invoking(r => r.DefineFunction("f", null!)).Should().Throw();
+ root.Invoking(r => r.AddModule(null!, null!)).Should().Throw();
+ }
+
+ [Fact]
+ public void ItThrowsAfterDisposal()
+ {
+ var linker = new ComponentLinker(fixture.Engine);
+ linker.Dispose();
+
+ linker.Invoking(l => l.Root()).Should().Throw();
+ }
+
+ [Fact]
+ public void ItAddsWasiPreview2()
+ {
+ using var linker = new ComponentLinker(fixture.Engine);
+
+ linker.Invoking(l => l.AddWasiPreview2()).Should().NotThrow();
+ }
+
+ private static void DefineHost(ComponentLinker linker, Action onTransform)
+ {
+ using var root = linker.Root();
+ using var host = root.AddInstance("host");
+
+ host.DefineFunction("transform", (arguments, results) =>
+ {
+ var input = arguments[0].AsS32();
+ onTransform(input);
+ results[0] = ComponentValue.S32(input * 2);
+ });
+
+ host.DefineFunction("greet", (arguments, results) =>
+ results[0] = ComponentValue.String($"hello, {arguments[0].AsString()}"));
+ }
+ }
+}
diff --git a/tests/ComponentTests.cs b/tests/ComponentTests.cs
new file mode 100644
index 00000000..85100a89
--- /dev/null
+++ b/tests/ComponentTests.cs
@@ -0,0 +1,107 @@
+using System;
+using System.IO;
+using FluentAssertions;
+using Wasmtime.Components;
+using Xunit;
+
+namespace Wasmtime.Tests
+{
+ public class ComponentTestsFixture : ComponentFixture { }
+
+ public sealed class ComponentTests : IClassFixture
+ {
+ private const string Text = @"
+(component
+ (core module $m
+ (func (export ""add"") (param i32 i32) (result i32)
+ local.get 0
+ local.get 1
+ i32.add)
+ )
+ (core instance $i (instantiate $m))
+ (func (export ""add"") (param ""a"" s32) (param ""b"" s32) (result s32)
+ (canon lift (core func $i ""add"")))
+)";
+
+ private readonly ComponentTestsFixture fixture;
+
+ public ComponentTests(ComponentTestsFixture fixture)
+ {
+ this.fixture = fixture;
+ }
+
+ [Fact]
+ public void ItLoadsAComponentFromText()
+ {
+ using var store = fixture.CreateStore();
+ using var component = Component.FromText(fixture.Engine, Text);
+ using var linker = new ComponentLinker(fixture.Engine);
+
+ var add = linker.Instantiate(store, component).GetFunction("add")!;
+
+ add.Call(ComponentValue.S32(40), ComponentValue.S32(2))!.AsS32().Should().Be(42);
+ }
+
+ [Fact]
+ public void ItLoadsAComponentFromATextFile()
+ {
+ using var store = fixture.CreateStore();
+ using var component = Component.FromTextFile(fixture.Engine, "Components/tiny.wat");
+ using var linker = new ComponentLinker(fixture.Engine);
+
+ var add = linker.Instantiate(store, component).GetFunction("add")!;
+
+ add.Call(ComponentValue.S32(1), ComponentValue.S32(2))!.AsS32().Should().Be(3);
+ }
+
+ [Fact]
+ public void ItThrowsForInvalidText()
+ {
+ FluentActions.Invoking(() => Component.FromText(fixture.Engine, "(component (this is not valid)"))
+ .Should().Throw();
+ }
+
+ [Fact]
+ public void ItThrowsForInvalidBytes()
+ {
+ FluentActions.Invoking(() => Component.FromBytes(fixture.Engine, new byte[] { 1, 2, 3, 4 }))
+ .Should().Throw();
+ }
+
+ [Fact]
+ public void ItThrowsForNullArguments()
+ {
+ FluentActions.Invoking(() => Component.FromText(null!, Text)).Should().Throw();
+ FluentActions.Invoking(() => Component.FromText(fixture.Engine, null!)).Should().Throw();
+ FluentActions.Invoking(() => Component.FromTextFile(fixture.Engine, null!)).Should().Throw();
+ }
+
+ [Fact]
+ public void ItRoundTripsThroughSerialization()
+ {
+ using var store = fixture.CreateStore();
+ using var original = Component.FromText(fixture.Engine, Text);
+
+ var serialized = original.Serialize();
+ serialized.Should().NotBeEmpty();
+
+ using var restored = Component.Deserialize(fixture.Engine, serialized);
+ using var linker = new ComponentLinker(fixture.Engine);
+
+ var add = linker.Instantiate(store, restored).GetFunction("add")!;
+
+ add.Call(ComponentValue.S32(2), ComponentValue.S32(2))!.AsS32().Should().Be(4);
+ }
+
+ [Fact]
+ public void ItLooksUpExportsOnTheComponentItself()
+ {
+ using var component = Component.FromText(fixture.Engine, Text);
+
+ using var export = component.GetExport("add");
+
+ export.Should().NotBeNull();
+ component.GetExport("nope").Should().BeNull();
+ }
+ }
+}
diff --git a/tests/ComponentValueLayoutTests.cs b/tests/ComponentValueLayoutTests.cs
new file mode 100644
index 00000000..5e63895a
--- /dev/null
+++ b/tests/ComponentValueLayoutTests.cs
@@ -0,0 +1,363 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using FluentAssertions;
+using Wasmtime.Components;
+using Xunit;
+
+namespace Wasmtime.Tests
+{
+ ///
+ /// Validates the native encoding of component values against Wasmtime itself.
+ ///
+ ///
+ ///
+ /// only proves that our writer and reader agree with each
+ /// other; it would still pass if every offset were wrong in the same way. These tests close
+ /// that gap by routing each value through wasmtime_component_val_clone, which walks
+ /// the structure using Wasmtime's own definition of the layout:
+ ///
+ ///
+ /// - we encode a value into source;
+ /// - Wasmtime deep-copies it, reading source with its layout;
+ /// - we decode destination, which Wasmtime wrote with its layout.
+ ///
+ ///
+ /// A mismatch in any size or offset therefore corrupts the value or crashes, rather than
+ /// cancelling out. This needs no engine, store or component.
+ ///
+ ///
+ public sealed class ComponentValueLayoutTests
+ {
+ private static ComponentValue Clone(ComponentValue value)
+ {
+ using var scope = new ComponentValueMarshaller.AllocationScope();
+ var source = scope.Allocate(ComponentValueMarshaller.ValueSize);
+ ComponentValueMarshaller.Write(value, source, scope);
+
+ var destination = Marshal.AllocHGlobal(ComponentValueMarshaller.ValueSize);
+ try
+ {
+ unsafe
+ {
+ new Span((void*)destination, ComponentValueMarshaller.ValueSize).Clear();
+ }
+
+ ComponentValueNative.wasmtime_component_val_clone(source, destination);
+ return ComponentValueMarshaller.Read(destination);
+ }
+ finally
+ {
+ ComponentValueNative.wasmtime_component_val_delete(destination);
+ Marshal.FreeHGlobal(destination);
+ }
+ }
+
+ ///
+ /// Clones the value through Wasmtime and asserts the result is structurally identical.
+ ///
+ private static void Survives(ComponentValue value)
+ {
+ AssertEquivalent(value, Clone(value), "$");
+ }
+
+ private static void AssertEquivalent(ComponentValue expected, ComponentValue actual, string path)
+ {
+ actual.Kind.Should().Be(expected.Kind, "kind at {0}", path);
+
+ switch (expected.Kind)
+ {
+ case ComponentValueKind.Bool:
+ actual.AsBool().Should().Be(expected.AsBool(), "value at {0}", path);
+ break;
+
+ case ComponentValueKind.S8:
+ case ComponentValueKind.U8:
+ case ComponentValueKind.S16:
+ case ComponentValueKind.U16:
+ case ComponentValueKind.S32:
+ case ComponentValueKind.U32:
+ case ComponentValueKind.S64:
+ case ComponentValueKind.U64:
+ actual.Integer.Should().Be(expected.Integer, "value at {0}", path);
+ break;
+
+ case ComponentValueKind.F32:
+ case ComponentValueKind.F64:
+ actual.Real.Should().Be(expected.Real, "value at {0}", path);
+ break;
+
+ case ComponentValueKind.String:
+ case ComponentValueKind.Enum:
+ actual.Text.Should().Be(expected.Text, "text at {0}", path);
+ break;
+
+ case ComponentValueKind.List:
+ case ComponentValueKind.Tuple:
+ actual.Items.Should().HaveCount(expected.Items.Count, "length at {0}", path);
+ for (var i = 0; i < expected.Items.Count; i++)
+ {
+ AssertEquivalent(expected.Items[i], actual.Items[i], $"{path}[{i}]");
+ }
+
+ break;
+
+ case ComponentValueKind.Record:
+ actual.Fields.Should().HaveCount(expected.Fields.Count, "field count at {0}", path);
+ for (var i = 0; i < expected.Fields.Count; i++)
+ {
+ actual.Fields[i].Key.Should().Be(expected.Fields[i].Key, "field name at {0}[{1}]", path, i);
+ AssertEquivalent(
+ expected.Fields[i].Value,
+ actual.Fields[i].Value,
+ $"{path}.{expected.Fields[i].Key}");
+ }
+
+ break;
+
+ case ComponentValueKind.Option:
+ case ComponentValueKind.Result:
+ actual.Flag.Should().Be(expected.Flag, "discriminant at {0}", path);
+ if (expected.Payload is null)
+ {
+ actual.Payload.Should().BeNull("payload at {0}", path);
+ }
+ else
+ {
+ actual.Payload.Should().NotBeNull("payload at {0}", path);
+ AssertEquivalent(expected.Payload, actual.Payload!, $"{path}.payload");
+ }
+
+ break;
+
+ default:
+ throw new NotSupportedException($"No comparison for kind {expected.Kind}.");
+ }
+ }
+
+ [Fact]
+ public void ScalarsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.Bool(true));
+ Survives(ComponentValue.Bool(false));
+ Survives(ComponentValue.S8(sbyte.MinValue));
+ Survives(ComponentValue.S8(-1));
+ Survives(ComponentValue.U8(byte.MaxValue));
+ Survives(ComponentValue.S16(short.MinValue));
+ Survives(ComponentValue.U16(ushort.MaxValue));
+ Survives(ComponentValue.S32(int.MinValue));
+ Survives(ComponentValue.S32(-1));
+ Survives(ComponentValue.U32(uint.MaxValue));
+ Survives(ComponentValue.S64(long.MinValue));
+ Survives(ComponentValue.U64(ulong.MaxValue));
+ Survives(ComponentValue.F32(float.MinValue));
+ Survives(ComponentValue.F32(float.NaN));
+ Survives(ComponentValue.F64(-2.718281828459045));
+ Survives(ComponentValue.F64(double.PositiveInfinity));
+ }
+
+ [Fact]
+ public void StringsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.String(string.Empty));
+ Survives(ComponentValue.String("hello"));
+ Survives(ComponentValue.String("h\u00e9llo \u4e16\u754c \ud83c\udf89"));
+ Survives(ComponentValue.String(new string('x', 4096)));
+ }
+
+ [Fact]
+ public void EnumsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.Enum("warning"));
+ Survives(ComponentValue.Enum("a"));
+ }
+
+ [Fact]
+ public void ListsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.List([]));
+ Survives(ComponentValue.List(new[] { ComponentValue.S32(1) }));
+ Survives(ComponentValue.List(new[]
+ {
+ ComponentValue.S32(1),
+ ComponentValue.S32(-2),
+ ComponentValue.S32(int.MaxValue),
+ }));
+ Survives(ComponentValue.List(new[]
+ {
+ ComponentValue.String("first"),
+ ComponentValue.String(string.Empty),
+ ComponentValue.String("third"),
+ }));
+ }
+
+ [Fact]
+ public void NestedListsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.List(new[]
+ {
+ ComponentValue.List(new[] { ComponentValue.String("a"), ComponentValue.String("b") }),
+ ComponentValue.List([]),
+ ComponentValue.List(new[] { ComponentValue.String("c") }),
+ }));
+ }
+
+ [Fact]
+ public void TuplesSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.Tuple(new[]
+ {
+ ComponentValue.Bool(true),
+ ComponentValue.String("two"),
+ ComponentValue.F64(3.5),
+ }));
+ }
+
+ [Fact]
+ public void RecordsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.Record([]));
+ Survives(ComponentValue.Record(new[]
+ {
+ new KeyValuePair("value", ComponentValue.F64(36.6)),
+ new KeyValuePair("timestamp", ComponentValue.U64(1234567890123)),
+ }));
+ }
+
+ [Fact]
+ public void OptionsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.None());
+ Survives(ComponentValue.Some(ComponentValue.S32(42)));
+ Survives(ComponentValue.Some(ComponentValue.String("boxed")));
+ Survives(ComponentValue.Some(ComponentValue.Some(ComponentValue.String("x"))));
+ }
+
+ [Fact]
+ public void ResultsSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.Ok());
+ Survives(ComponentValue.Err());
+ Survives(ComponentValue.Ok(ComponentValue.S32(7)));
+ Survives(ComponentValue.Err(ComponentValue.String("boom")));
+ }
+
+ [Fact]
+ public void DeeplyNestedValuesSurviveACloneThroughWasmtime()
+ {
+ Survives(ComponentValue.Record(new[]
+ {
+ new KeyValuePair("readings", ComponentValue.List(new[]
+ {
+ ComponentValue.Record(new[]
+ {
+ new KeyValuePair("value", ComponentValue.F64(1.5)),
+ new KeyValuePair("note", ComponentValue.Some(ComponentValue.String("ok"))),
+ }),
+ ComponentValue.Record(new[]
+ {
+ new KeyValuePair("value", ComponentValue.F64(2.5)),
+ new KeyValuePair("note", ComponentValue.None()),
+ }),
+ })),
+ new KeyValuePair("level", ComponentValue.Enum("warning")),
+ new KeyValuePair("outcome", ComponentValue.Ok(ComponentValue.Tuple(new[]
+ {
+ ComponentValue.U16(200),
+ ComponentValue.String("done"),
+ }))),
+ }));
+ }
+
+ ///
+ /// Host function results are written with
+ /// and then freed by Wasmtime, so every heap payload must come from Wasmtime's allocator.
+ /// Freeing memory allocated by the wrong allocator corrupts the heap, so this deletes the
+ /// value after reading it back.
+ ///
+ [Fact]
+ public void OwnedWritesUseWasmtimesAllocator()
+ {
+ var values = new[]
+ {
+ ComponentValue.S32(7),
+ ComponentValue.String("a host-allocated string"),
+ ComponentValue.String(string.Empty),
+ ComponentValue.Enum("warning"),
+ ComponentValue.List([]),
+ ComponentValue.List(new[] { ComponentValue.String("a"), ComponentValue.String("b") }),
+ ComponentValue.Tuple(new[] { ComponentValue.S32(3), ComponentValue.String("tuple") }),
+ ComponentValue.Record(new[]
+ {
+ new KeyValuePair("name", ComponentValue.String("x")),
+ new KeyValuePair("value", ComponentValue.F64(1.5)),
+ }),
+ ComponentValue.Record(new[]
+ {
+ new KeyValuePair("items", ComponentValue.List(new[]
+ {
+ ComponentValue.Some(ComponentValue.String("nested")),
+ ComponentValue.None(),
+ })),
+ new KeyValuePair("result", ComponentValue.Ok(ComponentValue.Tuple(new[]
+ {
+ ComponentValue.U16(12),
+ ComponentValue.String("done"),
+ }))),
+ }),
+ ComponentValue.Some(ComponentValue.String("boxed")),
+ ComponentValue.None(),
+ ComponentValue.Ok(ComponentValue.S32(1)),
+ ComponentValue.Err(ComponentValue.String("boom")),
+ };
+
+ foreach (var value in values)
+ {
+ var destination = Marshal.AllocHGlobal(ComponentValueMarshaller.ValueSize);
+ try
+ {
+ unsafe
+ {
+ new Span((void*)destination, ComponentValueMarshaller.ValueSize).Clear();
+ }
+
+ ComponentValueMarshaller.WriteOwned(value, destination);
+ AssertEquivalent(value, ComponentValueMarshaller.Read(destination), "$");
+ }
+ finally
+ {
+ ComponentValueNative.wasmtime_component_val_delete(destination);
+ Marshal.FreeHGlobal(destination);
+ }
+ }
+ }
+
+ ///
+ /// A large list exercises the element stride, which a single-element list cannot: an
+ /// over-estimated still round-trips one
+ /// element correctly but misaligns every element after the first.
+ ///
+ [Fact]
+ public void ElementStrideSurvivesACloneThroughWasmtime()
+ {
+ var items = new ComponentValue[64];
+ for (var i = 0; i < items.Length; i++)
+ {
+ items[i] = ComponentValue.Record(new[]
+ {
+ new KeyValuePair("index", ComponentValue.S32(i)),
+ new KeyValuePair("name", ComponentValue.String($"item-{i}")),
+ });
+ }
+
+ var result = Clone(ComponentValue.List(items)).AsList();
+
+ result.Should().HaveCount(items.Length);
+ for (var i = 0; i < items.Length; i++)
+ {
+ result[i].Field("index").AsS32().Should().Be(i);
+ result[i].Field("name").AsString().Should().Be($"item-{i}");
+ }
+ }
+ }
+}
diff --git a/tests/ComponentValueTests.cs b/tests/ComponentValueTests.cs
new file mode 100644
index 00000000..70f0cb46
--- /dev/null
+++ b/tests/ComponentValueTests.cs
@@ -0,0 +1,443 @@
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using Wasmtime.Components;
+using Xunit;
+
+namespace Wasmtime.Tests
+{
+ ///
+ /// Round-trip tests for the component value marshaller.
+ ///
+ ///
+ /// These verify that the writer and the reader agree on the native encoding. They do not
+ /// prove that the encoding matches Wasmtime's, which is fixed by the struct sizes and
+ /// offsets taken from wasmtime/component/val.h and is exercised end to end once a
+ /// component function is actually called.
+ ///
+ public sealed class ComponentValueTests
+ {
+ private static ComponentValue RoundTrip(ComponentValue value)
+ {
+ using var scope = new ComponentValueMarshaller.AllocationScope();
+ var pointer = scope.Allocate(ComponentValueMarshaller.ValueSize);
+ ComponentValueMarshaller.Write(value, pointer, scope);
+ return ComponentValueMarshaller.Read(pointer);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ItRoundTripsBool(bool value)
+ {
+ var result = RoundTrip(ComponentValue.Bool(value));
+
+ result.Kind.Should().Be(ComponentValueKind.Bool);
+ result.AsBool().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(sbyte.MinValue)]
+ [InlineData((sbyte)-1)]
+ [InlineData((sbyte)0)]
+ [InlineData(sbyte.MaxValue)]
+ public void ItRoundTripsS8(sbyte value)
+ {
+ RoundTrip(ComponentValue.S8(value)).AsS8().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(byte.MinValue)]
+ [InlineData((byte)1)]
+ [InlineData(byte.MaxValue)]
+ public void ItRoundTripsU8(byte value)
+ {
+ RoundTrip(ComponentValue.U8(value)).AsU8().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(short.MinValue)]
+ [InlineData((short)-1)]
+ [InlineData(short.MaxValue)]
+ public void ItRoundTripsS16(short value)
+ {
+ RoundTrip(ComponentValue.S16(value)).AsS16().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(ushort.MinValue)]
+ [InlineData(ushort.MaxValue)]
+ public void ItRoundTripsU16(ushort value)
+ {
+ RoundTrip(ComponentValue.U16(value)).AsU16().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(int.MinValue)]
+ [InlineData(-1)]
+ [InlineData(0)]
+ [InlineData(int.MaxValue)]
+ public void ItRoundTripsS32(int value)
+ {
+ RoundTrip(ComponentValue.S32(value)).AsS32().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(uint.MinValue)]
+ [InlineData(1u)]
+ [InlineData(uint.MaxValue)]
+ public void ItRoundTripsU32(uint value)
+ {
+ RoundTrip(ComponentValue.U32(value)).AsU32().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(long.MinValue)]
+ [InlineData(-1L)]
+ [InlineData(long.MaxValue)]
+ public void ItRoundTripsS64(long value)
+ {
+ RoundTrip(ComponentValue.S64(value)).AsS64().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(ulong.MinValue)]
+ [InlineData(1ul)]
+ [InlineData(ulong.MaxValue)]
+ public void ItRoundTripsU64(ulong value)
+ {
+ RoundTrip(ComponentValue.U64(value)).AsU64().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData(0.0f)]
+ [InlineData(3.14159f)]
+ [InlineData(float.MinValue)]
+ [InlineData(float.MaxValue)]
+ [InlineData(float.Epsilon)]
+ [InlineData(float.NaN)]
+ [InlineData(float.PositiveInfinity)]
+ [InlineData(float.NegativeInfinity)]
+ public void ItRoundTripsF32(float value)
+ {
+ RoundTrip(ComponentValue.F32(value)).AsF32().Should().Be(value);
+ }
+
+ ///
+ /// -0.0 == 0.0, so the sign of zero is only observable through the bits.
+ ///
+ [Fact]
+ public void ItPreservesTheSignOfZero()
+ {
+ var f32 = RoundTrip(ComponentValue.F32(-0.0f)).AsF32();
+ BitConverter.SingleToInt32Bits(f32).Should().Be(BitConverter.SingleToInt32Bits(-0.0f));
+
+ var f64 = RoundTrip(ComponentValue.F64(-0.0)).AsF64();
+ BitConverter.DoubleToInt64Bits(f64).Should().Be(BitConverter.DoubleToInt64Bits(-0.0));
+ }
+
+ [Theory]
+ [InlineData(0.0)]
+ [InlineData(-2.718281828459045)]
+ [InlineData(double.MinValue)]
+ [InlineData(double.MaxValue)]
+ [InlineData(double.Epsilon)]
+ [InlineData(double.NaN)]
+ [InlineData(double.PositiveInfinity)]
+ [InlineData(double.NegativeInfinity)]
+ public void ItRoundTripsF64(double value)
+ {
+ RoundTrip(ComponentValue.F64(value)).AsF64().Should().Be(value);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("hello")]
+ [InlineData("h\u00e9llo \u4e16\u754c \ud83c\udf89")]
+ [InlineData("embedded\0null")]
+ public void ItRoundTripsString(string value)
+ {
+ var result = RoundTrip(ComponentValue.String(value));
+
+ result.Kind.Should().Be(ComponentValueKind.String);
+ result.AsString().Should().Be(value);
+ }
+
+ [Fact]
+ public void ItRoundTripsEnum()
+ {
+ var result = RoundTrip(ComponentValue.Enum("warning"));
+
+ result.Kind.Should().Be(ComponentValueKind.Enum);
+ result.AsEnum().Should().Be("warning");
+ }
+
+ [Fact]
+ public void ItRoundTripsEmptyList()
+ {
+ RoundTrip(ComponentValue.List([])).AsList().Should().BeEmpty();
+ }
+
+ [Fact]
+ public void ItRoundTripsList()
+ {
+ var value = ComponentValue.List(new[]
+ {
+ ComponentValue.S32(1),
+ ComponentValue.S32(-2),
+ ComponentValue.S32(int.MaxValue),
+ });
+
+ var result = RoundTrip(value).AsList();
+
+ result.Should().HaveCount(3);
+ result[0].AsS32().Should().Be(1);
+ result[1].AsS32().Should().Be(-2);
+ result[2].AsS32().Should().Be(int.MaxValue);
+ }
+
+ [Fact]
+ public void ItRoundTripsNestedLists()
+ {
+ var value = ComponentValue.List(new[]
+ {
+ ComponentValue.List(new[] { ComponentValue.String("a"), ComponentValue.String("b") }),
+ ComponentValue.List([]),
+ ComponentValue.List(new[] { ComponentValue.String("c") }),
+ });
+
+ var result = RoundTrip(value).AsList();
+
+ result.Should().HaveCount(3);
+ result[0].AsList().Should().HaveCount(2);
+ result[0].AsList()[1].AsString().Should().Be("b");
+ result[1].AsList().Should().BeEmpty();
+ result[2].AsList()[0].AsString().Should().Be("c");
+ }
+
+ [Fact]
+ public void ItRoundTripsTupleOfMixedKinds()
+ {
+ var value = ComponentValue.Tuple(new[]
+ {
+ ComponentValue.Bool(true),
+ ComponentValue.String("two"),
+ ComponentValue.F64(3.5),
+ });
+
+ var result = RoundTrip(value);
+
+ result.Kind.Should().Be(ComponentValueKind.Tuple);
+ result.AsTuple()[0].AsBool().Should().BeTrue();
+ result.AsTuple()[1].AsString().Should().Be("two");
+ result.AsTuple()[2].AsF64().Should().Be(3.5);
+ }
+
+ [Fact]
+ public void ItRoundTripsRecordPreservingFieldOrder()
+ {
+ var value = ComponentValue.Record(new[]
+ {
+ new KeyValuePair("value", ComponentValue.F64(36.6)),
+ new KeyValuePair("timestamp", ComponentValue.U64(1234567890123)),
+ });
+
+ var result = RoundTrip(value);
+
+ result.Kind.Should().Be(ComponentValueKind.Record);
+ result.AsRecord().Should().HaveCount(2);
+ result.AsRecord()[0].Key.Should().Be("value");
+ result.AsRecord()[1].Key.Should().Be("timestamp");
+ result.Field("value").AsF64().Should().Be(36.6);
+ result.Field("timestamp").AsU64().Should().Be(1234567890123);
+ }
+
+ [Fact]
+ public void ItRoundTripsEmptyRecord()
+ {
+ RoundTrip(ComponentValue.Record([]))
+ .AsRecord().Should().BeEmpty();
+ }
+
+ [Fact]
+ public void ItRoundTripsSomeAndNone()
+ {
+ var some = RoundTrip(ComponentValue.Some(ComponentValue.S32(42)));
+ some.Kind.Should().Be(ComponentValueKind.Option);
+ some.IsSome.Should().BeTrue();
+ some.Payload!.AsS32().Should().Be(42);
+
+ var none = RoundTrip(ComponentValue.None());
+ none.Kind.Should().Be(ComponentValueKind.Option);
+ none.IsSome.Should().BeFalse();
+ none.Payload.Should().BeNull();
+ }
+
+ [Fact]
+ public void ItRoundTripsNestedOption()
+ {
+ var result = RoundTrip(ComponentValue.Some(ComponentValue.Some(ComponentValue.String("x"))));
+
+ result.IsSome.Should().BeTrue();
+ result.Payload!.IsSome.Should().BeTrue();
+ result.Payload!.Payload!.AsString().Should().Be("x");
+ }
+
+ [Fact]
+ public void ItRoundTripsResultWithPayload()
+ {
+ var ok = RoundTrip(ComponentValue.Ok(ComponentValue.S32(7)));
+ ok.Kind.Should().Be(ComponentValueKind.Result);
+ ok.IsOk.Should().BeTrue();
+ ok.Payload!.AsS32().Should().Be(7);
+
+ var err = RoundTrip(ComponentValue.Err(ComponentValue.String("boom")));
+ err.IsOk.Should().BeFalse();
+ err.Payload!.AsString().Should().Be("boom");
+ }
+
+ [Fact]
+ public void ItRoundTripsResultWithoutPayload()
+ {
+ var ok = RoundTrip(ComponentValue.Ok());
+ ok.IsOk.Should().BeTrue();
+ ok.Payload.Should().BeNull();
+
+ var err = RoundTrip(ComponentValue.Err());
+ err.IsOk.Should().BeFalse();
+ err.Payload.Should().BeNull();
+ }
+
+ [Fact]
+ public void ItRoundTripsDeeplyNestedValues()
+ {
+ var value = ComponentValue.Record(new[]
+ {
+ new KeyValuePair("readings", ComponentValue.List(new[]
+ {
+ ComponentValue.Record(new[]
+ {
+ new KeyValuePair("value", ComponentValue.F64(1.5)),
+ new KeyValuePair("note", ComponentValue.Some(ComponentValue.String("ok"))),
+ }),
+ ComponentValue.Record(new[]
+ {
+ new KeyValuePair("value", ComponentValue.F64(2.5)),
+ new KeyValuePair("note", ComponentValue.None()),
+ }),
+ })),
+ new KeyValuePair("level", ComponentValue.Enum("warning")),
+ });
+
+ var result = RoundTrip(value);
+ var readings = result.Field("readings").AsList();
+
+ readings.Should().HaveCount(2);
+ readings[0].Field("value").AsF64().Should().Be(1.5);
+ readings[0].Field("note").Payload!.AsString().Should().Be("ok");
+ readings[1].Field("value").AsF64().Should().Be(2.5);
+ readings[1].Field("note").IsSome.Should().BeFalse();
+ result.Field("level").AsEnum().Should().Be("warning");
+ }
+
+ [Fact]
+ public void ItThrowsReadingAnUnsupportedKind()
+ {
+ using var scope = new ComponentValueMarshaller.AllocationScope();
+ var pointer = scope.Allocate(ComponentValueMarshaller.ValueSize);
+ System.Runtime.InteropServices.Marshal.WriteByte(pointer, (byte)ComponentValueKind.Char);
+
+ var act = () => ComponentValueMarshaller.Read(pointer);
+
+ act.Should().Throw().WithMessage("*Char*");
+ }
+
+ ///
+ /// A native length is a size_t, so it can exceed what a managed length can hold.
+ /// Truncating it would produce a negative or far-too-small length and read out of bounds,
+ /// so it has to be rejected instead.
+ ///
+ [Theory]
+ [InlineData(ComponentValueKind.String)]
+ [InlineData(ComponentValueKind.List)]
+ [InlineData(ComponentValueKind.Record)]
+ public void ItRejectsALengthTooLargeToRepresent(ComponentValueKind kind)
+ {
+ using var scope = new ComponentValueMarshaller.AllocationScope();
+ var pointer = scope.Allocate(ComponentValueMarshaller.ValueSize);
+
+ System.Runtime.InteropServices.Marshal.WriteByte(pointer, (byte)kind);
+
+ // All bits set reads back as a huge unsigned size on both 32- and 64-bit.
+ System.Runtime.InteropServices.Marshal.WriteIntPtr(
+ pointer + ComponentValueMarshaller.ValuePayloadOffset, new IntPtr(-1));
+
+ // Non-null so the guard, rather than a null check, is what rejects this.
+ System.Runtime.InteropServices.Marshal.WriteIntPtr(
+ pointer + ComponentValueMarshaller.ValuePayloadOffset + 8, pointer);
+
+ var act = () => ComponentValueMarshaller.Read(pointer);
+
+ act.Should().Throw()
+ .WithMessage("*exceeds the maximum supported length*");
+ }
+
+ [Fact]
+ public void ItThrowsAccessingTheWrongKind()
+ {
+ var value = ComponentValue.S32(1);
+
+ value.Invoking(v => v.AsString()).Should().Throw();
+ value.Invoking(v => v.AsRecord()).Should().Throw();
+ value.Invoking(v => v.IsOk).Should().Throw();
+ }
+
+ [Fact]
+ public void ItCopiesTheCallersCollections()
+ {
+ var items = new List { ComponentValue.S32(1) };
+ var fields = new List> { new("a", ComponentValue.S32(1)) };
+
+ var list = ComponentValue.List(items);
+ var tuple = ComponentValue.Tuple(items);
+ var record = ComponentValue.Record(fields);
+
+ items.Add(ComponentValue.S32(2));
+ fields.Add(new("b", ComponentValue.S32(2)));
+
+ list.AsList().Should().ContainSingle();
+ tuple.AsTuple().Should().ContainSingle();
+ record.AsRecord().Should().ContainSingle();
+ }
+
+ [Fact]
+ public void ItThrowsForAMissingRecordField()
+ {
+ var value = ComponentValue.Record(new[]
+ {
+ new KeyValuePair("present", ComponentValue.S32(1)),
+ });
+
+ value.TryGetField("present", out var found).Should().BeTrue();
+ found!.AsS32().Should().Be(1);
+
+ value.TryGetField("absent", out var missing).Should().BeFalse();
+ missing.Should().BeNull();
+
+ value.Invoking(v => v.Field("absent"))
+ .Should().Throw()
+ .WithMessage("*absent*");
+ }
+
+ [Fact]
+ public void ItRejectsNullArguments()
+ {
+ FluentActions.Invoking(() => ComponentValue.String(null!)).Should().Throw();
+ FluentActions.Invoking(() => ComponentValue.Enum(null!)).Should().Throw();
+ FluentActions.Invoking(() => ComponentValue.List(null!)).Should().Throw();
+ FluentActions.Invoking(() => ComponentValue.Tuple(null!)).Should().Throw();
+ FluentActions.Invoking(() => ComponentValue.Record(null!)).Should().Throw();
+ FluentActions.Invoking(() => ComponentValue.Some(null!)).Should().Throw();
+ }
+ }
+}
diff --git a/tests/Components/host-import-declarations.wat b/tests/Components/host-import-declarations.wat
new file mode 100644
index 00000000..0a959e60
--- /dev/null
+++ b/tests/Components/host-import-declarations.wat
@@ -0,0 +1,6 @@
+;; Component importing host functions without invoking them.
+(component
+ (import "host" (instance $h
+ (export "transform" (func (param "x" s32) (result s32)))
+ (export "greet" (func (param "name" string) (result string)))))
+)
diff --git a/tests/Components/host-import.wat b/tests/Components/host-import.wat
new file mode 100644
index 00000000..cd222dcd
--- /dev/null
+++ b/tests/Components/host-import.wat
@@ -0,0 +1,30 @@
+;; Component importing a "host" instance, used to test host function definitions.
+;;
+;; The core start function calls "transform" during instantiation, exercising both callback
+;; argument and result handling. "greet" is only imported, so it must still be defined too.
+(component
+ (import "host" (instance $h
+ (export "transform" (func (param "x" s32) (result s32)))
+ (export "greet" (func (param "name" string) (result string)))))
+ (alias export $h "transform" (func $transform))
+ (core func $transform_core (canon lower (func $transform)))
+ (core module $m
+ (import "host" "transform" (func $transform (param i32) (result i32)))
+ (func (export "run") (param i32) (result i32)
+ local.get 0
+ call $transform)
+ (func $start
+ i32.const 21
+ call $transform
+ i32.const 42
+ i32.ne
+ if
+ unreachable
+ end)
+ (start $start)
+ )
+ (core instance $i (instantiate $m
+ (with "host" (instance (export "transform" (func $transform_core))))))
+ (func (export "run") (param "x" s32) (result s32)
+ (canon lift (core func $i "run")))
+)
diff --git a/tests/Components/strings.wat b/tests/Components/strings.wat
new file mode 100644
index 00000000..28a2d851
--- /dev/null
+++ b/tests/Components/strings.wat
@@ -0,0 +1,75 @@
+;; Component exercising heap-carrying values across the guest boundary: a string parameter,
+;; a string result and a list result. Lifting these requires the core module to export a
+;; memory and a realloc, and to return results indirectly through a return area.
+;;
+;; The allocator is a bump allocator that never frees, which is all these tests need.
+(component
+ (core module $m
+ (memory (export "memory") 1)
+ (global $bump (mut i32) (i32.const 1024))
+
+ (func $alloc (param $n i32) (result i32)
+ (local $r i32)
+ (local.set $r (global.get $bump))
+ (global.set $bump
+ (i32.and
+ (i32.add (i32.add (global.get $bump) (local.get $n)) (i32.const 7))
+ (i32.const -8)))
+ (local.get $r))
+
+ (func (export "realloc") (param $old i32) (param $oldsz i32) (param $align i32) (param $newsz i32) (result i32)
+ (call $alloc (local.get $newsz)))
+
+ ;; greet(name) -> "hi " ++ name
+ (func (export "greet") (param $ptr i32) (param $len i32) (result i32)
+ (local $out i32)
+ (local $ret i32)
+ (local.set $out (call $alloc (i32.add (i32.const 3) (local.get $len))))
+ (i32.store8 (local.get $out) (i32.const 104))
+ (i32.store8 offset=1 (local.get $out) (i32.const 105))
+ (i32.store8 offset=2 (local.get $out) (i32.const 32))
+ (memory.copy
+ (i32.add (local.get $out) (i32.const 3))
+ (local.get $ptr)
+ (local.get $len))
+ (local.set $ret (call $alloc (i32.const 8)))
+ (i32.store (local.get $ret) (local.get $out))
+ (i32.store offset=4 (local.get $ret) (i32.add (i32.const 3) (local.get $len)))
+ (local.get $ret))
+
+ ;; length(name) -> byte length of the string as seen by the guest
+ (func (export "length") (param $ptr i32) (param $len i32) (result i32)
+ (local.get $len))
+
+ ;; numbers() -> [10, 20, 30]
+ (func (export "numbers") (result i32)
+ (local $data i32)
+ (local $ret i32)
+ (local.set $data (call $alloc (i32.const 12)))
+ (i32.store (local.get $data) (i32.const 10))
+ (i32.store offset=4 (local.get $data) (i32.const 20))
+ (i32.store offset=8 (local.get $data) (i32.const 30))
+ (local.set $ret (call $alloc (i32.const 8)))
+ (i32.store (local.get $ret) (local.get $data))
+ (i32.store offset=4 (local.get $ret) (i32.const 3))
+ (local.get $ret))
+ )
+ (core instance $i (instantiate $m))
+
+ (func (export "greet") (param "name" string) (result string)
+ (canon lift (core func $i "greet")
+ (memory $i "memory")
+ (realloc (func $i "realloc"))
+ string-encoding=utf8))
+
+ (func (export "length") (param "name" string) (result s32)
+ (canon lift (core func $i "length")
+ (memory $i "memory")
+ (realloc (func $i "realloc"))
+ string-encoding=utf8))
+
+ (func (export "numbers") (result (list s32))
+ (canon lift (core func $i "numbers")
+ (memory $i "memory")
+ (realloc (func $i "realloc"))))
+)
diff --git a/tests/Components/tiny.wat b/tests/Components/tiny.wat
new file mode 100644
index 00000000..ebcb512c
--- /dev/null
+++ b/tests/Components/tiny.wat
@@ -0,0 +1,12 @@
+;; Minimal component with no imports, used to test instantiation and calling.
+(component
+ (core module $m
+ (func (export "add") (param i32 i32) (result i32)
+ local.get 0
+ local.get 1
+ i32.add)
+ )
+ (core instance $i (instantiate $m))
+ (func (export "add") (param "a" s32) (param "b" s32) (result s32)
+ (canon lift (core func $i "add")))
+)
diff --git a/tests/Components/trap.wasm b/tests/Components/trap.wasm
new file mode 100644
index 00000000..695ea598
Binary files /dev/null and b/tests/Components/trap.wasm differ
diff --git a/tests/Components/trap.wat b/tests/Components/trap.wat
new file mode 100644
index 00000000..a2c9d271
--- /dev/null
+++ b/tests/Components/trap.wat
@@ -0,0 +1,10 @@
+;; Component whose exported function always traps, used to test trap behaviour.
+(component
+ (core module $m
+ (func (export "boom") (result i32)
+ unreachable)
+ )
+ (core instance $i (instantiate $m))
+ (func (export "boom") (result s32)
+ (canon lift (core func $i "boom")))
+)
diff --git a/tests/Wasmtime.Tests.csproj b/tests/Wasmtime.Tests.csproj
index a13c361c..5ffe0d90 100644
--- a/tests/Wasmtime.Tests.csproj
+++ b/tests/Wasmtime.Tests.csproj
@@ -22,6 +22,7 @@
+