Skip to content

Commit 9fbb95f

Browse files
authored
Introduce Read32/64/128 methods (#289)
These methods allow reading arbitrary types that can be represented as integers of the above sizes. This avoids boxing while deserializing. Also adds support for Guid to use the 128 method by default.
1 parent 35c1858 commit 9fbb95f

11 files changed

Lines changed: 196 additions & 13 deletions

File tree

perf/bench/DataGenerator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public static T GenerateSerialize<T>() where T : Serde.ISerializeProvider<T>
1515
return (T)(object)Serde.Test.AllInOne.Sample;
1616
if (typeof(T) == typeof(Primitives))
1717
return (T)(object)Primitives.Sample;
18+
if (typeof(T) == typeof(Guids))
19+
return (T)(object)Guids.Sample;
1820

1921
throw new InvalidOperationException();
2022

@@ -51,6 +53,8 @@ public static string GenerateDeserialize<T>()
5153
return Serde.Test.AllInOne.SampleSerialized;
5254
if (typeof(T) == typeof(Primitives))
5355
return Primitives.SampleSerialized;
56+
if (typeof(T) == typeof(Guids))
57+
return Guids.SampleSerialized;
5458

5559
throw new InvalidOperationException("Unexpected type");
5660
}

perf/bench/DeserializeFromString.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
using BenchmarkDotNet.Attributes;
77
using Serde;
88
using Serde.Test;
9+
using STJ = System.Text.Json;
910

1011
namespace Benchmarks
1112
{
1213
[GenericTypeArguments(typeof(LoginViewModel), typeof(LoginViewModel))]
1314
[GenericTypeArguments(typeof(Location), typeof(LocationWrap))]
1415
[GenericTypeArguments(typeof(Primitives), typeof(Primitives))]
16+
[GenericTypeArguments(typeof(Guids), typeof(Guids))]
1517
[GenericTypeArguments(typeof(AllInOne), typeof(AllInOne))]
1618
public class DeserializeFromString<T, U>
1719
where T : Serde.IDeserializeProvider<T>
@@ -29,7 +31,11 @@ public void Setup()
2931
_options = new JsonSerializerOptions()
3032
{
3133
IncludeFields = true,
32-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
34+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
35+
Converters =
36+
{
37+
new STJ.Serialization.JsonStringEnumConverter(STJ.JsonNamingPolicy.CamelCase)
38+
}
3339
};
3440
value = DataGenerator.GenerateDeserialize<T>();
3541
}

perf/bench/SampleTypes.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,32 @@ long LongField
209209
"longField": 9223372036854775807
210210
}
211211
""";
212+
}
212213

213-
214+
[GenerateSerde]
215+
public partial record Guids(
216+
Guid GuidField,
217+
Guid GuidField2
218+
)
219+
{
220+
public static readonly Guids Sample = new Guids(
221+
GuidField: new Guid(new byte[] {
222+
0x01, 0x02, 0x03, 0x04,
223+
0x05, 0x06, 0x07, 0x08,
224+
0x09, 0x0A, 0x0B, 0x0C,
225+
0x0D, 0x0E, 0x0F, 0x10,
226+
}),
227+
GuidField2: new Guid(new byte[] {
228+
0x10, 0x0F, 0x0E, 0x0D,
229+
0x0C, 0x0B, 0x0A, 0x09,
230+
0x08, 0x07, 0x06, 0x05,
231+
0x04, 0x03, 0x02, 0x01,
232+
})
233+
);
234+
public const string SampleSerialized = """
235+
{
236+
"guidField": "04030201-0605-0807-090a-0b0c0d0e0f10",
237+
"guidField2": "0d0e0f10-0b0a-0908-0706-050403020101"
238+
}
239+
""";
214240
}

src/generator/DeserializeImplGen.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,14 @@ private static SourceBuilder GenerateCustomDeserializeMethod(
281281
}
282282
else if (Proxies.TryGetImplicitWrapper(m.Type, context, SerdeUsage.Deserialize, inProgress) is { Proxy: { } wrap })
283283
{
284-
readValueCall = $"{readMethodName}<{memberType}, {wrap}>";
284+
if (wrap == "global::Serde.GuidProxy")
285+
{
286+
readValueCall = $"ReadGuid<{wrap}>";
287+
}
288+
else
289+
{
290+
readValueCall = $"{readMethodName}<{memberType}, {wrap}>";
291+
}
285292
}
286293
else
287294
{

src/serde/IDeserializer.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
using System;
33
using System.Buffers;
4+
using System.Buffers.Binary;
45

56
namespace Serde;
67

@@ -83,9 +84,51 @@ public interface ITypeDeserializer : IDisposable
8384
/// </summary>
8485
(int, string? errorName) TryReadIndexWithName(ISerdeInfo info);
8586

87+
/// <summary>
88+
/// Read a value of type T using the provided deserializer.
89+
/// </summary>
8690
T ReadValue<T>(ISerdeInfo info, int index, IDeserialize<T> deserialize)
8791
where T : class?;
8892

93+
/// <summary>
94+
/// Read a value that can be represented as a 32-bit integer.
95+
/// </summary>
96+
/// <remarks>
97+
/// Must be implemented by deserializers to avoid boxing.
98+
///
99+
/// Useful for deserializing structs/enums without boxing. Unlike <see cref="ReadValue{T}"/> ,
100+
/// this method does not require T to be a reference type. And unlike <see cref="ReadU32" />
101+
/// there is no requirement that the serialized representation is exactly a 32-bit integer.
102+
/// </remarks>
103+
UInt32 ReadValue32(ISerdeInfo info, int index, IDeserialize<UInt32> deserialize)
104+
=> this.ReadBoxedValue(info, index, deserialize);
105+
106+
/// <summary>
107+
/// Read a value that can be represented as a 64-bit integer.
108+
/// </summary>
109+
/// <remarks>
110+
/// Must be implemented by deserializers to avoid boxing.
111+
///
112+
/// Useful for deserializing structs/enums without boxing. Unlike <see cref="ReadValue{T}"/> ,
113+
/// this method does not require T to be a reference type. And unlike <see cref="ReadU32" />
114+
/// there is no requirement that the serialized representation is exactly a 32-bit integer.
115+
/// </remarks>
116+
UInt64 ReadValue64(ISerdeInfo info, int index, IDeserialize<UInt64> deserialize)
117+
=> this.ReadBoxedValue(info, index, deserialize);
118+
119+
/// <summary>
120+
/// Read a value that can be represented as a 128-bit integer.
121+
/// </summary>
122+
/// <remarks>
123+
/// Must be implemented by deserializers to avoid boxing.
124+
///
125+
/// Useful for deserializing structs/enums without boxing. Unlike <see cref="ReadValue{T}"/> ,
126+
/// this method does not require T to be a reference type. And unlike <see cref="ReadU32" />
127+
/// there is no requirement that the serialized representation is exactly a 32-bit integer.
128+
/// </remarks>
129+
UInt128 ReadValue128(ISerdeInfo info, int index, IDeserialize<UInt128> deserialize)
130+
=> this.ReadBoxedValue(info, index, deserialize);
131+
89132
void SkipValue(ISerdeInfo info, int index);
90133
bool ReadBool(ISerdeInfo info, int index);
91134
char ReadChar(ISerdeInfo info, int index);
@@ -131,9 +174,31 @@ public static T ReadValue<T, TProvider>(this ITypeDeserializer deserializeType,
131174
return deserializeType.ReadValue(info, index, TProvider.Instance);
132175
}
133176

177+
public static T ReadBoxedValue<T>(this ITypeDeserializer deserializeType, ISerdeInfo info, int index, IDeserialize<T> d)
178+
where T : struct
179+
{
180+
return (T)deserializeType.ReadValue(info, index, new BoxProxy.De<T>(d))!;
181+
}
182+
134183
public static T ReadBoxedValue<T, TProvider>(this ITypeDeserializer deserializeType, ISerdeInfo info, int index)
135184
where TProvider : IDeserializeProvider<T>
136185
{
137186
return (T)deserializeType.ReadValue(info, index, BoxProxy.De<T, TProvider>.Instance)!;
138187
}
188+
189+
public static Guid ReadGuid<TProvider>(this ITypeDeserializer deserializeType, ISerdeInfo info, int index)
190+
where TProvider : IDeserializeProvider<UInt128>
191+
{
192+
var u128 = deserializeType.ReadValue128(info, index, TProvider.Instance);
193+
Span<byte> bytes = stackalloc byte[16];
194+
if (BitConverter.IsLittleEndian)
195+
{
196+
BinaryPrimitives.WriteUInt128LittleEndian(bytes, u128);
197+
}
198+
else
199+
{
200+
BinaryPrimitives.WriteUInt128BigEndian(bytes, u128);
201+
}
202+
return new Guid(bytes, !BitConverter.IsLittleEndian);
203+
}
139204
}

src/serde/Proxies.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using System;
44
using System.Buffers;
5+
using System.Buffers.Binary;
56
using System.Diagnostics;
67
using System.Threading;
78

@@ -370,19 +371,19 @@ public void Serialize(T value, ITypeSerializer serializer, ISerdeInfo info, int
370371
}
371372
}
372373

373-
public sealed class De<T, TProvider> : IDeserialize<object?>, ITypeDeserialize<T>
374-
where TProvider : IDeserializeProvider<T>
374+
public sealed class De<T>(IDeserialize<T> _underlying) : IDeserialize<object?>, ITypeDeserialize<T>
375375
{
376-
private IDeserialize<T> _underlying = TProvider.Instance;
377-
378-
public static readonly De<T, TProvider> Instance = new();
379-
private De() {}
380-
381376
public ISerdeInfo SerdeInfo => _underlying.SerdeInfo;
382377
public object? Deserialize(IDeserializer deserializer) => _underlying.Deserialize(deserializer);
383378
public T Deserialize(ITypeDeserializer deserializer, ISerdeInfo info, int index)
384379
=> (T)deserializer.ReadValue(info, index, this)!;
385380
}
381+
382+
public static class De<T, TProvider>
383+
where TProvider : IDeserializeProvider<T>
384+
{
385+
public static readonly De<T> Instance = new De<T>(TProvider.Instance);
386+
}
386387
}
387388

388389

@@ -473,9 +474,13 @@ private De() { }
473474
}
474475
}
475476

476-
public sealed class GuidProxy : ISerdePrimitive<GuidProxy, Guid>
477+
public sealed class GuidProxy
478+
: ISerdePrimitive<GuidProxy, Guid>,
479+
IDeserialize<UInt128>,
480+
IDeserializeProvider<UInt128>
477481
{
478482
public static GuidProxy Instance { get; } = new();
483+
static IDeserialize<UInt128> IDeserializeProvider<UInt128>.Instance => Instance;
479484
private GuidProxy() { }
480485

481486
public static ISerdeInfo SerdeInfo { get; }
@@ -494,6 +499,20 @@ Guid IDeserialize<Guid>.Deserialize(IDeserializer deserializer)
494499
return Guid.Parse(bytes);
495500
}
496501

502+
UInt128 IDeserialize<UInt128>.Deserialize(IDeserializer deserializer)
503+
{
504+
var str = deserializer.ReadString();
505+
var guid = Guid.Parse(str);
506+
Span<byte> guidBytes = stackalloc byte[16];
507+
if (!guid.TryWriteBytes(guidBytes, bigEndian: !BitConverter.IsLittleEndian, out int written) || written != 16)
508+
{
509+
throw new InvalidOperationException("Couldn't write GUID bytes");
510+
}
511+
return BitConverter.IsLittleEndian
512+
? BinaryPrimitives.ReadUInt128LittleEndian(guidBytes)
513+
: BinaryPrimitives.ReadUInt128BigEndian(guidBytes);
514+
}
515+
497516
void ITypeSerialize<Guid>.Serialize(Guid value, ITypeSerializer serializer, ISerdeInfo info, int index)
498517
{
499518
var bytes = value.ToString();

src/serde/json/JsonDeserializer.Collection.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,27 @@ public T ReadValue<T>(ISerdeInfo info, int index, IDeserialize<T> d) where T : c
101101
return next;
102102
}
103103

104+
public UInt32 ReadValue32(ISerdeInfo info, int index, IDeserialize<UInt32> d)
105+
{
106+
var next = d.Deserialize(_deserializer);
107+
_index++;
108+
return next;
109+
}
110+
111+
public UInt64 ReadValue64(ISerdeInfo info, int index, IDeserialize<UInt64> d)
112+
{
113+
var next = d.Deserialize(_deserializer);
114+
_index++;
115+
return next;
116+
}
117+
118+
public UInt128 ReadValue128(ISerdeInfo info, int index, IDeserialize<UInt128> d)
119+
{
120+
var next = d.Deserialize(_deserializer);
121+
_index++;
122+
return next;
123+
}
124+
104125
public void SkipValue(ISerdeInfo info, int index)
105126
{
106127
_deserializer.Reader.Skip();

src/serde/json/JsonDeserializer.Type.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ T ITypeDeserializer.ReadValue<T>(ISerdeInfo info, int index, IDeserialize<T> d)
7777
return d.Deserialize(_deserializer);
7878
}
7979

80+
uint ITypeDeserializer.ReadValue32(ISerdeInfo info, int index, IDeserialize<uint> d)
81+
{
82+
ReadColon();
83+
return d.Deserialize(_deserializer);
84+
}
85+
86+
ulong ITypeDeserializer.ReadValue64(ISerdeInfo info, int index, IDeserialize<ulong> d)
87+
{
88+
ReadColon();
89+
return d.Deserialize(_deserializer);
90+
}
91+
92+
UInt128 ITypeDeserializer.ReadValue128(ISerdeInfo info, int index, IDeserialize<UInt128> d)
93+
{
94+
ReadColon();
95+
return d.Deserialize(_deserializer);
96+
}
97+
8098
private void ReadColon()
8199
{
82100
var peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace());

test/Serde.Generation.Test/test_output/AllInOneTest.GeneratorTest/Serde.Test.AllInOne.IDeserialize.g.verified.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ sealed partial class _DeObj : Serde.IDeserialize<Serde.Test.AllInOne>
142142
break;
143143
case 17:
144144
Serde.DeserializeException.ThrowIfDuplicate(_r_assignedValid, 17, _l_serdeInfo);
145-
_l_guidfield = typeDeserialize.ReadBoxedValue<System.Guid, global::Serde.GuidProxy>(_l_serdeInfo, _l_index_);
145+
_l_guidfield = typeDeserialize.ReadGuid<global::Serde.GuidProxy>(_l_serdeInfo, _l_index_);
146146
_r_assignedValid |= ((uint)1) << 17;
147147
break;
148148
case 18:

test/Serde.Test/AllInOneJsonTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Serde.Json;
22
using Xunit;
3+
using STJ = System.Text.Json;
34

45
namespace Serde.Test;
56

@@ -18,4 +19,20 @@ public void DeserializeTest()
1819
var actual = JsonSerializer.Deserialize<AllInOne>(AllInOne.SampleSerialized);
1920
Assert.Equal(AllInOne.Sample, actual);
2021
}
22+
23+
[Fact]
24+
public void StjDeserializeTest()
25+
{
26+
var options = new STJ.JsonSerializerOptions
27+
{
28+
IncludeFields = true,
29+
PropertyNamingPolicy = STJ.JsonNamingPolicy.CamelCase,
30+
Converters =
31+
{
32+
new STJ.Serialization.JsonStringEnumConverter(STJ.JsonNamingPolicy.CamelCase)
33+
}
34+
};
35+
var actual = STJ.JsonSerializer.Deserialize<AllInOne>(AllInOne.SampleSerialized, options);
36+
Assert.Equal(AllInOne.Sample, actual);
37+
}
2138
}

0 commit comments

Comments
 (0)