Skip to content
Draft
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,8 @@ jobs:
run: dotnet restore --configfile ../../../NuGet.Config blackholio.csproj

- name: Build Godot project
run: godot --headless --verbose --path demo/Blackholio/client-godot --build-solutions --quit
working-directory: demo/Blackholio/client-godot
run: godot --headless --verbose --build-solutions --quit

- name: Start SpacetimeDB
run: |
Expand All @@ -1061,7 +1062,8 @@ jobs:
bash ./publish.sh

- name: Run Godot tests
run: godot --headless --path demo/Blackholio/client-godot --scene res://tests/GodotPlayModeTests.tscn
working-directory: demo/Blackholio/client-godot
run: godot --headless --scene res://tests/GodotPlayModeTests.tscn

csharp-testsuite:
needs: [merge_queue_noop, lints]
Expand Down
43 changes: 24 additions & 19 deletions crates/bindings-csharp/BSATN.Codegen/Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -624,16 +624,32 @@ public Scope.Extensions ToExtensions()

if (Kind is TypeKind.Sum)
{
extensions.BaseTypes.Add("SpacetimeDB.BSATN.IStructuralWrite");
extensions.ExtraAttrs.Add("abstract");

extensions.Contents.Append(
$$"""
private {{ShortNameIdentifier}}() { }

"""
);

extensions.Contents.Append(
string.Join(
"\n",
Members.Select(m =>
Members.Select((m, i) =>
// C# puts field names in the same namespace as records themselves, and will complain about clashes if they match.
// To avoid this, we append an underscore to the field name.
// In most cases the field name shouldn't matter anyway as you'll idiomatically use pattern matching to extract the value.
$$"""
public sealed record {{m.Identifier}}({{m.Type.Name}} {{m.Identifier}}_) : {{ShortNameIdentifier}}
{
public override void WriteFields(System.IO.BinaryWriter writer)
{
writer.Write((byte){{i}});
BSATN.{{m.Identifier}}{{TypeUse.BsatnFieldSuffix}}.Write(writer, {{m.Identifier}}_);
}

public override string ToString() =>
$"{{m.Name}}({ SpacetimeDB.BSATN.StringUtil.GenericToString({{m.Identifier}}_) })";
}
Expand All @@ -655,18 +671,7 @@ public override string ToString() =>
};
""";

write = $$"""
switch (value) {
{{string.Join(
"\n",
bsatnDecls.Select((m, i) => $"""
case {m.Identifier}(var inner):
writer.Write((byte){i});
{m.Identifier}{TypeUse.BsatnFieldSuffix}.Write(writer, inner);
break;
"""))}}
}
""";
write = "value.WriteFields(writer);";

getHashCode = $$"""
switch (this) {
Expand Down Expand Up @@ -740,13 +745,13 @@ public override string ToString() =>

write = "value.WriteFields(writer);";

var declHashName = (MemberDeclaration decl) => $"___hash{decl.Name}";
static string DeclHashName(MemberDeclaration decl) => $"___hash{decl.Name}";

getHashCode = $$"""
{{string.Join("\n", bsatnDecls.Select(decl => decl.Type.GetHashCodeStatement(decl.Identifier, declHashName(decl))))}}
{{string.Join("\n", bsatnDecls.Select(decl => decl.Type.GetHashCodeStatement(decl.Identifier, DeclHashName(decl))))}}
return {{JoinOrValue(
" ^\n ",
bsatnDecls.Select(declHashName),
bsatnDecls.Select(DeclHashName),
"0" // if there are no members, the hash is 0.
)}};
""";
Expand Down Expand Up @@ -789,7 +794,7 @@ public override int GetHashCode()
// If we are a reference type, various equality methods need to take nullable references.
// If we are a value type, everything is pleasantly by-value.
var fullNameMaybeRef = $"{FullName}{(Scope.IsStruct ? "" : "?")}";
var declEqualsName = (MemberDeclaration decl) => $"___eq{decl.Name}";
static string DeclEqualsName(MemberDeclaration decl) => $"___eq{decl.Name}";

extensions.Contents.Append(
$$"""
Expand All @@ -798,10 +803,10 @@ public override int GetHashCode()
public bool Equals({{fullNameMaybeRef}} that)
{
{{(Scope.IsStruct ? "" : "if (((object?)that) == null) { return false; }\n ")}}
{{string.Join("\n", bsatnDecls.Select(decl => decl.Type.EqualsStatement($"this.{decl.Identifier}", $"that.{decl.Identifier}", declEqualsName(decl))))}}
{{string.Join("\n", bsatnDecls.Select(decl => decl.Type.EqualsStatement($"this.{decl.Identifier}", $"that.{decl.Identifier}", DeclEqualsName(decl))))}}
return {{JoinOrValue(
" &&\n ",
bsatnDecls.Select(declEqualsName),
bsatnDecls.Select(DeclEqualsName),
"true" // if there are no elements, the structs are equal :)
)}};
}
Expand Down
9 changes: 7 additions & 2 deletions crates/bindings-csharp/BSATN.Runtime/Attrs.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace SpacetimeDB;

using System.IO;
using System.Runtime.CompilerServices;
using SpacetimeDB.BSATN;

[AttributeUsage(
AttributeTargets.Struct | AttributeTargets.Class | AttributeTargets.Enum,
Expand All @@ -11,5 +13,8 @@ public sealed class TypeAttribute : Attribute { }

// This could be an interface, but using `record` forces C# to check that it can
// only be applied on types that are records themselves.
public abstract record TaggedEnum<Variants>
where Variants : struct, ITuple { }
public abstract record TaggedEnum<Variants> : IStructuralWrite
where Variants : struct, ITuple
{
public abstract void WriteFields(BinaryWriter writer);
}
78 changes: 49 additions & 29 deletions crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,41 @@ namespace SpacetimeDB.BSATN;
/// Implemented by product types marked with [SpacetimeDB.Type].
/// All rows in SpacetimeDB are product types, so this is also implemented by all row types.
/// </summary>
public interface IStructuralReadWrite
public interface IStructuralWrite
{
/// <summary>
/// Write the fields of this type to the writer.
/// Throws an exception if the underlying writer throws.
/// Throws if this value is malformed (i.e. has null values for fields that
/// are not explicitly marked nullable.)
/// </summary>
/// <param name="writer"></param>
void WriteFields(BinaryWriter writer);

public static byte[] ToBytes<RW, T>(RW rw, T value)
where RW : IReadWrite<T>
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
rw.Write(writer, value);
return stream.ToArray();
}

public static byte[] ToBytes<T>(T value)
where T : IStructuralWrite
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
value.WriteFields(writer);
return stream.ToArray();
}
}

/// <summary>
/// Implemented by product types marked with [SpacetimeDB.Type] that can be read from BSATN.
/// All rows in SpacetimeDB are product types, so this is also implemented by all row types.
/// </summary>
public interface IStructuralReadWrite : IStructuralWrite
{
/// <summary>
/// Initialize this value from the reader.
Expand All @@ -18,15 +52,6 @@ public interface IStructuralReadWrite
/// <param name="reader"></param>
void ReadFields(BinaryReader reader);

/// <summary>
/// Write the fields of this type to the writer.
/// Throws an exception if the underlying writer throws.
/// Throws if this value is malformed (i.e. has null values for fields that
/// are not explicitly marked nullable.)
/// </summary>
/// <param name="writer"></param>
void WriteFields(BinaryWriter writer);

/// <summary>
/// Get an IReadWrite implementation that can read values of this type.
/// In Rust, this would return <c>IReadWrite&lt;Self&gt;</c>, but the C# type system
Expand Down Expand Up @@ -67,23 +92,13 @@ static T Read<T>(BinaryReader reader)
return result;
}

public static byte[] ToBytes<RW, T>(RW rw, T value)
where RW : IReadWrite<T>
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
rw.Write(writer, value);
return stream.ToArray();
}
public static new byte[] ToBytes<RW, T>(RW rw, T value)
where RW : IReadWrite<T> =>
IStructuralWrite.ToBytes(rw, value);

public static byte[] ToBytes<T>(T value)
where T : IStructuralReadWrite
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
value.WriteFields(writer);
return stream.ToArray();
}
public static new byte[] ToBytes<T>(T value)
where T : IStructuralReadWrite =>
IStructuralWrite.ToBytes(value);
}

/// <summary>
Expand Down Expand Up @@ -129,7 +144,9 @@ public interface IReadWrite<T>
/// Note: the [Type] macro rejects enums with explicitly set values (see Codegen.Tests),
/// so this array is guaranteed to be continuous and indexed starting from 0.
/// </summary>
#pragma warning disable CA2263, IDE0305 // netstandard2.1 lacks the generic Enum overloads.
private static readonly T[] TagToValue = Enum.GetValues(typeof(T)).Cast<T>().ToArray();
#pragma warning restore CA2263, IDE0305

public T Read(BinaryReader reader)
{
Expand Down Expand Up @@ -177,9 +194,12 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
registrar.RegisterType<T>(
(_) =>
new AlgebraicType.Sum(
Enum.GetNames(typeof(T))
.Select(name => new AggregateElement(name, AlgebraicType.Unit))
.ToArray()
#pragma warning disable CA2263 // netstandard2.1 lacks the generic Enum overloads.
[
.. Enum.GetNames(typeof(T))
.Select(name => new AggregateElement(name, AlgebraicType.Unit)),
]
#pragma warning restore CA2263
)
);
}
Expand Down
4 changes: 2 additions & 2 deletions crates/bindings-csharp/BSATN.Runtime/BSATN/U128.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public static U128 FromBytesBigEndian(ReadOnlySpan<byte> bytes)
);
}

var upper = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(0, 8));
var lower = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8));
var upper = BinaryPrimitives.ReadUInt64BigEndian(bytes[..8]);
var lower = BinaryPrimitives.ReadUInt64BigEndian(bytes[8..16]);

return new U128(upper, lower);
}
Expand Down
Loading
Loading