diff --git a/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs b/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs index 24226e37..aff60ebc 100644 --- a/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs +++ b/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs @@ -30,5 +30,5 @@ public interface IVariableSizeSerializer<T> : ISerializer /// </summary> public interface IGenericSerializer : ISerializer { - public bool TrySpecialze(Type baseType, Type[] argTypes, [NotNullWhen(true)] out ISerializer? serializer); + public bool TrySpecialze(Type baseType, Type[] argTypes, Func<Type, ISerializer> serializerFinder, [NotNullWhen(true)] out ISerializer? serializer); } diff --git a/src/NexusMods.EventSourcing/NexusMods.EventSourcing.csproj b/src/NexusMods.EventSourcing/NexusMods.EventSourcing.csproj index bedb8716..8b66b735 100644 --- a/src/NexusMods.EventSourcing/NexusMods.EventSourcing.csproj +++ b/src/NexusMods.EventSourcing/NexusMods.EventSourcing.csproj @@ -5,6 +5,7 @@ <ItemGroup> <PackageReference Include="DynamicData" Version="8.3.27" /> <PackageReference Include="MemoryPack" Version="1.10.0" /> + <PackageReference Include="Reloaded.Memory" Version="9.3.2" /> <PackageReference Include="System.Reactive" Version="6.0.0" /> <PackageReference Include="TransparentValueObjects" PrivateAssets="all" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> </ItemGroup> diff --git a/src/NexusMods.EventSourcing/Serialization/ArraySerializer.cs b/src/NexusMods.EventSourcing/Serialization/ArraySerializer.cs new file mode 100644 index 00000000..10d44ca6 --- /dev/null +++ b/src/NexusMods.EventSourcing/Serialization/ArraySerializer.cs @@ -0,0 +1,147 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using NexusMods.EventSourcing.Abstractions.Serialization; +using Reloaded.Memory.Extensions; + +namespace NexusMods.EventSourcing.Serialization; + +public class GenericArraySerializer : IGenericSerializer +{ + public bool CanSerialize(Type valueType) + { + return false; + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 0; + return false; + } + + public bool TrySpecialze(Type baseType, Type[] argTypes, Func<Type, ISerializer> serializerFinder, [NotNullWhen(true)] out ISerializer? serializer) + { + if (!baseType.IsArray) + { + serializer = null; + return false; + } + + var itemType = baseType.GetElementType()!; + var itemSerializer = serializerFinder(itemType); + + if (itemSerializer.TryGetFixedSize(itemType, out var itemSize)) + { + var type = typeof(FixedItemSizeArraySerializer<,>).MakeGenericType(itemType, itemSerializer.GetType()); + serializer = (ISerializer) Activator.CreateInstance(type, itemSerializer, itemSize)!; + return true; + } + else + { + var type = typeof(VariableItemSizeSerializer<,>).MakeGenericType(itemType, itemSerializer.GetType()); + serializer = (ISerializer) Activator.CreateInstance(type, itemSerializer, itemSize)!; + return true; + } + } +} + + +public class FixedItemSizeArraySerializer<TItem, TItemSerializer>(TItemSerializer itemSerializer, int itemSize) : IVariableSizeSerializer<TItem[]> + where TItemSerializer : IFixedSizeSerializer<TItem> +{ + public bool CanSerialize(Type valueType) + { + if (!valueType.IsArray) + { + return false; + } + + return itemSerializer.CanSerialize(valueType.GetElementType()!); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 0; + return false; + } + + public void Serialize<TWriter>(TItem[] value, TWriter output) where TWriter : IBufferWriter<byte> + { + var totalSize = sizeof(ushort) + (itemSize * value.Length); + var span = output.GetSpan(totalSize); + BinaryPrimitives.WriteUInt32BigEndian(span, (ushort)value.Length); + + foreach (var item in value) + { + itemSerializer.Serialize(item, span.SliceFast(itemSize, itemSize)); + } + output.Advance(totalSize); + } + + public int Deserialize(ReadOnlySpan<byte> from, out TItem[] value) + { + var size = BinaryPrimitives.ReadUInt16BigEndian(from); + var array = GC.AllocateUninitializedArray<TItem>(size); + + from = from.SliceFast(sizeof(ushort)); + for (var i = 0; i < size; i++) + { + array[i] = itemSerializer.Deserialize(from.SliceFast(i * itemSize, itemSize)); + } + + value = array; + return sizeof(ushort) + (itemSize * size); + } +} + + +public class VariableItemSizeSerializer<TItem, TItemSerializer>(TItemSerializer itemSerializer, int itemSize) : IVariableSizeSerializer<TItem[]> + where TItemSerializer : IVariableSizeSerializer<TItem> +{ + public bool CanSerialize(Type valueType) + { + if (!valueType.IsArray) + { + return false; + } + + return itemSerializer.CanSerialize(valueType.GetElementType()!); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 0; + return false; + } + + public void Serialize<TWriter>(TItem[] value, TWriter output) where TWriter : IBufferWriter<byte> + { + var totalSize = sizeof(ushort) + (itemSize * value.Length); + var span = output.GetSpan(totalSize); + BinaryPrimitives.WriteUInt32BigEndian(span, (ushort)value.Length); + + foreach (var item in value) + { + itemSerializer.Serialize(item, output); + } + output.Advance(totalSize); + } + + public int Deserialize(ReadOnlySpan<byte> from, out TItem[] value) + { + var size = BinaryPrimitives.ReadUInt16BigEndian(from); + var array = GC.AllocateUninitializedArray<TItem>(size); + + from = from.SliceFast(sizeof(ushort)); + var offset = sizeof(ushort); + for (var i = 0; i < size; i++) + { + offset += itemSerializer.Deserialize(from.SliceFast(offset), out var item); + array[i] = item; + } + + value = array; + return offset; + } +} diff --git a/src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs b/src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs index c5f93794..31babdd6 100644 --- a/src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs +++ b/src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs @@ -43,7 +43,7 @@ public bool TryGetFixedSize(Type valueType, out int size) return false; } - public bool TrySpecialze(Type baseType, Type[] argTypes, [NotNullWhen(true)] out ISerializer? serializer) + public bool TrySpecialze(Type baseType, Type[] argTypes, Func<Type, ISerializer> serializerFinder, [NotNullWhen(true)] out ISerializer? serializer) { if (baseType != typeof(EntityId<>) || argTypes.Length != 1) { diff --git a/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs b/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs index ed51a5ab..d28a1c2b 100644 --- a/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs +++ b/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs @@ -15,21 +15,19 @@ namespace NexusMods.EventSourcing.Serialization; using MemberDefinition = (ParameterInfo Ref, ParameterInfo Base, ParameterExpression Variable, ISerializer Serializer); -public sealed class BinaryEventSerializer : IEventSerializer +public sealed class BinaryEventSerializer : IEventSerializer, IVariableSizeSerializer<IEvent> { private readonly PooledMemoryBufferWriter _writer; private readonly Dictionary<Type, EventSerializerDelegate> _serializerDelegates = new(); private readonly Dictionary<UInt128, EventDeserializerDelegate> _deserializerDelegates = new(); - private static readonly GuidSerializer _guidSerializer = new(); - /// <summary> /// Write an event to the given writer, and return the /// </summary> internal delegate void EventSerializerDelegate(IEvent @event); - private delegate IEvent EventDeserializerDelegate(ReadOnlySpan<byte> data); + private delegate int EventDeserializerDelegate(ReadOnlySpan<byte> data, out IEvent @event); public BinaryEventSerializer(IEnumerable<ISerializer> diInjectedSerializers, IEnumerable<EventDefinition> eventDefinitions) { @@ -58,7 +56,8 @@ public ReadOnlySpan<byte> Serialize(IEvent @event) public IEvent Deserialize(ReadOnlySpan<byte> data) { var id = BinaryPrimitives.ReadUInt128BigEndian(data); - return _deserializerDelegates[id](SliceFastStart(data, 16)); + var used = _deserializerDelegates[id](SliceFastStart(data, 16), out var @event); + return @event; } @@ -165,6 +164,9 @@ private EventSerializerDelegate BuildVariableSizeSerializer(EventDefinition even private EventDeserializerDelegate BuildVariableSizeDeserializer(EventDefinition definition, MemberDefinition[] allParams, List<MemberDefinition> fixedParams, int fixedSize, List<MemberDefinition> unfixedParams) { + + var outParam = Expression.Parameter(typeof(IEvent).MakeByRefType(), "output"); + var spanParam = Expression.Parameter(typeof(ReadOnlySpan<byte>)); var ctorExpressions = new List<Expression>(); @@ -204,16 +206,22 @@ private EventDeserializerDelegate BuildVariableSizeDeserializer(EventDefinition var ctorCall = Expression.New(definition.Type.GetConstructors().First(c => c.GetParameters().Length == ctorParams.Length), ctorParams); - var casted = Expression.Convert(ctorCall, typeof(IEvent)); + var casted = Expression.Assign(outParam, Expression.Convert(ctorCall, typeof(IEvent))); blockExprs.Add(casted); + blockExprs.Add(offsetVariable); var outerBlock = Expression.Block(ctorParams.Append(offsetVariable), blockExprs); - var lambda = Expression.Lambda<EventDeserializerDelegate>(outerBlock, spanParam); + var lambda = Expression.Lambda<EventDeserializerDelegate>(outerBlock, [spanParam, outParam]); return lambda.Compile(); } private ISerializer GetSerializer(ISerializer[] serializers, Type type) { + if (type == typeof(IEvent)) + { + return this; + } + var result = serializers.FirstOrDefault(s => s.CanSerialize(type)); if (result != null) { @@ -225,18 +233,28 @@ private ISerializer GetSerializer(ISerializer[] serializers, Type type) var genericMakers = serializers.OfType<IGenericSerializer>(); foreach (var maker in genericMakers) { - if (maker.TrySpecialze(type.GetGenericTypeDefinition(), type.GetGenericArguments(), out var serializer)) + if (maker.TrySpecialze(type.GetGenericTypeDefinition(), + type.GetGenericArguments(), t => GetSerializer(serializers, t), out var serializer)) { return serializer; } } } + if (type.IsArray) + { + var arrayMaker = serializers.OfType<GenericArraySerializer>().First(); + arrayMaker.TrySpecialze(type, [type.GetElementType()!], t => GetSerializer(serializers, t), out var serializer); + return serializer!; + } + throw new Exception($"No serializer found for {type}"); } private EventDeserializerDelegate BuildFixedSizeDeserializer(EventDefinition definitions, MemberDefinition[] allDefinitions, List<MemberDefinition> fixedParams, int fixedSize) { + var outParam = Expression.Parameter(typeof(IEvent).MakeByRefType()); + var spanParam = Expression.Parameter(typeof(ReadOnlySpan<byte>)); var blockExprs = new List<Expression>(); @@ -256,11 +274,13 @@ private EventDeserializerDelegate BuildFixedSizeDeserializer(EventDefinition def var ctorCall = Expression.New(definitions.Type.GetConstructors().First(c => c.GetParameters().Length == allDefinitions.Length), allDefinitions.Select(d => d.Variable)); - var casted = Expression.Convert(ctorCall, typeof(IEvent)); + var casted = Expression.Assign(outParam, Expression.Convert(ctorCall, typeof(IEvent))); blockExprs.Add(casted); + blockExprs.Add(Expression.Constant(fixedSize)); + var outerBlock = Expression.Block(allDefinitions.Select(d => d.Variable), blockExprs); - var lambda = Expression.Lambda<EventDeserializerDelegate>(outerBlock, spanParam); + var lambda = Expression.Lambda<EventDeserializerDelegate>(outerBlock, [spanParam, outParam]); return lambda.Compile(); } @@ -406,4 +426,25 @@ private EventSerializerDelegate BuildFixedSizeSerializer(EventDefinition eventDe } + public bool CanSerialize(Type valueType) + { + return valueType == typeof(IEvent); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 0; + return false; + } + + public void Serialize<TWriter>(IEvent value, TWriter output) where TWriter : IBufferWriter<byte> + { + _serializerDelegates[value.GetType()](value); + } + + public int Deserialize(ReadOnlySpan<byte> from, out IEvent value) + { + var used = _deserializerDelegates[BinaryPrimitives.ReadUInt128BigEndian(from)](SliceFastStart(from, 16), out value); + return 16 + used; + } } diff --git a/src/NexusMods.EventSourcing/Services.cs b/src/NexusMods.EventSourcing/Services.cs index 2e7c8bba..053fb8bf 100644 --- a/src/NexusMods.EventSourcing/Services.cs +++ b/src/NexusMods.EventSourcing/Services.cs @@ -11,6 +11,7 @@ public static class Services public static IServiceCollection AddEventSourcing(this IServiceCollection services) { return services + .AddSingleton<ISerializer, GenericArraySerializer>() .AddSingleton<ISerializer, GenericEntityIdSerializer>() .AddSingleton<ISerializer, StringSerializer>() .AddSingleton<ISerializer, BoolSerializer>()