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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 101 additions & 6 deletions src/Components/ComponentValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public enum ComponentValueKind : byte
F32 = 9,
/// <summary>A 64-bit IEEE-754 float.</summary>
F64 = 10,
/// <summary>A Unicode scalar value. Not yet supported by this binding.</summary>
/// <summary>A Unicode scalar value.</summary>
Char = 11,
/// <summary>A UTF-8 string.</summary>
String = 12,
Expand All @@ -43,15 +43,15 @@ public enum ComponentValueKind : byte
Record = 14,
/// <summary>A tuple of positional values.</summary>
Tuple = 15,
/// <summary>A variant. Not yet supported by this binding.</summary>
/// <summary>A named variant case with an optional payload.</summary>
Variant = 16,
/// <summary>An enumeration, identified by case name.</summary>
Enum = 17,
/// <summary>An optional value.</summary>
Option = 18,
/// <summary>A result value.</summary>
Result = 19,
/// <summary>A set of flags. Not yet supported by this binding.</summary>
/// <summary>A set of flag names.</summary>
Flags = 20,
/// <summary>A resource handle. Not yet supported by this binding.</summary>
Resource = 21,
Expand All @@ -66,8 +66,7 @@ public enum ComponentValueKind : byte
/// <remarks>
/// 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 <see cref="Kind"/>. The
/// <see cref="ComponentValueKind.Char"/>, <see cref="ComponentValueKind.Variant"/>,
/// <see cref="ComponentValueKind.Flags"/>, <see cref="ComponentValueKind.Resource"/> and
/// <see cref="ComponentValueKind.Resource"/> and
/// <see cref="ComponentValueKind.Map"/> kinds are not yet supported.
/// </remarks>
public sealed class ComponentValue
Expand All @@ -94,6 +93,8 @@ private ComponentValue(ComponentValueKind kind)
/// <summary>Elements of a list or tuple.</summary>
internal IReadOnlyList<ComponentValue> Items { get; private set; } = Array.Empty<ComponentValue>();

internal IReadOnlyList<string> Names { get; private set; } = Array.Empty<string>();

/// <summary>Fields of a record, in declaration order.</summary>
internal IReadOnlyList<KeyValuePair<string, ComponentValue>> Fields { get; private set; } =
Array.Empty<KeyValuePair<string, ComponentValue>>();
Expand All @@ -103,7 +104,8 @@ private ComponentValue(ComponentValueKind kind)

/// <summary>
/// Gets the payload of an <see cref="ComponentValueKind.Option"/> or
/// <see cref="ComponentValueKind.Result"/>, or <c>null</c> when there is none.
/// <see cref="ComponentValueKind.Result"/> or <see cref="ComponentValueKind.Variant"/>,
/// or <c>null</c> when there is none.
/// </summary>
public ComponentValue? Payload { get; private set; }

Expand Down Expand Up @@ -183,6 +185,72 @@ public static ComponentValue F32(float value) =>
public static ComponentValue F64(double value) =>
new ComponentValue(ComponentValueKind.F64) { Real = value };

/// <summary>Creates a Unicode scalar value, not a UTF-16 code unit.</summary>
/// <param name="value">A value from 0 to 0x10FFFF, excluding surrogates.</param>
/// <returns>The component value.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown for an invalid scalar.</exception>
public static ComponentValue Char(uint value)
Comment thread
martindevans marked this conversation as resolved.
{
if (value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF))
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Expected a Unicode scalar value.");
}

return new ComponentValue(ComponentValueKind.Char) { Integer = value };
}

/// <summary>Creates a Unicode scalar value from a UTF-16 code unit.</summary>
/// <param name="value">A UTF-16 code unit that is not a surrogate.</param>
/// <returns>The component value.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown for a surrogate code unit.</exception>
public static ComponentValue Char(char value) => Char((uint)value);

#if NETCOREAPP3_0_OR_GREATER
/// <summary>Creates a Unicode scalar value.</summary>
/// <param name="value">The value.</param>
/// <returns>The component value.</returns>
public static ComponentValue Char(System.Text.Rune value) =>
new ComponentValue(ComponentValueKind.Char) { Integer = value.Value };
#endif

/// <summary>Creates a named variant case.</summary>
/// <param name="caseName">The WIT case name.</param>
/// <param name="value">The optional payload.</param>
/// <returns>The component value.</returns>
public static ComponentValue Variant(string caseName, ComponentValue? value = null)
{
if (caseName is null)
{
throw new ArgumentNullException(nameof(caseName));
}

return new ComponentValue(ComponentValueKind.Variant) { Text = caseName, Payload = value };
}

/// <summary>Creates a set of flag names, copying the input.</summary>
/// <param name="names">The enabled WIT flag names, with no nulls or duplicates.</param>
/// <returns>The component value.</returns>
/// <exception cref="ArgumentException">Thrown for a null or duplicate name.</exception>
public static ComponentValue Flags(IReadOnlyList<string> names)
{
if (names is null)
{
throw new ArgumentNullException(nameof(names));
}

var copy = Copy(names);
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var name in copy)
{
if (name is null || !seen.Add(name))
{
throw new ArgumentException("Flag names must not be null or duplicated.", nameof(names));
}
}

return new ComponentValue(ComponentValueKind.Flags) { Names = Array.AsReadOnly(copy) };
}

/// <summary>Creates a string value.</summary>
/// <param name="value">The value.</param>
/// <returns>The component value.</returns>
Expand Down Expand Up @@ -355,6 +423,24 @@ public static ComponentValue Err(ComponentValue? value = null) =>
/// <returns>The value.</returns>
public double AsF64() => Expect(ComponentValueKind.F64, Real);

/// <summary>Gets the Unicode scalar value.</summary>
/// <returns>The scalar, including supplementary-plane values.</returns>
public uint AsChar() => Expect(ComponentValueKind.Char, (uint)Integer);

#if NETCOREAPP3_0_OR_GREATER
/// <summary>Gets the Unicode scalar value.</summary>
/// <returns>The scalar.</returns>
public System.Text.Rune AsRune() => new System.Text.Rune(AsChar());
#endif

/// <summary>Gets the WIT variant case name.</summary>
/// <returns>The case name.</returns>
public string AsVariant() => Expect(ComponentValueKind.Variant, Text!);

/// <summary>Gets the enabled WIT flag names.</summary>
/// <returns>The read-only set of names in input order.</returns>
public IReadOnlyList<string> AsFlags() => Expect(ComponentValueKind.Flags, Names);

/// <summary>Gets the value as a string.</summary>
/// <returns>The value.</returns>
public string AsString() => Expect(ComponentValueKind.String, Text ?? string.Empty);
Expand Down Expand Up @@ -440,6 +526,12 @@ public override string ToString()
return unchecked((ulong)Integer).ToString(CultureInfo.InvariantCulture);
case ComponentValueKind.String:
return $"\"{Text}\"";
case ComponentValueKind.Char:
return char.ConvertFromUtf32((int)Integer);
case ComponentValueKind.Variant:
return Payload is null ? Text! : $"{Text}({Payload})";
case ComponentValueKind.Flags:
return $"{{{string.Join(", ", Names)}}}";
case ComponentValueKind.Enum:
return Text ?? string.Empty;
case ComponentValueKind.List:
Expand Down Expand Up @@ -477,6 +569,9 @@ internal static class ComponentValueNative
[DllImport(Engine.LibraryName)]
public static extern void wasmtime_component_valtuple_new_uninit(IntPtr value, nuint size);

[DllImport(Engine.LibraryName)]
public static extern void wasmtime_component_valflags_new_uninit(IntPtr value, nuint size);

[DllImport(Engine.LibraryName)]
public static extern IntPtr wasmtime_component_val_new(IntPtr value);

Expand Down
61 changes: 61 additions & 0 deletions src/Components/ComponentValueMarshaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal static class ComponentValueMarshaller

/// <summary>Offset of the <c>data</c> pointer within a vec or a <c>wasm_name_t</c>.</summary>
private const int VectorDataOffset = 8;
private const int NameSize = 16;

/// <summary>
/// Tracks the native allocations made while writing arguments, so they can be released
Expand Down Expand Up @@ -127,6 +128,22 @@ public static void Write(ComponentValue value, IntPtr destination, AllocationSco
WriteRecord(value.Fields, payload, scope);
break;

case ComponentValueKind.Variant:
WriteName(value.Text!, payload, scope);
Marshal.WriteIntPtr(payload + NameSize,
value.Payload is null ? IntPtr.Zero : WriteBoxed(value.Payload, scope));
break;

case ComponentValueKind.Flags:
var names = scope.Allocate(Math.Max(checked(value.Names.Count * NameSize), 1));
for (var i = 0; i < value.Names.Count; i++)
{
WriteName(value.Names[i], names + i * NameSize, scope);
}
Marshal.WriteIntPtr(payload, (IntPtr)value.Names.Count);
Marshal.WriteIntPtr(payload + VectorDataOffset, names);
break;

case ComponentValueKind.Option:
Marshal.WriteIntPtr(payload, value.Flag ? WriteBoxed(value.Payload!, scope) : IntPtr.Zero);
break;
Expand Down Expand Up @@ -194,6 +211,7 @@ private static bool WritePrimitive(ComponentValue value, IntPtr destination)

case ComponentValueKind.S32:
case ComponentValueKind.U32:
case ComponentValueKind.Char:
Marshal.WriteInt32(payload, unchecked((int)value.Integer));
break;

Expand Down Expand Up @@ -239,6 +257,31 @@ private static void WriteOwnedHeapValue(ComponentValue value, IntPtr destination
WriteOwnedRecord(value.Fields, destination);
break;

case ComponentValueKind.Variant:
Marshal.WriteByte(destination, (byte)value.Kind);
WriteOwnedName(value.Text!, payload);
if (value.Payload is not null)
{
Marshal.WriteIntPtr(payload + NameSize, WriteOwnedBoxed(value.Payload));
}
break;

case ComponentValueKind.Flags:
Marshal.WriteByte(destination, (byte)value.Kind);
var count = value.Names.Count;
_ = checked(count * NameSize);
ComponentValueNative.wasmtime_component_valflags_new_uninit(payload, (nuint)count);
var names = Marshal.ReadIntPtr(payload + VectorDataOffset);
unsafe
{
new Span<byte>((void*)names, count * NameSize).Clear();
}
for (var i = 0; i < count; i++)
{
WriteOwnedName(value.Names[i], names + i * NameSize);
}
break;

case ComponentValueKind.Option:
Marshal.WriteByte(destination, (byte)value.Kind);
if (value.Flag)
Expand Down Expand Up @@ -413,6 +456,9 @@ public static ComponentValue Read(IntPtr source)
case ComponentValueKind.U32:
return ComponentValue.U32(unchecked((uint)Marshal.ReadInt32(payload)));

case ComponentValueKind.Char:
return ComponentValue.Char(unchecked((uint)Marshal.ReadInt32(payload)));

case ComponentValueKind.S64:
return ComponentValue.S64(Marshal.ReadInt64(payload));

Expand Down Expand Up @@ -440,6 +486,21 @@ public static ComponentValue Read(IntPtr source)
case ComponentValueKind.Record:
return ComponentValue.OwnedRecord(ReadRecord(payload));

case ComponentValueKind.Variant:
var variant = Marshal.ReadIntPtr(payload + NameSize);
return ComponentValue.Variant(ReadName(payload),
variant == IntPtr.Zero ? null : Read(variant));

case ComponentValueKind.Flags:
var count = checked((int)Marshal.ReadIntPtr(payload).ToInt64());
var names = Marshal.ReadIntPtr(payload + VectorDataOffset);
var flags = new string[count];
for (var i = 0; i < count; i++)
{
flags[i] = ReadName(names + checked(i * NameSize));
}
return ComponentValue.Flags(flags);

case ComponentValueKind.Option:
var some = Marshal.ReadIntPtr(payload);
return some == IntPtr.Zero ? ComponentValue.None() : ComponentValue.Some(Read(some));
Expand Down
110 changes: 110 additions & 0 deletions tests/ComponentExtendedValueTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using FluentAssertions;
using Wasmtime.Components;
using Xunit;

namespace Wasmtime.Tests;

public sealed class ComponentExtendedValueTests
{
private static ComponentValue RoundTrip(ComponentValue value, bool owned)
{
using var scope = new ComponentValueMarshaller.AllocationScope();
var pointer = scope.Allocate(ComponentValueMarshaller.ValueSize);
if (owned) ComponentValueMarshaller.WriteOwned(value, pointer);
else ComponentValueMarshaller.Write(value, pointer, scope);
try { return ComponentValueMarshaller.Read(pointer); }
finally { if (owned) ComponentValueNative.wasmtime_component_val_delete(pointer); }
}

[Theory]
[InlineData(0u)]
[InlineData(0xD7FFu)]
[InlineData(0xE000u)]
[InlineData(0xFFFFu)]
[InlineData(0x10000u)]
[InlineData(0x1F600u)]
[InlineData(0x10FFFFu)]
public void ItRoundTripsUnicodeScalars(uint scalar)
{
var rune = new Rune(scalar);
foreach (var owned in new[] { false, true })
{
RoundTrip(ComponentValue.Char(scalar), owned).AsChar().Should().Be(scalar);
RoundTrip(ComponentValue.Char(rune), owned).AsRune().Should().Be(rune);
if (scalar <= char.MaxValue)
RoundTrip(ComponentValue.Char((char)scalar), owned).AsChar().Should().Be(scalar);
}
}

[Theory]
[InlineData(0xD800u)]
[InlineData(0xDFFFu)]
[InlineData(0x110000u)]
[InlineData(uint.MaxValue)]
public void ItRejectsInvalidUnicodeScalars(uint scalar)
{
Action create = () => ComponentValue.Char(scalar);
create.Should().Throw<ArgumentOutOfRangeException>();
if (scalar <= char.MaxValue)
{
Action createFromChar = () => ComponentValue.Char((char)scalar);
createFromChar.Should().Throw<ArgumentOutOfRangeException>();
}
using var scope = new ComponentValueMarshaller.AllocationScope();
var pointer = scope.Allocate(ComponentValueMarshaller.ValueSize);
Marshal.WriteByte(pointer, (byte)ComponentValueKind.Char);
Marshal.WriteInt32(pointer + ComponentValueMarshaller.ValuePayloadOffset, unchecked((int)scalar));
Action read = () => ComponentValueMarshaller.Read(pointer);
read.Should().Throw<ArgumentOutOfRangeException>();
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void ItRoundTripsNestedVariantsAndArbitraryFlagCounts(bool owned)
{
var flags = Enumerable.Range(0, 100).Select(i => "flag-" + i).ToArray();
var value = ComponentValue.Variant("outer",
ComponentValue.Some(ComponentValue.List(new[]
{
ComponentValue.Variant("empty"),
ComponentValue.Variant("scalar", ComponentValue.Char(0x1F600)),
ComponentValue.Variant("access", ComponentValue.Flags(flags)),
})));
var result = RoundTrip(value, owned);
result.AsVariant().Should().Be("outer");
var items = result.Payload!.Payload!.AsList();
items[0].AsVariant().Should().Be("empty");
items[0].Payload.Should().BeNull();
items[1].Payload!.AsChar().Should().Be(0x1F600);
items[2].Payload!.AsFlags().Should().Equal(flags);
RoundTrip(ComponentValue.Flags(Array.Empty<string>()), owned).AsFlags().Should().BeEmpty();
}

[Fact]
public void ItCopiesFlagsAndRejectsInvalidInputsAndWrongAccessors()
{
var names = new[] { "first" };
var flags = ComponentValue.Flags(names);
names[0] = "changed";
flags.AsFlags().Should().Equal("first");
Action duplicates = () => ComponentValue.Flags(new[] { "first", "first" });
duplicates.Should().Throw<ArgumentException>();
Action nullName = () => ComponentValue.Flags(new[] { (string)null! });
nullName.Should().Throw<ArgumentException>();
Action nullNames = () => ComponentValue.Flags(null!);
nullNames.Should().Throw<ArgumentNullException>();
Action nullCase = () => ComponentValue.Variant(null!);
nullCase.Should().Throw<ArgumentNullException>();
Action wrongChar = () => flags.AsChar();
wrongChar.Should().Throw<InvalidOperationException>();
Action wrongVariant = () => flags.AsVariant();
wrongVariant.Should().Throw<InvalidOperationException>();
Action wrongFlags = () => ComponentValue.Char(0).AsFlags();
wrongFlags.Should().Throw<InvalidOperationException>();
}
}
Loading
Loading