Skip to content

Commit 0d419af

Browse files
authored
Reduce reflection use in generated C# code (#2725)
1 parent 6b8d077 commit 0d419af

34 files changed

+439
-158
lines changed

crates/bindings-csharp/BSATN.Codegen/Type.cs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -422,22 +422,12 @@ public Scope.Extensions ToExtensions()
422422

423423
var bsatnDecls = Members.Cast<MemberDeclaration>();
424424
var fieldNames = bsatnDecls.Select(m => m.Name);
425+
var fieldNamesAndIds = fieldNames.Select((name, i) => (name, i));
425426

426427
extensions.BaseTypes.Add($"System.IEquatable<{ShortName}>");
427428

428429
if (Kind is TypeKind.Sum)
429430
{
430-
extensions.Contents.Append(
431-
$$"""
432-
private {{ShortName}}() { }
433-
434-
internal enum @enum: byte
435-
{
436-
{{string.Join(",\n ", fieldNames)}}
437-
}
438-
439-
"""
440-
);
441431
extensions.Contents.Append(
442432
string.Join(
443433
"\n",
@@ -458,24 +448,24 @@ public override string ToString() =>
458448
);
459449

460450
read = $$"""
461-
__enumTag.Read(reader) switch {
451+
return reader.ReadByte() switch {
462452
{{string.Join(
463453
"\n ",
464-
fieldNames.Select(name =>
465-
$"@enum.{name} => new {name}({name}.Read(reader)),"
454+
fieldNames.Select((name, i) =>
455+
$"{i} => new {name}({name}.Read(reader)),"
466456
)
467457
)}}
468458
_ => throw new System.InvalidOperationException("Invalid tag value, this state should be unreachable.")
469-
}
459+
};
470460
""";
471461

472462
write = $$"""
473463
switch (value) {
474464
{{string.Join(
475465
"\n",
476-
fieldNames.Select(name => $"""
466+
fieldNames.Select((name, i) => $"""
477467
case {name}(var inner):
478-
__enumTag.Write(writer, @enum.{name});
468+
writer.Write((byte){i});
479469
{name}.Write(writer, inner);
480470
break;
481471
"""))}}
@@ -501,10 +491,6 @@ public override string ToString() =>
501491
return 0;
502492
}
503493
""";
504-
505-
bsatnDecls = bsatnDecls.Prepend(
506-
new("__enumTag", new ValueUse("@enum", "SpacetimeDB.BSATN.Enum<@enum>"))
507-
);
508494
}
509495
else
510496
{
@@ -526,6 +512,10 @@ public void WriteFields(System.IO.BinaryWriter writer) {
526512
)}}
527513
}
528514
515+
object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() {
516+
return new BSATN();
517+
}
518+
529519
"""
530520
);
531521

@@ -543,7 +533,14 @@ public override string ToString() =>
543533
"""
544534
);
545535

546-
read = $"SpacetimeDB.BSATN.IStructuralReadWrite.Read<{FullName}>(reader)";
536+
// Directly allocating the result object here (instead of calling e.g. IStructuralReadWrite.Read<T>, which does the same thing)
537+
// avoids generics; we've found that generics often result in reflective code being generated.
538+
// Using simple code here hopefully helps IL2CPP and Mono do this faster.
539+
read = $$"""
540+
var ___result = new {{FullName}}();
541+
___result.ReadFields(reader);
542+
return ___result;
543+
""";
547544

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

@@ -566,7 +563,9 @@ public override string ToString() =>
566563
{
567564
{{MemberDeclaration.GenerateBsatnFields(Accessibility.Internal, bsatnDecls)}}
568565
569-
public {{FullName}} Read(System.IO.BinaryReader reader) => {{read}};
566+
public {{FullName}} Read(System.IO.BinaryReader reader) {
567+
{{read}}
568+
}
570569
571570
public void Write(System.IO.BinaryWriter writer, {{FullName}} value) {
572571
{{write}}

crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,60 @@ namespace SpacetimeDB.BSATN;
22

33
using System.Text;
44

5+
/// <summary>
6+
/// Implemented by product types marked with [SpacetimeDB.Type].
7+
/// All rows in SpacetimeDB are product types, so this is also implemented by all row types.
8+
/// </summary>
59
public interface IStructuralReadWrite
610
{
11+
/// <summary>
12+
/// Initialize this value from the reader.
13+
/// The reader is assumed to store a <see href="https://spacetimedb.com/docs/bsatn">BSATN-encoded</see>
14+
/// value of this type.
15+
/// Advances the reader to the first byte past the end of the read value.
16+
/// Throws an exception if this would advance past the end of the reader.
17+
/// </summary>
18+
/// <param name="reader"></param>
719
void ReadFields(BinaryReader reader);
820

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

30+
/// <summary>
31+
/// Get an IReadWrite implementation that can read values of this type.
32+
/// In Rust, this would return <c>IReadWrite&lt;Self&gt;</c>, but the C# type system
33+
/// has no equivalent Self type -- that is, you can't use the implementing type in type signatures
34+
/// in an interface. So, you have to manually downcast.
35+
/// A typical invocation looks like: <c>(IReadWrite&lt;Row&gt;) new Row().GetSerializer()</c>
36+
///
37+
/// This is an instance method because of limitations of C# interfaces.
38+
/// (C# 11 has static virtual interface members, but Unity does not support C# 11.)
39+
/// This method always works, whether or not the row it is called on is correctly initialized.
40+
/// The returned serializer has nothing to do with the row GetSerializer is called on -- it returns
41+
/// new rows and does not modify or interact with the original row.
42+
///
43+
/// Using the resulting serializer rather than <c>Read&lt;T&gt;</c> is usually faster in Mono/IL2CPP.
44+
/// This is because we manually monomorphise the code to read rows in our automatically-generated
45+
/// implementation of IReadWrite. This allows rows to be initialized with new() rather than reflection
46+
/// in the compiled code.
47+
/// </summary>
48+
/// <returns>An <c>IReadWrite&lt;T&gt;</c> for <c>T : IStructuralReadWrite</c>.</returns>
49+
object GetSerializer();
50+
51+
/// <summary>
52+
/// Read a row from the reader.
53+
/// This method usually uses Activator.CreateInstance to create the resulting row;
54+
/// if this is too slow, prefer using GetSerializer.
55+
/// </summary>
56+
/// <typeparam name="T"></typeparam>
57+
/// <param name="reader"></param>
58+
/// <returns></returns>
1159
static T Read<T>(BinaryReader reader)
1260
where T : IStructuralReadWrite, new()
1361
{
@@ -38,15 +86,43 @@ public static byte[] ToBytes<T>(T value)
3886
}
3987
}
4088

89+
/// <summary>
90+
/// Interface for types that know how to serialize another type.
91+
/// We auto-generate an implementation of <c>IReadWrite&lt;T&gt;</c> for all
92+
/// types marked with <c>[SpacetimeDB.Type]</c>. For a type <c>T</c>, this implementation
93+
/// is accessible at <c>T.BSATN</c>. The implementation is always a zero-sized struct.
94+
/// </summary>
95+
/// <typeparam name="T"></typeparam>
4196
public interface IReadWrite<T>
4297
{
98+
/// <summary>
99+
/// Read a BSATN-encoded value of type T from the reader.
100+
/// Throws on end-of-stream or if the stream is malformed.
101+
/// Advances the reader to the first byte past the end of the encoded value.
102+
/// </summary>
103+
/// <param name="reader"></param>
104+
/// <returns></returns>
43105
T Read(BinaryReader reader);
44106

107+
/// <summary>
108+
/// Write a BSATN-encoded value of type T to the writer.
109+
/// </summary>
45110
void Write(BinaryWriter writer, T value);
46111

112+
/// <summary>
113+
/// Get metadata for this type. Used in module initialization.
114+
/// </summary>
115+
/// <param name="registrar"></param>
116+
/// <returns></returns>
47117
AlgebraicType GetAlgebraicType(ITypeRegistrar registrar);
48118
}
49119

120+
/// <summary>
121+
/// Present for backwards-compatibility reasons, but no longer used.
122+
/// The auto-generated serialization code for enum now reads/writes
123+
/// the tag byte directly to the wire. This avoids calling into reflective code.
124+
/// </summary>
125+
/// <typeparam name="T"></typeparam>
50126
public readonly struct Enum<T> : IReadWrite<T>
51127
where T : struct, Enum
52128
{
@@ -68,6 +144,9 @@ private static T Validate(T value)
68144
// However, enum values are guaranteed to be sequential and zero based.
69145
// Hence we only ever need to do an upper bound check.
70146
// See `SpacetimeDB.Type.ParseEnum` for the syntax analysis.
147+
148+
// Later note: this STILL uses reflection. We've deprecated this class entirely
149+
// because of this.
71150
if (Convert.ToUInt64(value) >= NumVariants)
72151
{
73152
throw new ArgumentOutOfRangeException(

crates/bindings-csharp/BSATN.Runtime/Builtins.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,11 @@ public readonly void WriteFields(BinaryWriter writer)
392392
BSATN.MicrosecondsSinceUnixEpoch.Write(writer, MicrosecondsSinceUnixEpoch);
393393
}
394394

395+
readonly object IStructuralReadWrite.GetSerializer()
396+
{
397+
return new BSATN();
398+
}
399+
395400
public readonly partial struct BSATN : IReadWrite<Timestamp>
396401
{
397402
internal static readonly I64 MicrosecondsSinceUnixEpoch = new();
@@ -463,6 +468,11 @@ public readonly void WriteFields(BinaryWriter writer)
463468
BSATN.__time_duration_micros__.Write(writer, Microseconds);
464469
}
465470

471+
readonly object IStructuralReadWrite.GetSerializer()
472+
{
473+
return new BSATN();
474+
}
475+
466476
public readonly partial struct BSATN : IReadWrite<TimeDuration>
467477
{
468478
internal static readonly I64 __time_duration_micros__ = new();

crates/bindings-csharp/Codegen.Tests/fixtures/client/snapshots/Type#CustomClass.verified.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ public void WriteFields(System.IO.BinaryWriter writer)
1616
BSATN.StringField.Write(writer, StringField);
1717
}
1818

19+
object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer()
20+
{
21+
return new BSATN();
22+
}
23+
1924
public override string ToString() =>
2025
$"CustomClass {{ IntField = {SpacetimeDB.BSATN.StringUtil.GenericToString(IntField)}, StringField = {SpacetimeDB.BSATN.StringUtil.GenericToString(StringField)} }}";
2126

@@ -24,8 +29,12 @@ public override string ToString() =>
2429
internal static readonly SpacetimeDB.BSATN.I32 IntField = new();
2530
internal static readonly SpacetimeDB.BSATN.String StringField = new();
2631

27-
public CustomClass Read(System.IO.BinaryReader reader) =>
28-
SpacetimeDB.BSATN.IStructuralReadWrite.Read<CustomClass>(reader);
32+
public CustomClass Read(System.IO.BinaryReader reader)
33+
{
34+
var ___result = new CustomClass();
35+
___result.ReadFields(reader);
36+
return ___result;
37+
}
2938

3039
public void Write(System.IO.BinaryWriter writer, CustomClass value)
3140
{

crates/bindings-csharp/Codegen.Tests/fixtures/client/snapshots/Type#CustomStruct.verified.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public void WriteFields(System.IO.BinaryWriter writer)
1818
BSATN.StringField.Write(writer, StringField);
1919
}
2020

21+
object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer()
22+
{
23+
return new BSATN();
24+
}
25+
2126
public override string ToString() =>
2227
$"CustomStruct {{ IntField = {SpacetimeDB.BSATN.StringUtil.GenericToString(IntField)}, StringField = {SpacetimeDB.BSATN.StringUtil.GenericToString(StringField)} }}";
2328

@@ -26,8 +31,12 @@ public override string ToString() =>
2631
internal static readonly SpacetimeDB.BSATN.I32 IntField = new();
2732
internal static readonly SpacetimeDB.BSATN.String StringField = new();
2833

29-
public CustomStruct Read(System.IO.BinaryReader reader) =>
30-
SpacetimeDB.BSATN.IStructuralReadWrite.Read<CustomStruct>(reader);
34+
public CustomStruct Read(System.IO.BinaryReader reader)
35+
{
36+
var ___result = new CustomStruct();
37+
___result.ReadFields(reader);
38+
return ___result;
39+
}
3140

3241
public void Write(System.IO.BinaryWriter writer, CustomStruct value)
3342
{

crates/bindings-csharp/Codegen.Tests/fixtures/client/snapshots/Type#CustomTaggedEnum.verified.cs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,6 @@
44

55
partial record CustomTaggedEnum : System.IEquatable<CustomTaggedEnum>
66
{
7-
private CustomTaggedEnum() { }
8-
9-
internal enum @enum : byte
10-
{
11-
IntVariant,
12-
StringVariant
13-
}
14-
157
public sealed record IntVariant(int IntVariant_) : CustomTaggedEnum
168
{
179
public override string ToString() =>
@@ -26,31 +18,32 @@ public override string ToString() =>
2618

2719
public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite<CustomTaggedEnum>
2820
{
29-
internal static readonly SpacetimeDB.BSATN.Enum<@enum> __enumTag = new();
3021
internal static readonly SpacetimeDB.BSATN.I32 IntVariant = new();
3122
internal static readonly SpacetimeDB.BSATN.String StringVariant = new();
3223

33-
public CustomTaggedEnum Read(System.IO.BinaryReader reader) =>
34-
__enumTag.Read(reader) switch
24+
public CustomTaggedEnum Read(System.IO.BinaryReader reader)
25+
{
26+
return reader.ReadByte() switch
3527
{
36-
@enum.IntVariant => new IntVariant(IntVariant.Read(reader)),
37-
@enum.StringVariant => new StringVariant(StringVariant.Read(reader)),
28+
0 => new IntVariant(IntVariant.Read(reader)),
29+
1 => new StringVariant(StringVariant.Read(reader)),
3830
_
3931
=> throw new System.InvalidOperationException(
4032
"Invalid tag value, this state should be unreachable."
4133
)
4234
};
35+
}
4336

4437
public void Write(System.IO.BinaryWriter writer, CustomTaggedEnum value)
4538
{
4639
switch (value)
4740
{
4841
case IntVariant(var inner):
49-
__enumTag.Write(writer, @enum.IntVariant);
42+
writer.Write((byte)0);
5043
IntVariant.Write(writer, inner);
5144
break;
5245
case StringVariant(var inner):
53-
__enumTag.Write(writer, @enum.StringVariant);
46+
writer.Write((byte)1);
5447
StringVariant.Write(writer, inner);
5548
break;
5649
}

crates/bindings-csharp/Codegen.Tests/fixtures/client/snapshots/Type#PublicTable.verified.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public void WriteFields(System.IO.BinaryWriter writer)
6262
BSATN.NullableReferenceField.Write(writer, NullableReferenceField);
6363
}
6464

65+
object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer()
66+
{
67+
return new BSATN();
68+
}
69+
6570
public override string ToString() =>
6671
$"PublicTable {{ ByteField = {SpacetimeDB.BSATN.StringUtil.GenericToString(ByteField)}, UshortField = {SpacetimeDB.BSATN.StringUtil.GenericToString(UshortField)}, UintField = {SpacetimeDB.BSATN.StringUtil.GenericToString(UintField)}, UlongField = {SpacetimeDB.BSATN.StringUtil.GenericToString(UlongField)}, U128Field = {SpacetimeDB.BSATN.StringUtil.GenericToString(U128Field)}, U256Field = {SpacetimeDB.BSATN.StringUtil.GenericToString(U256Field)}, SbyteField = {SpacetimeDB.BSATN.StringUtil.GenericToString(SbyteField)}, ShortField = {SpacetimeDB.BSATN.StringUtil.GenericToString(ShortField)}, IntField = {SpacetimeDB.BSATN.StringUtil.GenericToString(IntField)}, LongField = {SpacetimeDB.BSATN.StringUtil.GenericToString(LongField)}, I128Field = {SpacetimeDB.BSATN.StringUtil.GenericToString(I128Field)}, I256Field = {SpacetimeDB.BSATN.StringUtil.GenericToString(I256Field)}, BoolField = {SpacetimeDB.BSATN.StringUtil.GenericToString(BoolField)}, FloatField = {SpacetimeDB.BSATN.StringUtil.GenericToString(FloatField)}, DoubleField = {SpacetimeDB.BSATN.StringUtil.GenericToString(DoubleField)}, StringField = {SpacetimeDB.BSATN.StringUtil.GenericToString(StringField)}, IdentityField = {SpacetimeDB.BSATN.StringUtil.GenericToString(IdentityField)}, ConnectionIdField = {SpacetimeDB.BSATN.StringUtil.GenericToString(ConnectionIdField)}, CustomStructField = {SpacetimeDB.BSATN.StringUtil.GenericToString(CustomStructField)}, CustomClassField = {SpacetimeDB.BSATN.StringUtil.GenericToString(CustomClassField)}, CustomEnumField = {SpacetimeDB.BSATN.StringUtil.GenericToString(CustomEnumField)}, CustomTaggedEnumField = {SpacetimeDB.BSATN.StringUtil.GenericToString(CustomTaggedEnumField)}, ListField = {SpacetimeDB.BSATN.StringUtil.GenericToString(ListField)}, NullableValueField = {SpacetimeDB.BSATN.StringUtil.GenericToString(NullableValueField)}, NullableReferenceField = {SpacetimeDB.BSATN.StringUtil.GenericToString(NullableReferenceField)} }}";
6772

@@ -100,8 +105,12 @@ internal static readonly SpacetimeDB.BSATN.RefOption<
100105
SpacetimeDB.BSATN.String
101106
> NullableReferenceField = new();
102107

103-
public PublicTable Read(System.IO.BinaryReader reader) =>
104-
SpacetimeDB.BSATN.IStructuralReadWrite.Read<PublicTable>(reader);
108+
public PublicTable Read(System.IO.BinaryReader reader)
109+
{
110+
var ___result = new PublicTable();
111+
___result.ReadFields(reader);
112+
return ___result;
113+
}
105114

106115
public void Write(System.IO.BinaryWriter writer, PublicTable value)
107116
{

0 commit comments

Comments
 (0)