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/src/Components/Component.cs b/src/Components/Component.cs
new file mode 100644
index 00000000..1dfcd573
--- /dev/null
+++ b/src/Components/Component.cs
@@ -0,0 +1,189 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Runtime.InteropServices;
+
+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);
+ }
+ }
+ }
+
+ ///
+ /// 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);
+ }
+
+ public ComponentExport? GetExport(string name)
+ {
+ var ret = Native.wasmtime_component_get_export_index(NativeHandle, null, name, (nuint)name.Length);
+ if (ret == IntPtr.Zero)
+ return null;
+
+ return new ComponentExport(ret);
+ }
+
+ public ComponentExport? GetExport(string name, ComponentExport instance_export_index)
+ {
+ var ret = Native.wasmtime_component_get_export_index(NativeHandle, instance_export_index.NativeHandle, name, (nuint)name.Length);
+ if (ret == IntPtr.Zero)
+ return null;
+
+ return new ComponentExport(ret);
+ }
+
+ 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 IntPtr wasmtime_component_get_export_index(Handle component, ComponentExport.Handle? instance_export_index, string name, nuint name_len);
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentExport.cs b/src/Components/ComponentExport.cs
new file mode 100644
index 00000000..4fb03259
--- /dev/null
+++ b/src/Components/ComponentExport.cs
@@ -0,0 +1,57 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Wasmtime.Components;
+
+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..7e390e0d
--- /dev/null
+++ b/src/Components/ComponentFunction.cs
@@ -0,0 +1,19 @@
+namespace Wasmtime.Components;
+
+///
+/// Represents a Wasmtime function.
+///
+public class ComponentFunction
+{
+ //todo: everything!
+
+
+ internal static class Native
+ {
+ // [DllImport(Engine.LibraryName)]
+ //todo: wasmtime_error_t * wasmtime_component_func_call (const wasmtime_component_func_t *func, wasmtime_context_t *context, const wasmtime_component_val_t *args, size_t args_size, wasmtime_component_val_t *results, size_t results_size)
+
+ // [DllImport(Engine.LibraryName)]
+ //todo: wasmtime_error_t * wasmtime_component_func_post_return (const wasmtime_component_func_t *func, wasmtime_context_t *context)
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentInstance.cs b/src/Components/ComponentInstance.cs
new file mode 100644
index 00000000..00f19632
--- /dev/null
+++ b/src/Components/ComponentInstance.cs
@@ -0,0 +1,16 @@
+namespace Wasmtime.Components;
+
+public class ComponentInstance
+{
+ //todo: everything!
+
+
+ internal static class Native
+ {
+ //[DllImport(Engine.LibraryName)]
+ //public static extern IntPtr /* wasmtime_component_export_index_t* */ wasmtime_component_instance_get_export_index (wasmtime_component_instance_t *instance, wasmtime_context_t *context, ComponentExport.Handle instance_export_index, string name, nuint name_len)
+
+ // [DllImport(Engine.LibraryName)]
+ //todo: bool wasmtime_component_instance_get_func (const wasmtime_component_instance_t *instance, wasmtime_context_t *context, const wasmtime_component_export_index_t *export_index, wasmtime_component_func_t *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..7f3047e3
--- /dev/null
+++ b/src/Components/ComponentLinker.cs
@@ -0,0 +1,75 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Wasmtime.Components;
+
+public class ComponentLinker
+ : IDisposable
+{
+ private readonly Handle handle;
+
+ internal Handle NativeHandle
+ {
+ get
+ {
+ if (handle.IsInvalid || handle.IsClosed)
+ {
+ throw new ObjectDisposedException(typeof(Module).FullName);
+ }
+
+ return handle;
+ }
+ }
+
+ internal ComponentLinker(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_linker_delete(handle);
+ return true;
+ }
+ }
+
+ internal static class Native
+ {
+ // [DllImport(Engine.LibraryName)]
+ //todo: wasmtime_component_linker_t * wasmtime_component_linker_new (const wasm_engine_t *engine)
+
+ // [DllImport(Engine.LibraryName)]
+ //todo: wasmtime_component_linker_instance_t * wasmtime_component_linker_root (wasmtime_component_linker_t *linker)
+
+ // [DllImport(Engine.LibraryName)]
+ //todo: wasmtime_error_t * wasmtime_component_linker_instantiate (const wasmtime_component_linker_t *linker, wasmtime_context_t *context, const wasmtime_component_t *component, wasmtime_component_instance_t *instance_out)
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_linker_delete(IntPtr /* wasmtime_component_linker_t* */ linker);
+
+ //todo: wasmtime_error_t * wasmtime_component_linker_instance_add_instance (wasmtime_component_linker_instance_t *linker_instance, const char *name, size_t name_len, wasmtime_component_linker_instance_t **linker_instance_out)
+ //todo: wasmtime_error_t* wasmtime_component_linker_instance_add_module(wasmtime_component_linker_instance_t* linker_instance, const char* name, size_t name_len, const wasmtime_module_t* module)
+ //todo: wasmtime_error_t * wasmtime_component_linker_instance_add_func (wasmtime_component_linker_instance_t *linker_instance, const char *name, size_t name_len, wasmtime_component_func_callback_t callback, void *data, void(*finalizer)(void *))
+ //todo: wasmtime_error_t * wasmtime_component_linker_add_wasip2 (wasmtime_component_linker_t *linker)
+
+ [DllImport(Engine.LibraryName)]
+ public static extern void wasmtime_component_linker_instance_delete(IntPtr /* wasmtime_component_linker_instance_t* */ linker_instance);
+
+ }
+}
\ No newline at end of file
diff --git a/src/Components/ComponentValue.cs b/src/Components/ComponentValue.cs
new file mode 100644
index 00000000..3b4eee7f
--- /dev/null
+++ b/src/Components/ComponentValue.cs
@@ -0,0 +1,484 @@
+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
+{
+ ///
+ /// 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..705a8e40
--- /dev/null
+++ b/src/Components/ComponentValueMarshaller.cs
@@ -0,0 +1,372 @@
+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.
+///
+///
+/// The native structures are written field by field rather than through the
+/// wasmtime_component_vallist_new family of helpers, because a value must be built in
+/// place inside an array element or a record entry.
+///
+///
+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;
+
+ ~AllocationScope()
+ {
+ FreeAllocations();
+ }
+
+ ///
+ /// 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();
+ GC.SuppressFinalize(this);
+ }
+
+ 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)
+ {
+ Marshal.WriteByte(destination, (byte)value.Kind);
+ 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;
+
+ 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.");
+ }
+ }
+
+ ///
+ /// 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 bytes = Encoding.UTF8.GetBytes(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(bytes.Length, 1));
+ Marshal.Copy(bytes, 0, buffer, bytes.Length);
+
+ Marshal.WriteIntPtr(destination, (IntPtr)bytes.Length);
+ 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/ComponentValueLayoutTests.cs b/tests/ComponentValueLayoutTests.cs
new file mode 100644
index 00000000..c77ae501
--- /dev/null
+++ b/tests/ComponentValueLayoutTests.cs
@@ -0,0 +1,300 @@
+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"),
+ }))),
+ }));
+ }
+
+ ///
+ /// 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();
+ }
+ }
+}