From 55951440969b8cb2a8253a962bea88d71446a2b7 Mon Sep 17 00:00:00 2001 From: halgari Date: Tue, 9 Jan 2024 15:06:46 -0700 Subject: [PATCH] Added variable sized serializers --- .../DependencyInjectionExtensions.cs | 8 +- .../EntityId.cs | 25 +- .../EventDefinition.cs | 4 +- .../Serialization/ISerializer.cs | 19 +- .../RocksDBEventStore.cs | 6 +- src/NexusMods.EventSourcing/EventFormatter.cs | 13 +- .../Serialization/BoolSerializer.cs | 28 ++ .../Serialization/EntityIdSerializer.cs | 82 ++++++ .../Serialization/EventSerializer.cs | 272 ++++++++++++++++-- .../Serialization/GuidSerializer.cs | 28 ++ .../Serialization/StringSerializer.cs | 36 +++ .../Serialization/UInt32Serializer.cs | 4 +- .../Serialization/UInt8Serializer.cs | 2 +- src/NexusMods.EventSourcing/Services.cs | 4 + .../Events/SwapModEnabled.cs | 3 +- .../SerializationTests.cs | 5 + 16 files changed, 496 insertions(+), 43 deletions(-) create mode 100644 src/NexusMods.EventSourcing/Serialization/BoolSerializer.cs create mode 100644 src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs create mode 100644 src/NexusMods.EventSourcing/Serialization/GuidSerializer.cs create mode 100644 src/NexusMods.EventSourcing/Serialization/StringSerializer.cs diff --git a/src/NexusMods.EventSourcing.Abstractions/DependencyInjectionExtensions.cs b/src/NexusMods.EventSourcing.Abstractions/DependencyInjectionExtensions.cs index ac697e83..35e71bce 100644 --- a/src/NexusMods.EventSourcing.Abstractions/DependencyInjectionExtensions.cs +++ b/src/NexusMods.EventSourcing.Abstractions/DependencyInjectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Binary; using System.Reflection; using Microsoft.Extensions.DependencyInjection; @@ -23,7 +24,12 @@ public static IServiceCollection AddEvent(this IServiceCollection collection) { throw new ArgumentException($"Event type {type.Name} does not have an EventIdAttribute."); } - collection.AddSingleton(s => new EventDefinition(attribute.Guid, type)); + + Span span = stackalloc byte[16]; + attribute.Guid.TryWriteBytes(span); + var id = BinaryPrimitives.ReadUInt128BigEndian(span); + + collection.AddSingleton(s => new EventDefinition(id, type)); return collection; } diff --git a/src/NexusMods.EventSourcing.Abstractions/EntityId.cs b/src/NexusMods.EventSourcing.Abstractions/EntityId.cs index f470d01c..8b696074 100644 --- a/src/NexusMods.EventSourcing.Abstractions/EntityId.cs +++ b/src/NexusMods.EventSourcing.Abstractions/EntityId.cs @@ -1,13 +1,30 @@ using System; +using System.Buffers.Binary; +using System.Globalization; using TransparentValueObjects; namespace NexusMods.EventSourcing.Abstractions; -[ValueObject] +[ValueObject] public readonly partial struct EntityId { public EntityId Cast() where T : IEntity => new(this); + public static EntityId NewId() + { + var guid = Guid.NewGuid(); + Span bytes = stackalloc byte[16]; + guid.TryWriteBytes(bytes); + var value = BinaryPrimitives.ReadUInt128BigEndian(bytes); + return From(value); + } + + public static EntityId From(ReadOnlySpan data) => new(BinaryPrimitives.ReadUInt128BigEndian(data)); + + public void TryWriteBytes(Span span) + { + BinaryPrimitives.WriteUInt128BigEndian(span, Value); + } } @@ -29,7 +46,7 @@ public readonly partial struct EntityId /// /// /// - public static EntityId From(Guid id) => new(EntityId.From(id)); + public static EntityId From(UInt128 id) => new(EntityId.From(id)); @@ -38,7 +55,7 @@ public readonly partial struct EntityId /// /// /// - public static EntityId From(string id) => From(Guid.Parse(id)); + public static EntityId From(string id) => From(UInt128.Parse(id, NumberStyles.HexNumber)); /// @@ -55,7 +72,7 @@ public readonly partial struct EntityId /// public override string ToString() { - return typeof(T).Name + "<" + Value.Value + ">"; + return typeof(T).Name + "<" + Value.Value.ToString("X") + ">"; } diff --git a/src/NexusMods.EventSourcing.Abstractions/EventDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/EventDefinition.cs index d949c6cc..4e9a4b5b 100644 --- a/src/NexusMods.EventSourcing.Abstractions/EventDefinition.cs +++ b/src/NexusMods.EventSourcing.Abstractions/EventDefinition.cs @@ -5,6 +5,6 @@ namespace NexusMods.EventSourcing.Abstractions; /// /// A record that defines an event and the unique GUID that identifies it. /// -/// +/// /// -public record EventDefinition(Guid Guid, Type Type); +public record EventDefinition(UInt128 Id, Type Type); diff --git a/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs b/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs index 34db8855..24226e37 100644 --- a/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs +++ b/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializer.cs @@ -1,4 +1,6 @@ using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace NexusMods.EventSourcing.Abstractions.Serialization; @@ -13,5 +15,20 @@ public interface ISerializer public interface IFixedSizeSerializer : ISerializer { public void Serialize(T value, Span output); - public T Deserialize(Span from); + public T Deserialize(ReadOnlySpan from); +} + +public interface IVariableSizeSerializer : ISerializer +{ + public void Serialize(T value, TWriter output) where TWriter : IBufferWriter; + public int Deserialize(ReadOnlySpan from, out T value); +} + + +/// +/// If the serializer can specialize (e.g. for a generic type), it should implement this interface. +/// +public interface IGenericSerializer : ISerializer +{ + public bool TrySpecialze(Type baseType, Type[] argTypes, [NotNullWhen(true)] out ISerializer? serializer); } diff --git a/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs b/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs index 8870f33f..f2889815 100644 --- a/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs +++ b/src/NexusMods.EventSourcing.RocksDB/RocksDBEventStore.cs @@ -57,7 +57,7 @@ public TransactionId Add(T eventValue) where T : IEvent BinaryPrimitives.WriteUInt64BigEndian(keySpan[16..], _tx.Value); foreach (var entityId in ingester.Entities) { - entityId.Value.TryWriteBytes(keySpan); + entityId.TryWriteBytes(keySpan); _db.Put(keySpan, keySpan, _entityIndexColumn); } } @@ -68,9 +68,9 @@ public TransactionId Add(T eventValue) where T : IEvent public void EventsForEntity(EntityId entityId, TIngester ingester) where TIngester : IEventIngester { Span startKey = stackalloc byte[24]; - entityId.Value.TryWriteBytes(startKey); + entityId.TryWriteBytes(startKey); Span endKey = stackalloc byte[24]; - entityId.Value.TryWriteBytes(endKey); + entityId.TryWriteBytes(endKey); BinaryPrimitives.WriteUInt64BigEndian(endKey[16..], ulong.MaxValue); var options = new ReadOptions(); diff --git a/src/NexusMods.EventSourcing/EventFormatter.cs b/src/NexusMods.EventSourcing/EventFormatter.cs index 92f1413c..fe00f3b0 100644 --- a/src/NexusMods.EventSourcing/EventFormatter.cs +++ b/src/NexusMods.EventSourcing/EventFormatter.cs @@ -10,14 +10,14 @@ namespace NexusMods.EventSourcing; internal class EventFormatter : MemoryPackFormatter { private static Guid _zeroGuid = Guid.Empty; - private readonly Dictionary _eventByGuid; - private readonly Dictionary _eventsByType; + private readonly Dictionary _eventByGuid; + private readonly Dictionary _eventsByType; public EventFormatter(IEnumerable events) { var eventsArray = events.ToArray(); - _eventByGuid = eventsArray.ToDictionary(e => e.Guid, e => e.Type); - _eventsByType = eventsArray.ToDictionary(e => e.Type, e => e.Guid); + _eventByGuid = eventsArray.ToDictionary(e => e.Id, e => e.Type); + _eventsByType = eventsArray.ToDictionary(e => e.Type, e => e.Id); } public override void Serialize(ref MemoryPackWriter writer, scoped ref IEvent? value) @@ -35,7 +35,9 @@ public override void Serialize(ref MemoryPackWriter(); + throw new NotImplementedException(); + /* + var readValue = reader.ReadValue(); if (readValue == _zeroGuid) { value = null; @@ -43,5 +45,6 @@ public override void Deserialize(ref MemoryPackReader reader, scoped ref IEvent? } var mappedType = _eventByGuid[readValue]; value = (IEvent)reader.ReadValue(mappedType)!; + */ } } diff --git a/src/NexusMods.EventSourcing/Serialization/BoolSerializer.cs b/src/NexusMods.EventSourcing/Serialization/BoolSerializer.cs new file mode 100644 index 00000000..dcd097a6 --- /dev/null +++ b/src/NexusMods.EventSourcing/Serialization/BoolSerializer.cs @@ -0,0 +1,28 @@ +using System; +using NexusMods.EventSourcing.Abstractions.Serialization; + +namespace NexusMods.EventSourcing.Serialization; + +public class BoolSerializer : IFixedSizeSerializer +{ + public bool CanSerialize(Type valueType) + { + return valueType == typeof(bool); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = sizeof(bool); + return valueType == typeof(bool); + } + + public void Serialize(bool value, Span output) + { + output[0] = value ? (byte)1 : (byte)0; + } + + public bool Deserialize(ReadOnlySpan from) + { + return from[0] == 1; + } +} diff --git a/src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs b/src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs new file mode 100644 index 00000000..c5f93794 --- /dev/null +++ b/src/NexusMods.EventSourcing/Serialization/EntityIdSerializer.cs @@ -0,0 +1,82 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using NexusMods.EventSourcing.Abstractions; +using NexusMods.EventSourcing.Abstractions.Serialization; + +namespace NexusMods.EventSourcing.Serialization; + +public class EntityIdSerializer : IFixedSizeSerializer +{ + public bool CanSerialize(Type valueType) + { + return valueType == typeof(EntityId); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 16; + return true; + } + + public void Serialize(EntityId value, Span output) + { + value.TryWriteBytes(output); + } + + public EntityId Deserialize(ReadOnlySpan from) + { + return EntityId.From(from); + } +} + +public class GenericEntityIdSerializer : 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, [NotNullWhen(true)] out ISerializer? serializer) + { + if (baseType != typeof(EntityId<>) || argTypes.Length != 1) + { + serializer = null; + return false; + } + + var type = typeof(EntityIdSerializer<>).MakeGenericType(argTypes[0]); + serializer = (ISerializer) Activator.CreateInstance(type)!; + return true; + } +} + +internal class EntityIdSerializer : IFixedSizeSerializer> where T : IEntity +{ + public bool CanSerialize(Type valueType) + { + return valueType == typeof(EntityId); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 16; + return true; + } + + public void Serialize(EntityId value, Span output) + { + value.Value.TryWriteBytes(output); + } + + public EntityId Deserialize(ReadOnlySpan from) + { + return EntityId.From(BinaryPrimitives.ReadUInt64BigEndian(from)); + } +} diff --git a/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs b/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs index 3687307b..ed51a5ab 100644 --- a/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs +++ b/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -19,23 +20,30 @@ public sealed class BinaryEventSerializer : IEventSerializer private readonly PooledMemoryBufferWriter _writer; private readonly Dictionary _serializerDelegates = new(); + private readonly Dictionary _deserializerDelegates = new(); + + private static readonly GuidSerializer _guidSerializer = new(); /// /// Write an event to the given writer, and return the /// internal delegate void EventSerializerDelegate(IEvent @event); + private delegate IEvent EventDeserializerDelegate(ReadOnlySpan data); + public BinaryEventSerializer(IEnumerable diInjectedSerializers, IEnumerable eventDefinitions) { _writer = new PooledMemoryBufferWriter(); - PopulateSerializers(diInjectedSerializers.ToArray(), eventDefinitions.Where(t => t.Type.Name == "SimpleTestEvent").ToArray()); + PopulateSerializers(diInjectedSerializers.ToArray(), eventDefinitions.ToArray()); } private void PopulateSerializers(ISerializer[] diInjectedSerializers, EventDefinition[] eventDefinitions) { foreach (var eventDefinition in eventDefinitions) { - _serializerDelegates[eventDefinition.Type] = MakeSerializer(eventDefinition, diInjectedSerializers); + var (serializer, deserializer) = MakeSerializer(eventDefinition, diInjectedSerializers); + _serializerDelegates[eventDefinition.Type] = serializer; + _deserializerDelegates[eventDefinition.Id] = deserializer; } } @@ -49,11 +57,12 @@ public ReadOnlySpan Serialize(IEvent @event) public IEvent Deserialize(ReadOnlySpan data) { - throw new NotImplementedException(); + var id = BinaryPrimitives.ReadUInt128BigEndian(data); + return _deserializerDelegates[id](SliceFastStart(data, 16)); } - private EventSerializerDelegate MakeSerializer(EventDefinition definition, ISerializer[] serializers) + private (EventSerializerDelegate, EventDeserializerDelegate) MakeSerializer(EventDefinition definition, ISerializer[] serializers) { var deconstructParams = definition.Type.GetMethod("Deconstruct")?.GetParameters().ToArray()!; var ctorParams = definition.Type.GetConstructors() @@ -61,7 +70,7 @@ private EventSerializerDelegate MakeSerializer(EventDefinition definition, ISeri .GetParameters(); var paramDefinitions = deconstructParams.Zip(ctorParams) - .Select(p => (p.First, p.Second, Expression.Variable(p.Second.ParameterType, p.Second.Name), serializers.First(s => s.CanSerialize(p.Second.ParameterType)))) + .Select(p => (p.First, p.Second, Expression.Variable(p.Second.ParameterType, p.Second.Name), GetSerializer(serializers, p.Second.ParameterType))) .ToArray(); @@ -69,10 +78,190 @@ private EventSerializerDelegate MakeSerializer(EventDefinition definition, ISeri if (isFixedSize) { - return BuildFixedSizeSerializer(definition, fixedParams, fixedSize); + return (BuildFixedSizeSerializer(definition, paramDefinitions, fixedParams, fixedSize), + BuildFixedSizeDeserializer(definition, paramDefinitions, fixedParams, fixedSize)); } - throw new NotImplementedException(); + return (BuildVariableSizeSerializer(definition, paramDefinitions, fixedParams, fixedSize, unfixedParams), + BuildVariableSizeDeserializer(definition, paramDefinitions, fixedParams, fixedSize, unfixedParams)); + + } + + + + private EventSerializerDelegate BuildVariableSizeSerializer(EventDefinition eventDefinition, MemberDefinition[] allDefinitions, + List fixedParams, int fixedSize, List unfixedParams) + { + // This function effectively generates: + // void Serialize(IEvent @event) + // { + // var span = _writer.GetSpan(17); + // BinaryPrimitives.WriteUInt128BigEndian(span, eventDefinition.Id); + // ((TEvent) event).Deconstruct(out byte a, out string strVal); + // uint8Serializer.Serialize(in a, span.SliceFast(16, 17)); + // _writer.Advance(17); + // + // stringSerializer.Serialize(strVal, _writer); + // } + + var inputParam = Expression.Parameter(typeof(IEvent)); + + var converted = Expression.Convert(inputParam, eventDefinition.Type); + + var writerParam = Expression.Constant(_writer); + + var block = new List(); + + + var spanParam = Expression.Variable(typeof(Span), "span"); + block.Add(Expression.Assign(spanParam, Expression.Call(writerParam, "GetSpan", null, [Expression.Constant(fixedSize + 16)]))); + + var callDeconstructExpr = Expression.Call(converted, "Deconstruct", null, allDefinitions.Select(d => d.Variable).ToArray()); + + block.Add(callDeconstructExpr); + + var writeIdMethod = typeof(BinaryPrimitives).GetMethod("WriteUInt128BigEndian")!; + var writeIdExpr = Expression.Call(writeIdMethod, spanParam, Expression.Constant(eventDefinition.Id)); + + block.Add(writeIdExpr); + + var offset = 16; + foreach (var definition in fixedParams) + { + + var method = definition.Serializer.GetType().GetMethod("Serialize")!; + + definition.Serializer.TryGetFixedSize(definition.Base.ParameterType, out var size); + + // Reduce the size of the span, so serializers don't need to do their own offsets + var windowed = MakeWindowExpression(spanParam, offset, size); + var expr = Expression.Call(Expression.Constant(definition.Serializer), method, [definition.Variable, windowed]); + block.Add(expr); + offset += size; + } + + var advanceCall = Expression.Call(writerParam, "Advance", null, Expression.Constant(fixedSize + 16)); + block.Add(advanceCall); + + foreach (var definition in unfixedParams) + { + var method = definition.Serializer.GetType().GetMethod("Serialize")!; + var genericMethod = method.MakeGenericMethod(typeof(PooledMemoryBufferWriter)); + var expr = Expression.Call(Expression.Constant(definition.Serializer), genericMethod, [definition.Variable, writerParam]); + block.Add(expr); + } + + var allParams = new List + { + inputParam + }; + + var blockExpr = Expression.Block(allDefinitions.Select(d => d.Variable).Append(spanParam), block); + + var lambda = Expression.Lambda(blockExpr, allParams); + return lambda.Compile(); + } + + private EventDeserializerDelegate BuildVariableSizeDeserializer(EventDefinition definition, MemberDefinition[] allParams, + List fixedParams, int fixedSize, List unfixedParams) + { + var spanParam = Expression.Parameter(typeof(ReadOnlySpan)); + + var ctorExpressions = new List(); + + var offsetVariable = Expression.Variable(typeof(int), "offset"); + + var blockExprs = new List + { + Expression.Assign(offsetVariable, Expression.Constant(0)) + }; + + + var offset = 0; + foreach (var fixedParam in fixedParams) + { + var method = fixedParam.Serializer.GetType().GetMethod("Deserialize")!; + + fixedParam.Serializer.TryGetFixedSize(fixedParam.Base.ParameterType, out var size); + + var windowed = MakeReadonlyWindowExpression(spanParam, offset, size); + var callExpression = Expression.Call(Expression.Constant(fixedParam.Serializer), method, [windowed]); + blockExprs.Add(Expression.Assign(fixedParam.Variable, callExpression)); + offset += size; + } + + blockExprs.Add(Expression.AddAssign(offsetVariable, Expression.Constant(fixedSize))); + + foreach (var unfixedParam in unfixedParams) + { + var method = unfixedParam.Serializer.GetType().GetMethod("Deserialize")!; + var windowed = MakeReadonlyWindowExpression(spanParam, offsetVariable); + blockExprs.Add(Expression.AddAssign(offsetVariable, Expression.Call(Expression.Constant(unfixedParam.Serializer), method, windowed, unfixedParam.Variable))); + } + + var ctorParams = allParams.Select(d => d.Variable).ToArray(); + + var ctorCall = Expression.New(definition.Type.GetConstructors().First(c => c.GetParameters().Length == ctorParams.Length), + ctorParams); + + var casted = Expression.Convert(ctorCall, typeof(IEvent)); + blockExprs.Add(casted); + + var outerBlock = Expression.Block(ctorParams.Append(offsetVariable), blockExprs); + var lambda = Expression.Lambda(outerBlock, spanParam); + return lambda.Compile(); + } + + private ISerializer GetSerializer(ISerializer[] serializers, Type type) + { + var result = serializers.FirstOrDefault(s => s.CanSerialize(type)); + if (result != null) + { + return result; + } + + if (type.IsConstructedGenericType) + { + var genericMakers = serializers.OfType(); + foreach (var maker in genericMakers) + { + if (maker.TrySpecialze(type.GetGenericTypeDefinition(), type.GetGenericArguments(), out var serializer)) + { + return serializer; + } + } + } + + throw new Exception($"No serializer found for {type}"); + } + + private EventDeserializerDelegate BuildFixedSizeDeserializer(EventDefinition definitions, MemberDefinition[] allDefinitions, List fixedParams, int fixedSize) + { + var spanParam = Expression.Parameter(typeof(ReadOnlySpan)); + + var blockExprs = new List(); + + var offset = 0; + foreach (var fixedParam in fixedParams) + { + var method = fixedParam.Serializer.GetType().GetMethod("Deserialize")!; + + fixedParam.Serializer.TryGetFixedSize(fixedParam.Base.ParameterType, out var size); + + var windowed = MakeReadonlyWindowExpression(spanParam, offset, size); + var callExpression = Expression.Call(Expression.Constant(fixedParam.Serializer), method, [windowed]); + blockExprs.Add(Expression.Assign(fixedParam.Variable, callExpression)); + offset += size; + } + + 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)); + blockExprs.Add(casted); + + var outerBlock = Expression.Block(allDefinitions.Select(d => d.Variable), blockExprs); + var lambda = Expression.Lambda(outerBlock, spanParam); + return lambda.Compile(); } @@ -99,7 +288,7 @@ private static void SortParams(MemberDefinition[] paramDefinitions, out bool isF isFixedSize = unfixedParams.Count == 0; } - internal static Span SliceFastStart(Span data, int start, int to) + internal static Span SliceFastStart(ReadOnlySpan data, int start) { return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(data), start), data.Length - start); @@ -113,24 +302,56 @@ internal static Span SliceFastStartLength(Span data, int start, int private MethodInfo _sliceFastStartLengthMethodInfo = typeof(BinaryEventSerializer).GetMethod(nameof(SliceFastStartLength), BindingFlags.Static | BindingFlags.NonPublic)!; + internal static ReadOnlySpan ReadOnlySliceFastStartLength(ReadOnlySpan data, int start, int length) + { + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(data), start), length); + } + + internal static ReadOnlySpan ReadOnlySliceFastStart(ReadOnlySpan data, int start) + { + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(data), start), data.Length - start); + } - internal Expression MakeWindowExpress(Expression span, int offset, int size) + private Expression MakeWindowExpression(Expression span, int offset, int size) { return Expression.Call(null, _sliceFastStartLengthMethodInfo, span, Expression.Constant(offset), Expression.Constant(size)); } - private EventSerializerDelegate BuildFixedSizeSerializer(EventDefinition eventDefinition, List definitions, int fixedSize) + private MethodInfo _readonlySliceFastStartLengthMethodInfo = + typeof(BinaryEventSerializer).GetMethod(nameof(ReadOnlySliceFastStartLength), BindingFlags.Static | BindingFlags.NonPublic)!; + + private Expression MakeReadonlyWindowExpression(Expression span, int offset, int size) + { + return Expression.Call(null, _readonlySliceFastStartLengthMethodInfo, span, Expression.Constant(offset), + Expression.Constant(size)); + } + + private MethodInfo _readonlySliceFastStartMethodInfo = + typeof(BinaryEventSerializer).GetMethod(nameof(ReadOnlySliceFastStart), BindingFlags.Static | BindingFlags.NonPublic)!; + + private Expression MakeReadonlyWindowExpression(Expression span, int offset) + { + return Expression.Call(null, _readonlySliceFastStartMethodInfo, span, Expression.Constant(offset)); + } + + private Expression MakeReadonlyWindowExpression(Expression span, Expression offset) + { + return Expression.Call(null, _readonlySliceFastStartMethodInfo, span, offset); + } + + private EventSerializerDelegate BuildFixedSizeSerializer(EventDefinition eventDefinition, MemberDefinition[] allDefinitions, List definitions, int fixedSize) { // This function effectively generates: // void Serialize(IEvent @event) // { - // var span = _writer.GetSpan(7); + // var span = _writer.GetSpan(23); + // BinaryPrimitives.WriteUInt128BigEndian(span, eventDefinition.Id); // ((TEvent) event).Deconstruct(out var a, out var b, out var c); - // uint8Serializer.Serialize(in a, span.SliceFast(0, 1)); - // uint32Serializer.Serialize(in b, span.SliceFast(1, 4)); - // uint16Serializer.Serialize(in c, span.SliceFast(5, 7)); - // _writer.Advance(7); + // uint8Serializer.Serialize(in a, span.SliceFast(16, 17)); + // uint32Serializer.Serialize(in b, span.SliceFast(17, 21)); + // uint16Serializer.Serialize(in c, span.SliceFast(21, 23)); + // _writer.Advance(23); // } @@ -140,16 +361,22 @@ private EventSerializerDelegate BuildFixedSizeSerializer(EventDefinition eventDe var writerParam = Expression.Constant(_writer); - var spanParam = Expression.Call(writerParam, "GetSpan", null, [Expression.Constant(fixedSize)]); - var block = new List(); - var callDeconstructExpr = Expression.Call(converted, "Deconstruct", null, definitions.Select(d => d.Variable).ToArray()); + + var spanParam = Expression.Variable(typeof(Span), "span"); + block.Add(Expression.Assign(spanParam, Expression.Call(writerParam, "GetSpan", null, [Expression.Constant(fixedSize + 16)]))); + + var callDeconstructExpr = Expression.Call(converted, "Deconstruct", null, allDefinitions.Select(d => d.Variable).ToArray()); block.Add(callDeconstructExpr); + var writeIdMethod = typeof(BinaryPrimitives).GetMethod("WriteUInt128BigEndian")!; + var writeIdExpr = Expression.Call(writeIdMethod, spanParam, Expression.Constant(eventDefinition.Id)); - var offset = 0; + block.Add(writeIdExpr); + + var offset = 16; foreach (var definition in definitions) { @@ -158,12 +385,13 @@ private EventSerializerDelegate BuildFixedSizeSerializer(EventDefinition eventDe definition.Serializer.TryGetFixedSize(definition.Base.ParameterType, out var size); // Reduce the size of the span, so serializers don't need to do their own offsets - var windowed = MakeWindowExpress(spanParam, offset, size); + var windowed = MakeWindowExpression(spanParam, offset, size); var expr = Expression.Call(Expression.Constant(definition.Serializer), method, [definition.Variable, windowed]); block.Add(expr); + offset += size; } - var advanceCall = Expression.Call(writerParam, "Advance", null, Expression.Constant(fixedSize)); + var advanceCall = Expression.Call(writerParam, "Advance", null, Expression.Constant(fixedSize + 16)); block.Add(advanceCall); var allParams = new List @@ -171,7 +399,7 @@ private EventSerializerDelegate BuildFixedSizeSerializer(EventDefinition eventDe inputParam }; - var blockExpr = Expression.Block(definitions.Select(d => d.Variable), block); + var blockExpr = Expression.Block(definitions.Select(d => d.Variable).Append(spanParam), block); var lambda = Expression.Lambda(blockExpr, allParams); return lambda.Compile(); diff --git a/src/NexusMods.EventSourcing/Serialization/GuidSerializer.cs b/src/NexusMods.EventSourcing/Serialization/GuidSerializer.cs new file mode 100644 index 00000000..31d9c205 --- /dev/null +++ b/src/NexusMods.EventSourcing/Serialization/GuidSerializer.cs @@ -0,0 +1,28 @@ +using System; +using NexusMods.EventSourcing.Abstractions.Serialization; + +namespace NexusMods.EventSourcing.Serialization; + +public sealed class GuidSerializer : IFixedSizeSerializer +{ + public bool CanSerialize(Type valueType) + { + return valueType == typeof(Guid); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 16; + return true; + } + + public void Serialize(Guid value, Span output) + { + value.TryWriteBytes(output); + } + + public Guid Deserialize(ReadOnlySpan from) + { + return new(from); + } +} diff --git a/src/NexusMods.EventSourcing/Serialization/StringSerializer.cs b/src/NexusMods.EventSourcing/Serialization/StringSerializer.cs new file mode 100644 index 00000000..9798fe4a --- /dev/null +++ b/src/NexusMods.EventSourcing/Serialization/StringSerializer.cs @@ -0,0 +1,36 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using NexusMods.EventSourcing.Abstractions.Serialization; + +namespace NexusMods.EventSourcing.Serialization; + +public class StringSerializer : IVariableSizeSerializer +{ + public bool CanSerialize(Type valueType) + { + return valueType == typeof(string); + } + + public bool TryGetFixedSize(Type valueType, out int size) + { + size = 0; + return false; + } + + public void Serialize(string value, TWriter output) where TWriter : IBufferWriter + { + var size = System.Text.Encoding.UTF8.GetByteCount(value); + var span = output.GetSpan(size + 2); + BinaryPrimitives.WriteUInt16LittleEndian(span, (ushort)size); + System.Text.Encoding.UTF8.GetBytes(value, span[2..]); + output.Advance(size + 2); + } + + public int Deserialize(ReadOnlySpan from, out string value) + { + var size = BinaryPrimitives.ReadUInt16LittleEndian(from); + value = System.Text.Encoding.UTF8.GetString(from[2..(2 + size)]); + return size + 2; + } +} diff --git a/src/NexusMods.EventSourcing/Serialization/UInt32Serializer.cs b/src/NexusMods.EventSourcing/Serialization/UInt32Serializer.cs index 8e21511b..40ca04d2 100644 --- a/src/NexusMods.EventSourcing/Serialization/UInt32Serializer.cs +++ b/src/NexusMods.EventSourcing/Serialization/UInt32Serializer.cs @@ -4,7 +4,7 @@ namespace NexusMods.EventSourcing.Serialization; -public class UInt32Serializer : IFixedSizeSerializer +public sealed class UInt32Serializer : IFixedSizeSerializer { public bool CanSerialize(Type valueType) => valueType == typeof(uint); @@ -19,7 +19,7 @@ public void Serialize(uint value, Span output) BinaryPrimitives.WriteUInt32BigEndian(output, value); } - public uint Deserialize(Span from) + public uint Deserialize(ReadOnlySpan from) { return BinaryPrimitives.ReadUInt32BigEndian(from); } diff --git a/src/NexusMods.EventSourcing/Serialization/UInt8Serializer.cs b/src/NexusMods.EventSourcing/Serialization/UInt8Serializer.cs index 440541a8..b3bfb3ca 100644 --- a/src/NexusMods.EventSourcing/Serialization/UInt8Serializer.cs +++ b/src/NexusMods.EventSourcing/Serialization/UInt8Serializer.cs @@ -18,7 +18,7 @@ public void Serialize(byte value, Span output) output[0] = value; } - public byte Deserialize(Span from) + public byte Deserialize(ReadOnlySpan from) { return from[0]; } diff --git a/src/NexusMods.EventSourcing/Services.cs b/src/NexusMods.EventSourcing/Services.cs index 9916febb..2e7c8bba 100644 --- a/src/NexusMods.EventSourcing/Services.cs +++ b/src/NexusMods.EventSourcing/Services.cs @@ -11,9 +11,13 @@ public static class Services public static IServiceCollection AddEventSourcing(this IServiceCollection services) { return services + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddEvent() .AddSingleton(); } diff --git a/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs b/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs index 9d11f15f..1bf2b854 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs @@ -5,8 +5,7 @@ namespace NexusMods.EventSourcing.TestModel.Events; [EventId("8492075A-DED5-42BF-8D01-B4CDCE2526CF")] -[MemoryPackable] -public partial record SwapModEnabled(EntityId ModId, bool Enabled) : IEvent +public record SwapModEnabled(EntityId ModId, bool Enabled) : IEvent { public void Apply(T context) where T : IEventContext { diff --git a/tests/NexusMods.EventSourcing.Tests/SerializationTests.cs b/tests/NexusMods.EventSourcing.Tests/SerializationTests.cs index 2897d032..3533406b 100644 --- a/tests/NexusMods.EventSourcing.Tests/SerializationTests.cs +++ b/tests/NexusMods.EventSourcing.Tests/SerializationTests.cs @@ -14,6 +14,11 @@ public class SerializationTests(BinaryEventSerializer serializer) public void CanSerializeEvents() { + var evnt = new SimpleTestEvent(420000, 112); + var serialized = serializer.Serialize(evnt); + + var deserialized = serializer.Deserialize(serialized); + deserialized.Should().Be(evnt); }