diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs index aeffeb1..3722462 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs @@ -1,6 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; using AyCode.Core.Extensions; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace AyCode.Core.Tests.Serialization; @@ -27,6 +31,10 @@ public class AcBinarySerializerIIdReferenceTests /// private static int CountObjectRefs(byte[] binary) { + Console.WriteLine(); + Console.WriteLine( BitConverter.ToString(binary)); + Console.WriteLine(); + var count = 0; for (var i = 0; i < binary.Length; i++) { @@ -71,18 +79,22 @@ public class AcBinarySerializerIIdReferenceTests public void SameInstance_SerializeAndDeserialize() { // Arrange: SAME instance used 4 times + var userPreferences = new UserPreferences(); var sharedTag = new SharedTag { Id = 1, Name = "ImportantTag", Color = "#FF0000" }; + var sharedUser = new SharedUser { Id = 1, Preferences = userPreferences }; + var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", PrimaryTag = sharedTag, + Owner = sharedUser, Items = [ - new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = sharedTag }, - new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = sharedTag }, - new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = sharedTag } + new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = sharedTag, Assignee = sharedUser }, + new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = sharedTag, Assignee = new SharedUser { Id = 2, Preferences = userPreferences }}, + new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = sharedTag, Assignee = new SharedUser { Id = 3, Preferences = userPreferences } } ] }; @@ -95,8 +107,8 @@ public class AcBinarySerializerIIdReferenceTests Console.WriteLine($"Binary size: {binary.Length} bytes"); Console.WriteLine($"ObjectRef count: {objectRefCount}"); - Assert.IsTrue(objectRefCount >= 3, - $"Expected at least 3 ObjectRef entries for shared tag, found {objectRefCount}. " + + Assert.IsTrue(objectRefCount >= 6, + $"Expected at least 6 ObjectRef entries for shared tag and user (3 each), found {objectRefCount}. " + "Reference handling may not be active!"); // Assert 2: Data integrity - ALL tags are present and have correct data @@ -114,6 +126,8 @@ public class AcBinarySerializerIIdReferenceTests Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!"); Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect"); Assert.AreEqual("ImportantTag", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect"); + + Assert.IsNotNull(result.Items[i].Assignee, $"Items[{i}].Assignee is null - data lost!"); } // Assert 3: Reference identity - all should be same object reference @@ -123,6 +137,16 @@ public class AcBinarySerializerIIdReferenceTests "Item[1].Tag should be same reference as PrimaryTag"); Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, "Item[2].Tag should be same reference as PrimaryTag"); + + Assert.AreSame(result.Owner, result.Items[0].Assignee, + "Item[0].Assignee should be same reference as Owner"); + + Assert.AreSame(result.Owner.Preferences, result.Items[0].Assignee.Preferences, + "Item[0].Assignee.Preferences should be same reference as Owner.Preferences"); + Assert.AreSame(result.Owner.Preferences, result.Items[1].Assignee.Preferences, + "Item[1].Assignee.Preferences should be same reference as Owner.Preferences"); + Assert.AreSame(result.Items[1].Assignee.Preferences, result.Items[2].Assignee.Preferences, + "Item[1].Assignee.Preferences should be same reference as Item[2].Assignee.Preferences"); } #endregion @@ -142,6 +166,7 @@ public class AcBinarySerializerIIdReferenceTests { // Arrange: DIFFERENT instances but SAME IId.Id // CRITICAL: Multiple DIFFERENT TYPES all have Id=1 - must not be confused! + var sharedTag = new SharedTag { Id = 55, Name = "ImportantTag_55", Color = "#FF0000" }; var order = new TestOrder { Id = 1, @@ -182,7 +207,7 @@ public class AcBinarySerializerIIdReferenceTests // Assert 1: Check if ObjectRef is used (IId-based deduplication active) var objectRefCount = CountObjectRefs(binary); - Console.WriteLine($"Binary size: {binary.Length} bytes"); + Console.WriteLine($"\nBinary size: {binary.Length} bytes (WithRef)"); Console.WriteLine($"ObjectRef count: {objectRefCount}"); // Assert 3: Reference identity - same TYPE with same Id should be same reference diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 6e59f49..1e50ad2 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using AyCode.Core.Helpers; using AyCode.Core.Serializers.Expressions; using AyCode.Core.Serializers.Jsons; @@ -597,6 +598,7 @@ public static class AcSerializerCommon /// public enum IdAccessorType : byte { + None = 0, /// Id is int (most common). Int32 = 1, /// Id is long. @@ -618,28 +620,64 @@ public static class AcSerializerCommon } /// - /// Generic identity map for tracking IId values during serialization. - /// Uses JIT-optimized EqualityComparer for maximum performance with common ID types. + /// Generic identity map for tracking IId values during serialization/deserialization. + /// Uses Dictionary internally for unified tracking + object storage. /// /// The ID type (int, long, Guid) public sealed class IdentityMap : IIdentityMap where TId : notnull { - private readonly HashSet _seenIds; + private readonly Dictionary _tracked; public IdentityMap() { - _seenIds = new HashSet(EqualityComparer.Default); + _tracked = new Dictionary(EqualityComparer.Default); } /// - /// Tries to add an ID to the tracking set. - /// Returns true if this is the first occurrence (ID was added). + /// Tries to add a key to tracking (serialization). + /// Returns true if first occurrence (key was added). /// Returns false if already seen. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryAdd(TId id) + public bool TryAddKey(TId key) { - return _seenIds.Add(id); + return _tracked.TryAdd(key, null); + } + + /// + /// Checks if key exists and returns existing value, or adds new key (deserialization). + /// Returns true if first occurrence (key was added, out = null). + /// Returns false if already seen (out = existing value). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetOrAddKey(TId key, out object? existing) + { + ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists); + existing = exists ? slot : null; + return !exists; + } + + /// + /// Gets existing value or adds new value for key (deserialization with object storage). + /// Returns the existing value if key was seen before, or stores and returns newValue. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object TryGetOrAddValue(TId key, object newValue) + { + ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists); + if (exists && slot != null) return slot; + slot = newValue; + return newValue; + } + + /// + /// Tries to get the value for a key (ObjectRef lookup). + /// Returns true if found, false if not. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TId key, out object? value) + { + return _tracked.TryGetValue(key, out value); } /// @@ -647,180 +685,10 @@ public static class AcSerializerCommon /// public void Reset() { - _seenIds.Clear(); + _tracked.Clear(); } } - - /// - /// IId-based reference tracking for serialization. - /// Supplements (not replaces) the ReferenceEquals-based SerializationReferenceTracker. - /// Tracks objects by (Type, Id) key to detect same IId across different instances. - /// Uses typed dictionaries for int/long/Guid to avoid boxing overhead. - /// Stores the original object to enable marking it as multi-referenced when a duplicate is found. - /// - public sealed class IIdReferenceTracker - { - // Typed dictionaries for common Id types (no boxing!) - // Value is the first object registered with that (Type, Id) key - private Dictionary<(Type, int), object>? _int32Cache; - private Dictionary<(Type, long), object>? _int64Cache; - private Dictionary<(Type, Guid), object>? _guidCache; - private Dictionary<(Type, object), object>? _objectCache; // Fallback for exotic types - - /// - /// Tries to get existing object for an IId. - /// Returns true if same (Type, Id) was already tracked. - /// The out parameter contains the ORIGINAL object that was first registered. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetOriginalObject(object obj, TMetadata metadata, out object? originalObject) - where TMetadata : TypeMetadataBase - { - originalObject = null; - if (!metadata.IsIId) return false; - - return metadata.IdAccessorType switch - { - IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject), - IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject), - IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject), - _ => false - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryGetInt32Original(object obj, TMetadata metadata, out object? originalObject) - where TMetadata : TypeMetadataBase - { - originalObject = null; - if (_int32Cache == null) return false; - - var id = metadata.GetIdInt32(obj); - if (id == 0) return false; // Skip default Id - - var key = (obj.GetType(), id); - return _int32Cache.TryGetValue(key, out originalObject); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryGetInt64Original(object obj, TMetadata metadata, out object? originalObject) - where TMetadata : TypeMetadataBase - { - originalObject = null; - if (_int64Cache == null) return false; - - var id = metadata.GetIdInt64(obj); - if (id == 0) return false; // Skip default Id - - var key = (obj.GetType(), id); - return _int64Cache.TryGetValue(key, out originalObject); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryGetGuidOriginal(object obj, TMetadata metadata, out object? originalObject) - where TMetadata : TypeMetadataBase - { - originalObject = null; - if (_guidCache == null) return false; - - var id = metadata.GetIdGuid(obj); - if (id == Guid.Empty) return false; // Skip default Id - - var key = (obj.GetType(), id); - return _guidCache.TryGetValue(key, out originalObject); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryGetObjectOriginal(object obj, TMetadata metadata, out object? originalObject) - where TMetadata : TypeMetadataBase - { - originalObject = null; - if (_objectCache == null || metadata.IdGetter == null) return false; - - var id = metadata.IdGetter(obj); - if (id == null) return false; - - var key = (obj.GetType(), id); - return _objectCache.TryGetValue(key, out originalObject); - } - /// - /// Registers an IId object. - /// - public void Register(object obj, TMetadata metadata) - where TMetadata : TypeMetadataBase - { - if (!metadata.IsIId) return; - - switch (metadata.IdAccessorType) - { - case IdAccessorType.Int32: - RegisterInt32(obj, metadata); - break; - case IdAccessorType.Int64: - RegisterInt64(obj, metadata); - break; - case IdAccessorType.Guid: - RegisterGuid(obj, metadata); - break; - } - } - - private void RegisterInt32(object obj, TMetadata metadata) - where TMetadata : TypeMetadataBase - { - var id = metadata.GetIdInt32(obj); - if (id == 0) return; // Skip default Id - - _int32Cache ??= new Dictionary<(Type, int), object>(64); - _int32Cache[(obj.GetType(), id)] = obj; - } - - private void RegisterInt64(object obj, TMetadata metadata) - where TMetadata : TypeMetadataBase - { - var id = metadata.GetIdInt64(obj); - if (id == 0) return; // Skip default Id - - _int64Cache ??= new Dictionary<(Type, long), object>(64); - _int64Cache[(obj.GetType(), id)] = obj; - } - - private void RegisterGuid(object obj, TMetadata metadata) - where TMetadata : TypeMetadataBase - { - var id = metadata.GetIdGuid(obj); - if (id == Guid.Empty) return; // Skip default Id - - _guidCache ??= new Dictionary<(Type, Guid), object>(64); - _guidCache[(obj.GetType(), id)] = obj; - } - - private void RegisterObject(object obj, TMetadata metadata) - where TMetadata : TypeMetadataBase - { - if (metadata.IdGetter == null) return; - - var id = metadata.IdGetter(obj); - if (id == null) return; - - _objectCache ??= new Dictionary<(Type, object), object>(64); - _objectCache[(obj.GetType(), id)] = obj; - } - - /// - /// Clears all tracking data for reuse. - /// - public void Reset() - { - _int32Cache?.Clear(); - _int64Cache?.Clear(); - _guidCache?.Clear(); - _objectCache?.Clear(); - } - } - - #endregion #region Chain Reference Tracking diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index 051a9da..f84ea3f 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -90,7 +90,7 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM map = new AcSerializerCommon.IdentityMap(); wrapper.IdentityMap = map; } - return map.TryAdd(refId); + return map.TryAddKey(refId); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -138,7 +138,7 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM map = new AcSerializerCommon.IdentityMap(); wrapper.IdentityMap = map; } - return map.TryAdd(refId); + return map.TryAddKey(refId); } #endregion @@ -164,7 +164,64 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM map = new AcSerializerCommon.IdentityMap(); wrapper.IdentityMap = map; } - return map.TryAdd(refId); + return map.TryAddKey(refId); + } + + #endregion + + #region Deserialization API - TryGetOrStore + + /// + /// For deserialization: checks if an object with this Id was already seen. + /// If yes, returns the existing object. If no, stores this object and returns it. + /// Uses IdentityMap.TryGetOrAddValue - single Dictionary operation! + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object TryGetOrStoreInt32(TypeMetadataWrapper wrapper, object newObj, int id) + { + if (id == 0) return newObj; // Default Id - no tracking + + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + return map.TryGetOrAddValue(id, newObj); + } + + /// + /// For deserialization: checks if an object with this Id was already seen (long version). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object TryGetOrStoreLong(TypeMetadataWrapper wrapper, object newObj, long id) + { + if (id == 0) return newObj; + + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + return map.TryGetOrAddValue(id, newObj); + } + + /// + /// For deserialization: checks if an object with this Id was already seen (Guid version). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object TryGetOrStoreGuid(TypeMetadataWrapper wrapper, object newObj, Guid id) + { + if (id == Guid.Empty) return newObj; + + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + return map.TryGetOrAddValue(id, newObj); } #endregion diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 0df74c8..e47603c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -11,6 +11,7 @@ public static partial class AcBinaryDeserializer { /// /// Binary deserialization context. Public for generated serializers. + /// Uses composition with BinaryDeserializationContextClass for IId-based tracking. /// internal ref struct BinaryDeserializationContext { @@ -23,6 +24,12 @@ public static partial class AcBinaryDeserializer private readonly byte _minStringInternLength; private readonly bool _useStringCaching; private readonly int _maxCachedStringLength; + + /// + /// Heap-allocated context class for IId-based reference tracking. + /// Uses AcSerializerContextBase infrastructure. + /// + public readonly BinaryDeserializationContextClass ContextClass; public bool HasMetadata { get; private set; } public bool HasReferenceHandling { get; private set; } @@ -44,11 +51,16 @@ public static partial class AcBinaryDeserializer public readonly bool IsChainMode => ChainTracker != null; public BinaryDeserializationContext(ReadOnlySpan data) - : this(data, AcBinarySerializerOptions.Default) + : this(data, AcBinarySerializerOptions.Default, new BinaryDeserializationContextClass()) { } public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options) + : this(data, options, new BinaryDeserializationContextClass()) + { + } + + public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options, BinaryDeserializationContextClass contextClass) { _buffer = data; _position = 0; @@ -64,6 +76,7 @@ public static partial class AcBinaryDeserializer _minStringInternLength = options.MinStringInternLength; _useStringCaching = options.UseStringCaching; _maxCachedStringLength = options.MaxCachedStringLength; + ContextClass = contextClass; } public void ReadHeader() @@ -551,89 +564,17 @@ public static partial class AcBinaryDeserializer return ReadStringUtf8(byteLength); } - #region IId Reference Cache - - // Typed caches for IId-based deduplication (no boxing for int/long/Guid) - private Dictionary<(Type, int), object>? _iidCacheInt32; - private Dictionary<(Type, long), object>? _iidCacheLong; - private Dictionary<(Type, Guid), object>? _iidCacheGuid; + #region IId Reference Cache - Delegates to ContextClass /// /// After PopulateObject, checks if we should reuse an existing IId object. + /// Delegates to ContextClass which uses AcSerializerContextBase infrastructure. /// Returns the object to use (either the new one or an existing cached one). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata) { - if (!metadata.IsIId) return newObj; - - return metadata.IdAccessorType switch - { - AcSerializerCommon.IdAccessorType.Int32 => GetOrRegisterInt32(newObj, metadata), - AcSerializerCommon.IdAccessorType.Int64 => GetOrRegisterInt64(newObj, metadata), - AcSerializerCommon.IdAccessorType.Guid => GetOrRegisterGuid(newObj, metadata), - _ => newObj // Object fallback - don't cache (boxing overhead) - }; - } - - private object GetOrRegisterInt32(object newObj, BinaryDeserializeTypeMetadata metadata) - { - var id = metadata.GetIdInt32(newObj); - if (id == 0) return newObj; // Default Id → no caching - - var key = (newObj.GetType(), id); - - _iidCacheInt32 ??= new Dictionary<(Type, int), object>(64); - - // If already exists → return existing - if (_iidCacheInt32.TryGetValue(key, out var existing)) - { - return existing; - } - - // New → register and return - _iidCacheInt32[key] = newObj; - return newObj; - } - - private object GetOrRegisterInt64(object newObj, BinaryDeserializeTypeMetadata metadata) - { - var id = metadata.GetIdInt64(newObj); - if (id == 0) return newObj; // Default Id → no caching - - var key = (newObj.GetType(), id); - - _iidCacheLong ??= new Dictionary<(Type, long), object>(64); - - // If already exists → return existing - if (_iidCacheLong.TryGetValue(key, out var existing)) - { - return existing; - } - - // New → register and return - _iidCacheLong[key] = newObj; - return newObj; - } - - private object GetOrRegisterGuid(object newObj, BinaryDeserializeTypeMetadata metadata) - { - var id = metadata.GetIdGuid(newObj); - if (id == Guid.Empty) return newObj; // Default Id → no caching - - var key = (newObj.GetType(), id); - - _iidCacheGuid ??= new Dictionary<(Type, Guid), object>(64); - - // If already exists → return existing - if (_iidCacheGuid.TryGetValue(key, out var existing)) - { - return existing; - } - - // New → register and return - _iidCacheGuid[key] = newObj; - return newObj; + return ContextClass.GetOrRegisterIIdObject(newObj, metadata); } #endregion diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs new file mode 100644 index 0000000..0798333 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers.Binaries; + +public static partial class AcBinaryDeserializer +{ + /// + /// Heap-allocated context class for binary deserialization. + /// Inherits from AcSerializerContextBase for unified metadata caching and IId-based reference tracking. + /// Used in composition with the ref struct BinaryDeserializationContext. + /// + internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase + { + /// + /// Factory for creating BinaryDeserializeTypeMetadata instances. + /// + protected override Func MetadataFactory + => static t => new BinaryDeserializeTypeMetadata(t); + + /// + /// After PopulateObject, checks if we should reuse an existing IId object. + /// Uses the unified tracking from AcSerializerContextBase (IdentityMap). + /// Returns the object to use (either the new one or an existing cached one). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata) + { + if (!metadata.IsIId) return newObj; + + var wrapper = GetWrapper(newObj.GetType()); + + return metadata.IdAccessorType switch + { + AcSerializerCommon.IdAccessorType.Int32 => TryGetOrStoreInt32(wrapper, newObj, metadata.GetIdInt32(newObj)), + AcSerializerCommon.IdAccessorType.Int64 => TryGetOrStoreLong(wrapper, newObj, metadata.GetIdInt64(newObj)), + AcSerializerCommon.IdAccessorType.Guid => TryGetOrStoreGuid(wrapper, newObj, metadata.GetIdGuid(newObj)), + _ => newObj + }; + } + } +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 2b93581..74149c6 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Frozen; @@ -66,7 +67,7 @@ public static partial class AcBinaryDeserializer RegisterReader(BinaryTypeCode.Guid, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe()); RegisterReader(BinaryTypeCode.Enum, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type)); RegisterReader(BinaryTypeCode.Object, ReadObject); - RegisterReader(BinaryTypeCode.ObjectRef, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt())); + RegisterReader(BinaryTypeCode.ObjectRef, ReadObjectRef); RegisterReader(BinaryTypeCode.Array, ReadArray); RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary); RegisterReader(BinaryTypeCode.ByteArray, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx)); @@ -893,35 +894,101 @@ public static partial class AcBinaryDeserializer return context.ReadBytes(length); } + + #endregion #region Object Reading + /// + /// Reads an ObjectRef - looks up previously registered object by (Type, IId) in ContextClass. + /// The wire format depends on IdAccessorType: + /// - Int32: VarInt (1-5 bytes) + /// - Int64: VarLong (1-10 bytes) + /// - Guid: Raw 16 bytes + /// + private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth) + { + var metadata = GetTypeMetadata(targetType); + + if (metadata.IsIId) + { + var wrapper = context.ContextClass.GetWrapper(targetType); + var map = wrapper.IdentityMap; + + // Read ID based on type and lookup in IdentityMap + return metadata.IdAccessorType switch + { + AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, map), + AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, map), + AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, map), + _ => context.GetReferencedObject(context.ReadVarInt()) + }; + } + + // Non-IId: sequential int refId + return context.GetReferencedObject(context.ReadVarInt()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map) + { + var id = context.ReadVarInt(); + if (map is AcSerializerCommon.IdentityMap intMap && intMap.TryGetValue(id, out var obj)) + return obj; + return context.GetReferencedObject(id); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map) + { + var id = context.ReadVarLong(); // VarLong, not VarInt! + if (map is AcSerializerCommon.IdentityMap longMap && longMap.TryGetValue(id, out var obj)) + return obj; + return null; // Long IIds don't use flat reference tracking + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map) + { + var id = context.ReadGuidUnsafe(); // 16 bytes raw! + if (map is AcSerializerCommon.IdentityMap guidMap && guidMap.TryGetValue(id, out var obj)) + return obj; + return null; // Guid IIds don't use flat reference tracking + } + private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth) { - // Read reference ID if present - int refId = -1; + var metadata = GetTypeMetadata(targetType); + + // Read reference ID based on IdAccessorType (different wire format for each type) + object? readId = null; if (context.HasReferenceHandling) { - refId = context.ReadVarInt(); + readId = metadata.IdAccessorType switch + { + AcSerializerCommon.IdAccessorType.Int32 => context.ReadVarInt(), + AcSerializerCommon.IdAccessorType.Int64 => context.ReadVarLong(), + AcSerializerCommon.IdAccessorType.Guid => context.ReadGuidUnsafe(), + _ => throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}") + }; } - + // Handle dictionary types if (IsDictionaryType(targetType, out var keyType, out var valueType)) { return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth); } - var metadata = GetTypeMetadata(targetType); - // Create instance var instance = CreateInstance(targetType, metadata); if (instance == null) return null; - // Register reference - if (refId > 0) + // Register reference for flat lookup (int refId only) + if (readId is int intRefId) { - context.RegisterObject(refId, instance); + if (intRefId == 0) throw new Exception("intRefId == 0"); + context.RegisterObject(intRefId, instance); } // Deserialize mode: object just created, skip writing defaults (already at default)