From 9b8e56557fc0c66c1f00b83afecd85dea1e27910 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 3 May 2026 19:09:25 +0200 Subject: [PATCH] [LOADED_DOCS: 3 files, no new loads] NativeAOT: fallback for delegates, exclude MessagePack Added AYCODE_NATIVEAOT symbol for AOT builds and excluded MessagePack benchmarks from NativeAOT due to lack of AOT support. Updated AcSerializerCommon to use reflection-based delegates when dynamic code is unavailable, ensuring compatibility with both JIT and AOT. Added explanatory comments throughout. --- .../AyCode.Core.Serializers.Console.csproj | 6 ++++ AyCode.Core.Serializers.Console/Program.cs | 21 +++++++++++-- AyCode.Core/Serializers/AcSerializerCommon.cs | 31 +++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj index 39a0534..9654e4d 100644 --- a/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj +++ b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj @@ -21,6 +21,12 @@ true true + + $(DefineConstants);AYCODE_NATIVEAOT + true diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index b756403..292f1fe 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -4,8 +4,10 @@ using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.Serialization; // DrainFromAsync extension (test-only, used by benchmark) using AyCode.Core.Tests.TestModels; using MemoryPack; +#if !AYCODE_NATIVEAOT using MessagePack; using MessagePack.Resolvers; +#endif using Microsoft.Extensions.Options; using System.Buffers; using System.Diagnostics; @@ -52,7 +54,9 @@ public static class Program // Engine identifiers (used in Engine column + comparison logic) private const string EngineAcBinary = "AcBinary"; private const string EngineMemoryPack = "MemoryPack"; +#if !AYCODE_NATIVEAOT private const string EngineMessagePack = "MessagePack"; +#endif private const string EngineSystemTextJson = "System.Text.Json"; // IO mode identifiers (used in IO column + comparison logic) @@ -557,9 +561,9 @@ public static class Program new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, "FastMode"), // Fastest Byte[] — Runtime path (UseGeneratedCode=false). Same wire/options, no source-generated dispatch. // Always paired with the SGen variant so every layer can compare the SGen speed-up apples-to-apples. - // COMMENTED: Reflection.Emit-based dispatch crashes under NativeAOT (PlatformNotSupportedException). - // Re-enable for JIT-mode benchmarks where SGen-vs-Runtime delta matters. - //new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, "FastMode"), + // NativeAOT-safe: AcSerializerCommon.Create*Getter/Setter falls back to reflection-based delegates + // when RuntimeFeature.IsDynamicCodeSupported is false (slower but works under AOT publish). + new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, "FastMode"), // Default preset Byte[] — RefHandling=OnlyId (deduplicates IId-shared references on the wire) + // UseStringInterning=All (deduplicates repeated strings). Showcases the Default preset's wire-size // and CPU trade-off vs FastMode on the ~20% IId-ref / repeated-string test data. @@ -602,7 +606,12 @@ public static class Program // ============================================================ // MessagePack — for legacy comparison // ============================================================ +#if !AYCODE_NATIVEAOT + // MessagePack v3's DynamicGenericResolver uses Activator.CreateInstance on trimmed + // ListFormatter et al. — fails under NativeAOT publish with "No parameterless constructor". + // Excluded from the AOT build; available for regular JIT runs only. new MessagePackBenchmark(testData.Order, "ContractBased"), +#endif // System.Text.Json (commented — JSON serializer for reference; not in active suite) //new SystemTextJsonBenchmark(testData.Order, "Default") @@ -1031,6 +1040,11 @@ public static class Program } } +#if !AYCODE_NATIVEAOT + // MessagePack benchmark — excluded from NativeAOT build because v3's StandardResolver falls back + // to DynamicGenericResolver for closed-generic types (List et al.), which uses + // Activator.CreateInstance on formatter types the AOT trimmer drops → MissingMethodException at runtime. + // Available for regular JIT runs (`dotnet run`) only. private sealed class MessagePackBenchmark : ISerializerBenchmark { private readonly TestOrder _order; @@ -1083,6 +1097,7 @@ public static class Program return DeepEqualsViaJson(_order, roundTripped); } } +#endif /// /// Benchmarks AcBinary via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter. diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 5392ad3..b07e40a 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -465,6 +465,12 @@ public static class AcSerializerCommon [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Func CreateCompiledGetter(Type declaringType, PropertyInfo prop) { + // NativeAOT (and other no-dynamic-code targets): fall back to plain reflection. + // The returned delegate is cached per-property by the caller, so the indirection cost is paid + // only at cache-population time — hot path is a direct delegate invocation either way. + if (!RuntimeFeature.IsDynamicCodeSupported) + return prop.GetValue; + var objParam = LExpression.Parameter(typeof(object), "obj"); var castExpr = LExpression.Convert(objParam, declaringType); var propAccess = LExpression.Property(castExpr, prop); @@ -478,6 +484,9 @@ public static class AcSerializerCommon /// public static Action CreateCompiledSetter(Type declaringType, PropertyInfo prop) { + if (!RuntimeFeature.IsDynamicCodeSupported) + return prop.SetValue; + var targetParam = LExpression.Parameter(typeof(object), "target"); var valueParam = LExpression.Parameter(typeof(object), "value"); @@ -524,6 +533,9 @@ public static class AcSerializerCommon var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null); if (ctor == null) return null; + if (!RuntimeFeature.IsDynamicCodeSupported) + return () => ctor.Invoke(null); + var newExpr = LExpression.New(ctor); var convert = LExpression.Convert(newExpr, typeof(object)); return LExpression.Lambda>(convert).Compile(); @@ -534,15 +546,18 @@ public static class AcSerializerCommon /// public static Func CreateTypedGetter(Type declaringType, PropertyInfo prop) { + if (!RuntimeFeature.IsDynamicCodeSupported) + return obj => (TProperty)prop.GetValue(obj)!; + var objParam = LExpression.Parameter(typeof(object), "obj"); var castExpr = LExpression.Convert(objParam, declaringType); var propAccess = LExpression.Property(castExpr, prop); - + // Only convert if property type differs from TProperty (avoids unnecessary boxing) Expression resultExpr = prop.PropertyType == typeof(TProperty) ? propAccess : LExpression.Convert(propAccess, typeof(TProperty)); - + return LExpression.Lambda>(resultExpr, objParam).Compile(); } @@ -551,6 +566,9 @@ public static class AcSerializerCommon /// public static Func CreateEnumGetter(Type declaringType, PropertyInfo prop) { + if (!RuntimeFeature.IsDynamicCodeSupported) + return obj => Convert.ToInt32(prop.GetValue(obj)); + var objParam = LExpression.Parameter(typeof(object), "obj"); var castExpr = LExpression.Convert(objParam, declaringType); var propAccess = LExpression.Property(castExpr, prop); @@ -563,6 +581,9 @@ public static class AcSerializerCommon /// public static Action CreateTypedSetter(Type declaringType, PropertyInfo prop) { + if (!RuntimeFeature.IsDynamicCodeSupported) + return (obj, value) => prop.SetValue(obj, value); + var objParam = LExpression.Parameter(typeof(object), "obj"); var valueParam = LExpression.Parameter(typeof(TProperty), "value"); var castExpr = LExpression.Convert(objParam, declaringType); @@ -576,6 +597,12 @@ public static class AcSerializerCommon /// public static Action CreateEnumSetter(Type declaringType, PropertyInfo prop) { + if (!RuntimeFeature.IsDynamicCodeSupported) + { + var enumType = prop.PropertyType; + return (obj, value) => prop.SetValue(obj, Enum.ToObject(enumType, value)); + } + var objParam = LExpression.Parameter(typeof(object), "obj"); var valueParam = LExpression.Parameter(typeof(int), "value"); var castExpr = LExpression.Convert(objParam, declaringType);