From f633b33cdea3fc3e7178e6761a5bc1cd05fbee4c Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 14 Dec 2024 16:03:36 -0800 Subject: [PATCH] Remove a GVM for deserializing nullable values GVMs (Generic Virtual Methods) can cause large code size increases in AOT. Because all reference types share a single code implementation, generics constrained to class don't have this downside. This change removes a GVM for deserializing nullable types, which includes nullable structs. Instead, deserializing nullable structs will box. --- src/serde/IDeserialize.cs | 32 +- src/serde/Proxies.cs | 36 +- src/serde/json/JsonDeserializer.ReadAny.cs | 236 ++++++++++++ src/serde/json/JsonDeserializer.cs | 424 +++++---------------- 4 files changed, 365 insertions(+), 363 deletions(-) create mode 100644 src/serde/json/JsonDeserializer.ReadAny.cs diff --git a/src/serde/IDeserialize.cs b/src/serde/IDeserialize.cs index f221727..61feeac 100644 --- a/src/serde/IDeserialize.cs +++ b/src/serde/IDeserialize.cs @@ -75,14 +75,6 @@ T VisitDictionary(ref D d) where D : IDeserializeDictionary T VisitNotNull(IDeserializer d) => throw new InvalidOperationException("Expected type " + ExpectedTypeName); } -public interface IDeserializeCollection -{ - int? SizeOpt { get; } - - bool TryReadValue(ISerdeInfo typeInfo, TProxy deserialize, [MaybeNullWhen(false)] out T next) - where TProxy : IDeserialize; -} - public interface IDeserializeEnumerable { bool TryGetNext(TProxy deserialize, [MaybeNullWhen(false)] out T next) @@ -101,6 +93,14 @@ bool TryGetNextEntry(DK dk, DV dv, [MaybeNullWhen(false)] out (K, int? SizeOpt { get; } } +public interface IDeserializeCollection +{ + int? SizeOpt { get; } + + bool TryReadValue(ISerdeInfo typeInfo, TProxy deserialize, [MaybeNullWhen(false)] out T next) + where TProxy : IDeserialize; +} + public interface IDeserializeType { public const int EndOfType = -1; @@ -147,8 +147,20 @@ public static T ReadValue(this IDeserializeType deserializeType, i public interface IDeserializer : IDisposable { - T ReadAny(IDeserializeVisitor v); - T ReadNullableRef(IDeserializeVisitor v); + /// + /// Read a value of any type, as decided by the input. This method can be used when the + /// expected type may vary at runtime, such as when reading a union type. + /// + /// + /// Note that `T` is constrained to `class` to avoid GVM size explosion with AOT. + /// + T ReadAny(IDeserializeVisitor v) + where T : class; + + T? ReadNullableRef(TProxy proxy) + where T : class + where TProxy : IDeserialize; + bool ReadBool(); char ReadChar(); byte ReadByte(); diff --git a/src/serde/Proxies.cs b/src/serde/Proxies.cs index c2c8ec0..215554f 100644 --- a/src/serde/Proxies.cs +++ b/src/serde/Proxies.cs @@ -376,25 +376,18 @@ public class DeserializeInstance(TProxy proxy) : IDeserialize where T : struct where TProxy : IDeserialize { - private readonly Visitor _visitor = new(proxy); + private readonly BoxProxy _boxProxy = new(proxy); public T? Deserialize(IDeserializer deserializer) { - return deserializer.ReadNullableRef(_visitor); + return (T?)deserializer.ReadNullableRef(_boxProxy); } - private sealed class Visitor(TProxy proxy) : IDeserializeVisitor + private sealed class BoxProxy(TProxy underlyingProxy) : IDeserialize { - public string ExpectedTypeName => typeof(T).ToString() + "?"; - - T? IDeserializeVisitor.VisitNull() - { - return null; - } - - T? IDeserializeVisitor.VisitNotNull(IDeserializer d) + public object Deserialize(IDeserializer deserializer) { - return proxy.Deserialize(d); + return underlyingProxy.Deserialize(deserializer); } } } @@ -446,26 +439,9 @@ public class DeserializeInstance(TProxy proxy) : IDeserialize where T : class where TProxy : IDeserialize { - private readonly Visitor _visitor = new(proxy); - public T? Deserialize(IDeserializer deserializer) { - return deserializer.ReadNullableRef(_visitor); - } - - private sealed class Visitor(TProxy proxy) : IDeserializeVisitor - { - public string ExpectedTypeName => typeof(T).ToString() + "?"; - - T? IDeserializeVisitor.VisitNull() - { - return null; - } - - T? IDeserializeVisitor.VisitNotNull(IDeserializer d) - { - return proxy.Deserialize(d); - } + return deserializer.ReadNullableRef(proxy); } } } \ No newline at end of file diff --git a/src/serde/json/JsonDeserializer.ReadAny.cs b/src/serde/json/JsonDeserializer.ReadAny.cs new file mode 100644 index 0000000..d84dd9f --- /dev/null +++ b/src/serde/json/JsonDeserializer.ReadAny.cs @@ -0,0 +1,236 @@ + +using System; +using System.Diagnostics.CodeAnalysis; +using Serde.IO; +using static Serde.Json.ThrowHelpers; + +namespace Serde.Json; + +internal sealed partial class JsonDeserializer : IDeserializer + where TReader : IByteReader +{ + public T ReadAny(IDeserializeVisitor v) + where T : class + { + var peek = Reader.SkipWhitespace(); + T result; + switch (ThrowIfEos(peek)) + { + case (byte)'[': + result = ReadEnumerable(v); + break; + + case (byte)'-' or (>= (byte)'0' and <= (byte)'9'): + var d = ReadDouble(); + result = v.VisitDouble(d); + break; + + case (byte)'{': + result = DeserializeDictionary(v); + break; + + case (byte)'"': + result = VisitString(v); + break; + + case (byte)'n' when Reader.StartsWith("null"u8): + Reader.Advance(4); + result = v.VisitNull(); + break; + + case (byte)'t' when Reader.StartsWith("true"u8): + Reader.Advance(4); + result = v.VisitBool(true); + break; + + case (byte)'f' when Reader.StartsWith("false"u8): + Reader.Advance(5); + result = v.VisitBool(false); + break; + + default: + throw new JsonException($"Could not deserialize '{(char)peek}"); + } + return result; + } + + private T VisitString(IDeserializeVisitor v) + { + var span = ReadUtf8Span(); + return v.VisitUtf8Span(span); + } + + public T? ReadNullableRef(TProxy proxy) + where T : class + where TProxy : IDeserialize + { + var peek = Reader.SkipWhitespace(); + switch (ThrowIfEos(peek)) + { + case (byte)'n' when Reader.StartsWith("null"u8): + Reader.Advance(4); + return null; + default: + return proxy.Deserialize(this); + } + } + + public bool ReadBool() + { + return Reader.GetBoolean(); + } + + public T DeserializeDictionary(IDeserializeVisitor v) + { + var peek = Reader.SkipWhitespace(); + + if (peek != (short)'{') + { + throw new JsonException("Expected object start"); + } + + Reader.Advance(); + var map = new DeDictionary(this); + return v.VisitDictionary(ref map); + } + + /// + /// Expects to be one byte after '[' + /// + private T ReadEnumerable(IDeserializeVisitor v) + { + var peek = Reader.Peek(); + if (peek != (byte)'[') + { + throw new JsonException("Expected array start"); + } + Reader.Advance(); + + var enumerable = new DeEnumerable(this); + return v.VisitEnumerable(ref enumerable); + } + + private struct DeEnumerable : IDeserializeEnumerable + { + private JsonDeserializer _deserializer; + private bool _first = true; + public DeEnumerable(JsonDeserializer de) + { + _deserializer = de; + } + public int? SizeOpt => null; + + public bool TryGetNext(TProxy proxy, [MaybeNullWhen(false)] out T next) + where TProxy : IDeserialize + { + while (true) + { + var peek = _deserializer.Reader.SkipWhitespace(); + if (peek == (short)',') + { + if (_first) + { + throw new JsonException("Unexpected comma before first element"); + } + _deserializer.Reader.Advance(); + peek = _deserializer.Reader.SkipWhitespace(); + } + + switch (peek) + { + case IByteReader.EndOfStream: + throw new JsonException("Unexpected end of stream"); + + case (short)']': + _deserializer.Reader.Advance(); + next = default; + return false; + + default: + _first = false; + next = proxy.Deserialize(_deserializer); + return true; + } + } + } + } + + private struct DeDictionary : IDeserializeDictionary + { + private JsonDeserializer _deserializer; + private bool _first = true; + public DeDictionary(JsonDeserializer de) + { + _deserializer = de; + } + + public int? SizeOpt => null; + + public bool TryGetNextEntry(DK dk, DV dv, [MaybeNullWhen(false)] out (K, V) next) + where DK : IDeserialize + where DV : IDeserialize + { + // Don't save state + if (!TryGetNextKey(dk, out K? nextKey)) + { + next = default; + return false; + } + var nextValue = GetNextValue(dv); + next = (nextKey, nextValue); + return true; + } + + public bool TryGetNextKey(D d, [MaybeNullWhen(false)] out K next) + where D : IDeserialize + { + while (true) + { + var peek = _deserializer.Reader.SkipWhitespace(); + + if (peek == (short)',') + { + if (_first) + { + throw new JsonException("Unexpected comma before first element"); + } + _deserializer.Reader.Advance(); + peek = _deserializer.Reader.SkipWhitespace(); + } + + switch (peek) + { + case IByteReader.EndOfStream: + throw new JsonException("Unexpected end of stream"); + + case (short)'}': + // Check if the next token is the end of the object, but don't advance the stream if not + _deserializer.Reader.Advance(); + next = default; + _first = false; + return false; + + case (short)'"': + next = d.Deserialize(_deserializer); + _first = false; + return true; + + default: + throw new JsonException("Expected property name, found: " + (char)peek); + } + } + } + + public V GetNextValue(D d) where D : IDeserialize + { + var peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); + if (peek != (byte)':') + { + throw new JsonException("Expected ':'"); + } + _deserializer.Reader.Advance(); + return d.Deserialize(_deserializer); + } + } + +} \ No newline at end of file diff --git a/src/serde/json/JsonDeserializer.cs b/src/serde/json/JsonDeserializer.cs index 9947079..559af56 100644 --- a/src/serde/json/JsonDeserializer.cs +++ b/src/serde/json/JsonDeserializer.cs @@ -47,190 +47,6 @@ internal JsonDeserializer(TReader byteReader) _scratch = new ScratchBuffer(); } - public T ReadAny(IDeserializeVisitor v) - { - var peek = Reader.SkipWhitespace(); - T result; - switch (ThrowIfEos(peek)) - { - case (byte)'[': - result = ReadEnumerable(v); - break; - - case (byte)'-' or (>= (byte)'0' and <= (byte)'9'): - var d = ReadDouble(); - result = v.VisitDouble(d); - break; - - case (byte)'{': - result = DeserializeDictionary(v); - break; - - case (byte)'"': - result = VisitString(v); - break; - - case (byte)'n' when Reader.StartsWith("null"u8): - Reader.Advance(4); - result = v.VisitNull(); - break; - - case (byte)'t' when Reader.StartsWith("true"u8): - Reader.Advance(4); - result = v.VisitBool(true); - break; - - case (byte)'f' when Reader.StartsWith("false"u8): - Reader.Advance(5); - result = v.VisitBool(false); - break; - - default: - throw new JsonException($"Could not deserialize '{(char)peek}"); - } - return result; - } - - private T VisitString(IDeserializeVisitor v) - { - var span = ReadUtf8Span(); - return v.VisitUtf8Span(span); - } - - public T ReadNullableRef(IDeserializeVisitor v) - { - var peek = Reader.SkipWhitespace(); - switch (ThrowIfEos(peek)) - { - case (byte)'n' when Reader.StartsWith("null"u8): - Reader.Advance(4); - return v.VisitNull(); - default: - return v.VisitNotNull(this); - } - } - - - public bool ReadBool() - { - return Reader.GetBoolean(); - } - - public T DeserializeDictionary(IDeserializeVisitor v) - { - var peek = Reader.SkipWhitespace(); - - if (peek != (short)'{') - { - throw new JsonException("Expected object start"); - } - - Reader.Advance(); - var map = new DeDictionary(this); - return v.VisitDictionary(ref map); - } - - public IDeserializeCollection ReadCollection(ISerdeInfo typeInfo) - { - var kind = typeInfo.Kind; - if (kind is not (InfoKind.Enumerable or InfoKind.Dictionary)) - { - throw new ArgumentException($"TypeKind is {typeInfo.Kind}, expected Enumerable or Dictionary"); - } - switch ((ThrowIfEos(Reader.SkipWhitespace()), kind)) - { - case ((byte)'[', InfoKind.Enumerable): - case ((byte)'{', InfoKind.Dictionary): - Reader.Advance(); - break; - case (_, InfoKind.Enumerable): - throw new JsonException("Expected array start"); - case (_, InfoKind.Dictionary): - throw new JsonException("Expected object start"); - } - - return new DeCollection(this); - } - - private struct DeCollection : IDeserializeCollection - { - private JsonDeserializer _deserializer; - private bool _first = true; - private bool _afterKey = false; - - public DeCollection(JsonDeserializer de) - { - _deserializer = de; - } - - public int? SizeOpt => null; - - public bool TryReadValue(ISerdeInfo typeInfo, TProxy d, [MaybeNullWhen(false)] out T next) - where TProxy : IDeserialize - { - var peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); - if (peek == (short)',') - { - if (_first) - { - throw new JsonException("Unexpected comma before first element"); - } - if (_afterKey) - { - throw new JsonException("Unexpected comma after key"); - } - _deserializer.Reader.Advance(); - peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); - } - - if (_afterKey && peek != (short)':' && typeInfo.Kind == InfoKind.Dictionary) - { - throw new JsonException("Expected ':' after key"); - } - - if (peek == (short)':') - { - if (typeInfo.Kind == InfoKind.Enumerable) - { - throw new JsonException("Unexpected ':' in array"); - } - if (_first || !_afterKey) - { - throw new JsonException("Unexpected ':' before key"); - } - _deserializer.Reader.Advance(); - peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); - } - - switch (peek) - { - case (byte)']' when typeInfo.Kind == InfoKind.Enumerable: - _deserializer.Reader.Advance(); - next = default; - return false; - - case (byte)'}': - if (typeInfo.Kind != InfoKind.Dictionary) - { - throw new JsonException("Unexpected '}' in array"); - } - if (_afterKey) - { - throw new JsonException("Expected object value, found '}'"); - } - _deserializer.Reader.Advance(); - next = default; - return false; - - default: - next = d.Deserialize(_deserializer); - _first = false; - _afterKey = typeInfo.Kind == InfoKind.Dictionary && !_afterKey; - return true; - } - } - } - public float ReadFloat() => Convert.ToSingle(ReadDouble()); public double ReadDouble() @@ -247,145 +63,6 @@ public decimal ReadDecimal() return Reader.GetDecimal(_scratch); } - /// - /// Expects to be one byte after '[' - /// - private T ReadEnumerable(IDeserializeVisitor v) - { - var peek = Reader.Peek(); - if (peek != (byte)'[') - { - throw new JsonException("Expected array start"); - } - Reader.Advance(); - - var enumerable = new DeEnumerable(this); - return v.VisitEnumerable(ref enumerable); - } - - private struct DeEnumerable : IDeserializeEnumerable - { - private JsonDeserializer _deserializer; - private bool _first = true; - public DeEnumerable(JsonDeserializer de) - { - _deserializer = de; - } - public int? SizeOpt => null; - - public bool TryGetNext(TProxy proxy, [MaybeNullWhen(false)] out T next) - where TProxy : IDeserialize - { - while (true) - { - var peek = _deserializer.Reader.SkipWhitespace(); - if (peek == (short)',') - { - if (_first) - { - throw new JsonException("Unexpected comma before first element"); - } - _deserializer.Reader.Advance(); - peek = _deserializer.Reader.SkipWhitespace(); - } - - switch (peek) - { - case IByteReader.EndOfStream: - throw new JsonException("Unexpected end of stream"); - - case (short)']': - _deserializer.Reader.Advance(); - next = default; - return false; - - default: - _first = false; - next = proxy.Deserialize(_deserializer); - return true; - } - } - } - } - - private struct DeDictionary : IDeserializeDictionary - { - private JsonDeserializer _deserializer; - private bool _first = true; - public DeDictionary(JsonDeserializer de) - { - _deserializer = de; - } - - public int? SizeOpt => null; - - public bool TryGetNextEntry(DK dk, DV dv, [MaybeNullWhen(false)] out (K, V) next) - where DK : IDeserialize - where DV : IDeserialize - { - // Don't save state - if (!TryGetNextKey(dk, out K? nextKey)) - { - next = default; - return false; - } - var nextValue = GetNextValue(dv); - next = (nextKey, nextValue); - return true; - } - - public bool TryGetNextKey(D d, [MaybeNullWhen(false)] out K next) - where D : IDeserialize - { - while (true) - { - var peek = _deserializer.Reader.SkipWhitespace(); - - if (peek == (short)',') - { - if (_first) - { - throw new JsonException("Unexpected comma before first element"); - } - _deserializer.Reader.Advance(); - peek = _deserializer.Reader.SkipWhitespace(); - } - - switch (peek) - { - case IByteReader.EndOfStream: - throw new JsonException("Unexpected end of stream"); - - case (short)'}': - // Check if the next token is the end of the object, but don't advance the stream if not - _deserializer.Reader.Advance(); - next = default; - _first = false; - return false; - - case (short)'"': - next = d.Deserialize(_deserializer); - _first = false; - return true; - - default: - throw new JsonException("Expected property name, found: " + (char)peek); - } - } - } - - public V GetNextValue(D d) where D : IDeserialize - { - var peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); - if (peek != (byte)':') - { - throw new JsonException("Expected ':'"); - } - _deserializer.Reader.Advance(); - return d.Deserialize(_deserializer); - } - } - public sbyte ReadSByte() => Convert.ToSByte(ReadI64()); public short ReadI16() => Convert.ToInt16(ReadI64()); @@ -563,6 +240,107 @@ void IDeserializeType.SkipValue() Reader.Skip(); } + public IDeserializeCollection ReadCollection(ISerdeInfo typeInfo) + { + var kind = typeInfo.Kind; + if (kind is not (InfoKind.Enumerable or InfoKind.Dictionary)) + { + throw new ArgumentException($"TypeKind is {typeInfo.Kind}, expected Enumerable or Dictionary"); + } + switch ((ThrowIfEos(Reader.SkipWhitespace()), kind)) + { + case ((byte)'[', InfoKind.Enumerable): + case ((byte)'{', InfoKind.Dictionary): + Reader.Advance(); + break; + case (_, InfoKind.Enumerable): + throw new JsonException("Expected array start"); + case (_, InfoKind.Dictionary): + throw new JsonException("Expected object start"); + } + + return new DeCollection(this); + } + + private struct DeCollection : IDeserializeCollection + { + private JsonDeserializer _deserializer; + private bool _first = true; + private bool _afterKey = false; + + public DeCollection(JsonDeserializer de) + { + _deserializer = de; + } + + public int? SizeOpt => null; + + public bool TryReadValue(ISerdeInfo typeInfo, TProxy d, [MaybeNullWhen(false)] out T next) + where TProxy : IDeserialize + { + var peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); + if (peek == (short)',') + { + if (_first) + { + throw new JsonException("Unexpected comma before first element"); + } + if (_afterKey) + { + throw new JsonException("Unexpected comma after key"); + } + _deserializer.Reader.Advance(); + peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); + } + + if (_afterKey && peek != (short)':' && typeInfo.Kind == InfoKind.Dictionary) + { + throw new JsonException("Expected ':' after key"); + } + + if (peek == (short)':') + { + if (typeInfo.Kind == InfoKind.Enumerable) + { + throw new JsonException("Unexpected ':' in array"); + } + if (_first || !_afterKey) + { + throw new JsonException("Unexpected ':' before key"); + } + _deserializer.Reader.Advance(); + peek = ThrowIfEos(_deserializer.Reader.SkipWhitespace()); + } + + switch (peek) + { + case (byte)']' when typeInfo.Kind == InfoKind.Enumerable: + _deserializer.Reader.Advance(); + next = default; + return false; + + case (byte)'}': + if (typeInfo.Kind != InfoKind.Dictionary) + { + throw new JsonException("Unexpected '}' in array"); + } + if (_afterKey) + { + throw new JsonException("Expected object value, found '}'"); + } + _deserializer.Reader.Advance(); + next = default; + return false; + + default: + next = d.Deserialize(_deserializer); + _first = false; + _afterKey = typeInfo.Kind == InfoKind.Dictionary && !_afterKey; + return true; + } + } + } + int IDeserializeType.TryReadIndex(ISerdeInfo serdeInfo, out string? errorName) { if (serdeInfo.Kind == InfoKind.Enum)