From 76d29feab8d767018d8f3c12f18f1682a40099df Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 08:38:34 +0800 Subject: [PATCH 01/16] feat(csharp): implement xlang serialization runtime and peer tests --- csharp/Fory.sln | 50 + csharp/README.md | 1 + .../src/Fory.Generator/Fory.Generator.csproj | 16 + .../src/Fory.Generator/ForyObjectGenerator.cs | 1413 ++++++++++++++++ csharp/src/Fory/AnySerializer.cs | 627 +++++++ csharp/src/Fory/Attributes.cs | 41 + csharp/src/Fory/ByteBuffer.cs | 418 +++++ csharp/src/Fory/CollectionSerializers.cs | 837 ++++++++++ csharp/src/Fory/Context.cs | 397 +++++ csharp/src/Fory/EnumSerializer.cs | 44 + csharp/src/Fory/FieldSkipper.cs | 114 ++ csharp/src/Fory/Fory.cs | 244 +++ csharp/src/Fory/Fory.csproj | 8 + csharp/src/Fory/ForyConfig.cs | 90 + csharp/src/Fory/ForyException.cs | 70 + csharp/src/Fory/ForyFlags.cs | 54 + csharp/src/Fory/ForyMap.cs | 330 ++++ csharp/src/Fory/ForyTypeId.cs | 116 ++ csharp/src/Fory/MetaString.cs | 533 ++++++ csharp/src/Fory/MurmurHash3.cs | 146 ++ csharp/src/Fory/OptionalSerializer.cs | 140 ++ csharp/src/Fory/PrimitiveSerializers.cs | 530 ++++++ csharp/src/Fory/RefResolver.cs | 103 ++ csharp/src/Fory/SchemaHash.cs | 31 + csharp/src/Fory/Serializer.cs | 667 ++++++++ csharp/src/Fory/SerializerRegistry.cs | 288 ++++ csharp/src/Fory/TypeMeta.cs | 690 ++++++++ csharp/src/Fory/TypeResolver.cs | 369 +++++ csharp/src/Fory/Union.cs | 133 ++ csharp/src/Fory/UnionSerializer.cs | 99 ++ csharp/tests/Fory.Tests/Fory.Tests.csproj | 28 + csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 402 +++++ csharp/tests/Fory.Tests/GlobalUsings.cs | 1 + .../Fory.XlangPeer/Fory.XlangPeer.csproj | 14 + csharp/tests/Fory.XlangPeer/Program.cs | 1449 +++++++++++++++++ .../apache/fory/xlang/CSharpXlangTest.java | 121 ++ 36 files changed, 10614 insertions(+) create mode 100644 csharp/Fory.sln create mode 100644 csharp/README.md create mode 100644 csharp/src/Fory.Generator/Fory.Generator.csproj create mode 100644 csharp/src/Fory.Generator/ForyObjectGenerator.cs create mode 100644 csharp/src/Fory/AnySerializer.cs create mode 100644 csharp/src/Fory/Attributes.cs create mode 100644 csharp/src/Fory/ByteBuffer.cs create mode 100644 csharp/src/Fory/CollectionSerializers.cs create mode 100644 csharp/src/Fory/Context.cs create mode 100644 csharp/src/Fory/EnumSerializer.cs create mode 100644 csharp/src/Fory/FieldSkipper.cs create mode 100644 csharp/src/Fory/Fory.cs create mode 100644 csharp/src/Fory/Fory.csproj create mode 100644 csharp/src/Fory/ForyConfig.cs create mode 100644 csharp/src/Fory/ForyException.cs create mode 100644 csharp/src/Fory/ForyFlags.cs create mode 100644 csharp/src/Fory/ForyMap.cs create mode 100644 csharp/src/Fory/ForyTypeId.cs create mode 100644 csharp/src/Fory/MetaString.cs create mode 100644 csharp/src/Fory/MurmurHash3.cs create mode 100644 csharp/src/Fory/OptionalSerializer.cs create mode 100644 csharp/src/Fory/PrimitiveSerializers.cs create mode 100644 csharp/src/Fory/RefResolver.cs create mode 100644 csharp/src/Fory/SchemaHash.cs create mode 100644 csharp/src/Fory/Serializer.cs create mode 100644 csharp/src/Fory/SerializerRegistry.cs create mode 100644 csharp/src/Fory/TypeMeta.cs create mode 100644 csharp/src/Fory/TypeResolver.cs create mode 100644 csharp/src/Fory/Union.cs create mode 100644 csharp/src/Fory/UnionSerializer.cs create mode 100644 csharp/tests/Fory.Tests/Fory.Tests.csproj create mode 100644 csharp/tests/Fory.Tests/ForyRuntimeTests.cs create mode 100644 csharp/tests/Fory.Tests/GlobalUsings.cs create mode 100644 csharp/tests/Fory.XlangPeer/Fory.XlangPeer.csproj create mode 100644 csharp/tests/Fory.XlangPeer/Program.cs create mode 100644 java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java diff --git a/csharp/Fory.sln b/csharp/Fory.sln new file mode 100644 index 0000000000..b2f9edf7b1 --- /dev/null +++ b/csharp/Fory.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C3FE93C6-2294-475C-88DD-BE1FFFADC4D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory", "src\Fory\Fory.csproj", "{309709EA-773D-449D-AE12-D1C2B9518793}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory.Generator", "src\Fory.Generator\Fory.Generator.csproj", "{4DBAD732-2820-4B15-B655-19384F100997}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02DC4D41-0522-42EA-B643-471F4BB0747E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory.Tests", "tests\Fory.Tests\Fory.Tests.csproj", "{65701195-E254-4D93-9CD0-F587FDFCE769}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory.XlangPeer", "tests\Fory.XlangPeer\Fory.XlangPeer.csproj", "{8E1D5E47-AF72-46FF-B60F-B1C6210654A4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {309709EA-773D-449D-AE12-D1C2B9518793}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {309709EA-773D-449D-AE12-D1C2B9518793}.Debug|Any CPU.Build.0 = Debug|Any CPU + {309709EA-773D-449D-AE12-D1C2B9518793}.Release|Any CPU.ActiveCfg = Release|Any CPU + {309709EA-773D-449D-AE12-D1C2B9518793}.Release|Any CPU.Build.0 = Release|Any CPU + {4DBAD732-2820-4B15-B655-19384F100997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DBAD732-2820-4B15-B655-19384F100997}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DBAD732-2820-4B15-B655-19384F100997}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DBAD732-2820-4B15-B655-19384F100997}.Release|Any CPU.Build.0 = Release|Any CPU + {65701195-E254-4D93-9CD0-F587FDFCE769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65701195-E254-4D93-9CD0-F587FDFCE769}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65701195-E254-4D93-9CD0-F587FDFCE769}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65701195-E254-4D93-9CD0-F587FDFCE769}.Release|Any CPU.Build.0 = Release|Any CPU + {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {309709EA-773D-449D-AE12-D1C2B9518793} = {C3FE93C6-2294-475C-88DD-BE1FFFADC4D6} + {4DBAD732-2820-4B15-B655-19384F100997} = {C3FE93C6-2294-475C-88DD-BE1FFFADC4D6} + {65701195-E254-4D93-9CD0-F587FDFCE769} = {02DC4D41-0522-42EA-B643-471F4BB0747E} + {8E1D5E47-AF72-46FF-B60F-B1C6210654A4} = {02DC4D41-0522-42EA-B643-471F4BB0747E} + EndGlobalSection +EndGlobal diff --git a/csharp/README.md b/csharp/README.md new file mode 100644 index 0000000000..ad4f19d919 --- /dev/null +++ b/csharp/README.md @@ -0,0 +1 @@ +# Apache Fory™ C# diff --git a/csharp/src/Fory.Generator/Fory.Generator.csproj b/csharp/src/Fory.Generator/Fory.Generator.csproj new file mode 100644 index 0000000000..48864e20cf --- /dev/null +++ b/csharp/src/Fory.Generator/Fory.Generator.csproj @@ -0,0 +1,16 @@ + + + netstandard2.0 + 12.0 + enable + enable + true + true + false + + + + + + + diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs new file mode 100644 index 0000000000..d0625b8735 --- /dev/null +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -0,0 +1,1413 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Apache.Fory.Generator; + +[Generator(LanguageNames.CSharp)] +public sealed class ForyObjectGenerator : IIncrementalGenerator +{ + private static readonly SymbolDisplayFormat FullNameFormat = + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + private static readonly DiagnosticDescriptor GenericTypeNotSupported = new( + id: "FORY001", + title: "Generic types are not supported by ForyObject generator", + messageFormat: "Type '{0}' is generic and is not supported by [ForyObject].", + category: "Fory", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor MissingCtor = new( + id: "FORY002", + title: "Missing parameterless constructor", + messageFormat: "Class '{0}' must declare an accessible parameterless constructor for [ForyObject].", + category: "Fory", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor UnsupportedEncoding = new( + id: "FORY003", + title: "Unsupported ForyField encoding", + messageFormat: "Member '{0}' uses unsupported [ForyField] encoding for type '{1}'.", + category: "Fory", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor UnsupportedDynamicAny = new( + id: "FORY004", + title: "Unsupported dynamic Any field shape", + messageFormat: "Member '{0}' has unsupported dynamic Any shape '{1}'. Only object, List and Dictionary are supported.", + category: "Fory", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider typeModels = context.SyntaxProvider + .ForAttributeWithMetadataName( + "Apache.Fory.ForyObjectAttribute", + static (node, _) => node is TypeDeclarationSyntax || node is EnumDeclarationSyntax, + static (syntaxContext, ct) => BuildTypeModel(syntaxContext, ct)) + .Where(static m => m is not null); + + context.RegisterSourceOutput( + typeModels.Collect(), + static (spc, models) => Emit(spc, models)); + } + + private static void Emit(SourceProductionContext context, ImmutableArray maybeModels) + { + if (maybeModels.IsDefaultOrEmpty) + { + return; + } + + Dictionary models = new(StringComparer.Ordinal); + foreach (TypeModel? maybeModel in maybeModels) + { + if (maybeModel is null) + { + continue; + } + + models[maybeModel.TypeName] = maybeModel; + } + + if (models.Count == 0) + { + return; + } + + StringBuilder sb = new(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("namespace Apache.Fory.Generated;"); + sb.AppendLine(); + + foreach (KeyValuePair entry in models.OrderBy(kv => kv.Key, StringComparer.Ordinal)) + { + TypeModel model = entry.Value; + if (model.Kind == DeclKind.Struct || model.Kind == DeclKind.Class) + { + EmitObjectSerializer(sb, model); + sb.AppendLine(); + } + } + + sb.AppendLine("internal static class __ForyGeneratedModuleInitializer"); + sb.AppendLine("{"); + sb.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]"); + sb.AppendLine(" internal static void Register()"); + sb.AppendLine(" {"); + foreach (KeyValuePair entry in models.OrderBy(kv => kv.Key, StringComparer.Ordinal)) + { + TypeModel model = entry.Value; + if (model.Kind == DeclKind.Enum) + { + sb.AppendLine( + $" global::Apache.Fory.SerializerRegistry.RegisterGenerated<{model.TypeName}>(() => new global::Apache.Fory.EnumSerializer<{model.TypeName}>());"); + } + else + { + sb.AppendLine( + $" global::Apache.Fory.SerializerRegistry.RegisterGenerated<{model.TypeName}>(() => new {model.SerializerName}());"); + } + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + + context.AddSource("Fory.GeneratedSerializers.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + } + + private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) + { + sb.AppendLine($"file sealed class {model.SerializerName} : global::Apache.Fory.Serializer<{model.TypeName}>"); + sb.AppendLine("{"); + sb.AppendLine(" private static global::Apache.Fory.RefMode __ForyRefMode(bool nullable, bool trackRef)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (trackRef)"); + sb.AppendLine(" {"); + sb.AppendLine(" return global::Apache.Fory.RefMode.Tracking;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" return nullable ? global::Apache.Fory.RefMode.NullOnly : global::Apache.Fory.RefMode.None;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" private static bool __ForyNeedsTypeInfoForField(global::Apache.Fory.ForyTypeId typeId)"); + sb.AppendLine(" {"); + sb.AppendLine(" return typeId switch"); + sb.AppendLine(" {"); + sb.AppendLine(" global::Apache.Fory.ForyTypeId.Struct or"); + sb.AppendLine(" global::Apache.Fory.ForyTypeId.CompatibleStruct or"); + sb.AppendLine(" global::Apache.Fory.ForyTypeId.NamedStruct or"); + sb.AppendLine(" global::Apache.Fory.ForyTypeId.NamedCompatibleStruct or"); + sb.AppendLine(" global::Apache.Fory.ForyTypeId.Ext or"); + sb.AppendLine(" global::Apache.Fory.ForyTypeId.NamedExt or"); + sb.AppendLine(" global::Apache.Fory.ForyTypeId.Unknown => true,"); + sb.AppendLine(" _ => false,"); + sb.AppendLine(" };"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" private static uint __ForySchemaHash(bool trackRef)"); + sb.AppendLine(" {"); + sb.Append(" return global::Apache.Fory.SchemaHash.StructHash32("); + sb.Append(BuildSchemaFingerprintExpression(model.Members)); + sb.AppendLine(");"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" public override global::Apache.Fory.ForyTypeId StaticTypeId => global::Apache.Fory.ForyTypeId.Struct;"); + if (model.Kind == DeclKind.Class) + { + sb.AppendLine(" public override bool IsNullableType => true;"); + sb.AppendLine(" public override bool IsReferenceTrackableType => true;"); + sb.AppendLine($" public override {model.TypeName} DefaultValue => null!;"); + sb.AppendLine($" public override bool IsNone({model.TypeName} value) => value is null;"); + } + else + { + sb.AppendLine($" public override {model.TypeName} DefaultValue => new {model.TypeName}();"); + } + + sb.AppendLine(); + sb.AppendLine(" public override global::System.Collections.Generic.IReadOnlyList CompatibleTypeMetaFields(bool trackRef)"); + sb.AppendLine(" {"); + if (model.SortedMembers.Length == 0) + { + sb.AppendLine(" return global::System.Array.Empty();"); + } + else + { + sb.AppendLine(" return new global::Apache.Fory.TypeMetaFieldInfo[]"); + sb.AppendLine(" {"); + foreach (MemberModel member in model.SortedMembers) + { + sb.AppendLine( + $" new global::Apache.Fory.TypeMetaFieldInfo(null, \"{EscapeString(member.Name)}\", {BuildCompatibleTypeMetaExpression(member.TypeMeta, "trackRef")}),"); + } + + sb.AppendLine(" };"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine( + $" public override void WriteData(ref global::Apache.Fory.WriteContext context, in {model.TypeName} value, bool hasGenerics)"); + sb.AppendLine(" {"); + sb.AppendLine(" _ = hasGenerics;"); + sb.AppendLine(" if (context.Compatible)"); + sb.AppendLine(" {"); + if (model.SortedMembers.Length == 0) + { + sb.AppendLine(" return;"); + } + else + { + foreach (MemberModel member in model.SortedMembers) + { + EmitWriteMember(sb, member, true); + } + + sb.AppendLine(" return;"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" context.Writer.WriteInt32(unchecked((int)__ForySchemaHash(context.TrackRef)));"); + foreach (MemberModel member in model.SortedMembers) + { + EmitWriteMember(sb, member, false); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine($" public override {model.TypeName} ReadData(ref global::Apache.Fory.ReadContext context)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (context.Compatible)"); + sb.AppendLine(" {"); + sb.AppendLine($" global::Apache.Fory.TypeMeta typeMeta = context.ConsumeCompatibleTypeMeta(typeof({model.TypeName}));"); + sb.AppendLine($" {model.TypeName} value = new {model.TypeName}();"); + if (model.Kind == DeclKind.Class) + { + sb.AppendLine(" context.BindPendingReference(value);"); + } + + sb.AppendLine(" foreach (global::Apache.Fory.TypeMetaFieldInfo remoteField in typeMeta.Fields)"); + sb.AppendLine(" {"); + sb.AppendLine(" global::Apache.Fory.RefMode remoteRefMode = __ForyRefMode(remoteField.FieldType.Nullable, remoteField.FieldType.TrackRef);"); + sb.AppendLine(" bool remoteReadTypeInfo = __ForyNeedsTypeInfoForField((global::Apache.Fory.ForyTypeId)remoteField.FieldType.TypeId);"); + sb.AppendLine(" switch (remoteField.FieldName)"); + sb.AppendLine(" {"); + foreach (MemberModel member in model.SortedMembers) + { + sb.AppendLine($" case \"{EscapeString(member.FieldIdentifier)}\":"); + sb.AppendLine(" {"); + EmitReadMemberAssignment(sb, member, "remoteRefMode", "remoteReadTypeInfo", "value", "Compat", 7); + sb.AppendLine(" break;"); + sb.AppendLine(" }"); + } + + sb.AppendLine(" default:"); + sb.AppendLine(" global::Apache.Fory.FieldSkipper.SkipFieldValue(ref context, remoteField.FieldType);"); + sb.AppendLine(" break;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" return value;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" uint schemaHash = unchecked((uint)context.Reader.ReadInt32());"); + sb.AppendLine(" uint expectedHash = __ForySchemaHash(context.TrackRef);"); + sb.AppendLine(" if (schemaHash != expectedHash)"); + sb.AppendLine(" {"); + sb.AppendLine(" throw new global::Apache.Fory.ForyInvalidDataException($\"class version hash mismatch: expected {expectedHash}, got {schemaHash}\");"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine($" {model.TypeName} valueSchema = new {model.TypeName}();"); + if (model.Kind == DeclKind.Class) + { + sb.AppendLine(" context.BindPendingReference(valueSchema);"); + } + + foreach (MemberModel member in model.SortedMembers) + { + EmitReadMemberAssignment(sb, member, BuildWriteRefModeExpression(member), "false", "valueSchema", "Schema", 2); + } + + sb.AppendLine(" return valueSchema;"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + } + + private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool compatibleMode) + { + string refModeExpr = BuildWriteRefModeExpression(member); + string memberAccess = $"value.{member.Name}"; + string hasGenerics = member.IsCollection ? "true" : "false"; + string writeTypeInfo = compatibleMode + ? $"__ForyNeedsTypeInfoForField(global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().StaticTypeId)" + : "false"; + + switch (member.DynamicAnyKind) + { + case DynamicAnyKind.AnyValue: + sb.AppendLine( + $" global::Apache.Fory.DynamicAnyCodec.WriteAny(ref context, {memberAccess}, {refModeExpr}, true, false);"); + return; + case DynamicAnyKind.AnyList: + sb.AppendLine( + $" global::Apache.Fory.DynamicAnyCodec.WriteAnyList(ref context, {memberAccess}, {refModeExpr}, false, true);"); + return; + case DynamicAnyKind.StringAnyMap: + sb.AppendLine( + $" global::Apache.Fory.DynamicAnyCodec.WriteStringAnyMap(ref context, {memberAccess}, {refModeExpr}, false, true);"); + return; + case DynamicAnyKind.Int32AnyMap: + sb.AppendLine( + $" global::Apache.Fory.DynamicAnyCodec.WriteInt32AnyMap(ref context, {memberAccess}, {refModeExpr}, false, true);"); + return; + case DynamicAnyKind.None: + break; + default: + throw new InvalidOperationException($"unsupported dynamic any kind {member.DynamicAnyKind}"); + } + + if (member.CustomWrapperTypeName is not null) + { + string wrappedVarName = compatibleMode + ? $"__{member.Name}WrappedCompat" + : $"__{member.Name}WrappedSchema"; + if (member.IsNullableValueType) + { + sb.AppendLine( + $" {member.CustomWrapperTypeName}? {wrappedVarName} = {memberAccess}.HasValue ? new {member.CustomWrapperTypeName}({memberAccess}.Value) : null;"); + sb.AppendLine( + $" global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}?>().Write(ref context, {wrappedVarName}, {refModeExpr}, false, false);"); + } + else + { + sb.AppendLine( + $" global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}>().Write(ref context, new {member.CustomWrapperTypeName}({memberAccess}), {refModeExpr}, false, false);"); + } + + return; + } + + sb.AppendLine( + $" global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().Write(ref context, {memberAccess}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});"); + } + + private static void EmitReadMemberAssignment( + StringBuilder sb, + MemberModel member, + string refModeExpr, + string readTypeInfoExpr, + string valueVar, + string variableSuffix, + int indentLevel) + { + string indent = new(' ', indentLevel * 2); + string assignmentTarget = $"{valueVar}.{member.Name}"; + switch (member.DynamicAnyKind) + { + case DynamicAnyKind.AnyValue: + sb.AppendLine( + $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAny(ref context, {refModeExpr}, true), typeof({member.TypeName}))!;"); + return; + case DynamicAnyKind.AnyList: + sb.AppendLine( + $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAnyList(ref context, {refModeExpr}, false), typeof({member.TypeName}))!;"); + return; + case DynamicAnyKind.StringAnyMap: + sb.AppendLine( + $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadStringAnyMap(ref context, {refModeExpr}, false), typeof({member.TypeName}))!;"); + return; + case DynamicAnyKind.Int32AnyMap: + sb.AppendLine( + $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadInt32AnyMap(ref context, {refModeExpr}, false), typeof({member.TypeName}))!;"); + return; + case DynamicAnyKind.None: + break; + default: + throw new InvalidOperationException($"unsupported dynamic any kind {member.DynamicAnyKind}"); + } + + if (member.CustomWrapperTypeName is not null) + { + string wrappedVarName = $"__{member.Name}Wrapped{variableSuffix}"; + if (member.IsNullableValueType) + { + sb.AppendLine( + $"{indent}{member.CustomWrapperTypeName}? {wrappedVarName} = global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}?>().Read(ref context, {refModeExpr}, false);"); + sb.AppendLine( + $"{indent}{assignmentTarget} = {wrappedVarName}.HasValue ? {wrappedVarName}.Value.RawValue : ({member.TypeName})null!;"); + } + else + { + sb.AppendLine( + $"{indent}{assignmentTarget} = global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}>().Read(ref context, {refModeExpr}, false).RawValue;"); + } + + return; + } + + sb.AppendLine( + $"{indent}{assignmentTarget} = global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().Read(ref context, {refModeExpr}, {readTypeInfoExpr});"); + } + + private static string BuildSchemaFingerprintExpression(ImmutableArray members) + { + if (members.IsDefaultOrEmpty) + { + return "\"\""; + } + + IEnumerable ordered = members + .OrderBy(m => m.FieldIdentifier, StringComparer.Ordinal) + .ThenBy(m => m.OriginalIndex); + + StringBuilder sb = new(); + bool first = true; + foreach (MemberModel member in ordered) + { + uint fingerprintTypeId = (member.Classification.IsPrimitive || member.Classification.IsBuiltIn) + ? member.Classification.TypeId + : 0; + string trackRefExpr = member.DynamicAnyKind switch + { + DynamicAnyKind.AnyValue => "(trackRef ? 1 : 0)", + DynamicAnyKind.AnyList or DynamicAnyKind.StringAnyMap or DynamicAnyKind.Int32AnyMap => "0", + _ => member.Classification.IsBuiltIn + ? "0" + : $"((trackRef && global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)", + }; + string nullable = member.IsNullable ? "1" : "0"; + string piece = $"\"{EscapeString(member.FieldIdentifier)},{fingerprintTypeId},\" + {trackRefExpr} + \",{nullable};\""; + if (!first) + { + sb.Append(" + "); + } + + first = false; + sb.Append(piece); + } + + return sb.ToString(); + } + + private static string BuildCompatibleTypeMetaExpression(TypeMetaFieldTypeModel model, string trackRefExpr) + { + string localTrackRefExpr = model.TrackRefByContext ? trackRefExpr : "false"; + if (model.Generics.Length > 0) + { + string generics = string.Join( + ", ", + model.Generics.Select(g => BuildCompatibleTypeMetaExpression(g, "false"))); + return + $"new global::Apache.Fory.TypeMetaFieldType({model.TypeIdExpr}, {BoolLiteral(model.Nullable)}, {localTrackRefExpr}, new global::Apache.Fory.TypeMetaFieldType[] {{ {generics} }})"; + } + + return $"new global::Apache.Fory.TypeMetaFieldType({model.TypeIdExpr}, {BoolLiteral(model.Nullable)}, {localTrackRefExpr})"; + } + + private static string BuildWriteRefModeExpression(MemberModel member) + { + return member.DynamicAnyKind switch + { + DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)", + DynamicAnyKind.AnyList or DynamicAnyKind.StringAnyMap or DynamicAnyKind.Int32AnyMap => + $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)", + _ => member.Classification.IsBuiltIn + ? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)" + : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().IsReferenceTrackableType)", + }; + } + + private static TypeModel? BuildTypeModel(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + _ = cancellationToken; + if (context.TargetSymbol is not INamedTypeSymbol typeSymbol) + { + return null; + } + + if (typeSymbol.TypeParameters.Length > 0) + { + return null; + } + + string typeName = typeSymbol.ToDisplayString(FullNameFormat); + string serializerName = "__ForySerializer_" + Sanitize(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + if (typeSymbol.TypeKind == TypeKind.Enum) + { + return new TypeModel( + typeName, + serializerName, + DeclKind.Enum, + ImmutableArray.Empty, + ImmutableArray.Empty); + } + + DeclKind kind = typeSymbol.TypeKind switch + { + TypeKind.Struct => DeclKind.Struct, + TypeKind.Class => DeclKind.Class, + _ => DeclKind.Unknown, + }; + + if (kind == DeclKind.Unknown) + { + return null; + } + + if (kind == DeclKind.Class && !HasAccessibleParameterlessCtor(typeSymbol)) + { + return null; + } + + List members = []; + foreach (ISymbol member in typeSymbol.GetMembers()) + { + if (member.IsStatic) + { + continue; + } + + if (member is IFieldSymbol field) + { + if (field.IsConst || field.IsReadOnly || !IsReadableWritableAccessibility(field.DeclaredAccessibility)) + { + continue; + } + + MemberModel? parsedField = BuildMemberModel(field.Name, field.Type, field, MemberDeclKind.Field); + if (parsedField is not null) + { + members.Add(parsedField); + } + + continue; + } + + if (member is IPropertySymbol property) + { + if (property.IsIndexer || property.GetMethod is null || property.SetMethod is null) + { + continue; + } + + if (property.SetMethod.IsInitOnly) + { + continue; + } + + if (!IsReadableWritableAccessibility(property.GetMethod.DeclaredAccessibility) || + !IsReadableWritableAccessibility(property.SetMethod.DeclaredAccessibility)) + { + continue; + } + + MemberModel? parsedProperty = BuildMemberModel( + property.Name, + property.Type, + property, + MemberDeclKind.Property); + if (parsedProperty is not null) + { + members.Add(parsedProperty); + } + } + } + + ImmutableArray ordered = members + .OrderBy(m => m.OriginalIndex) + .ToImmutableArray(); + ImmutableArray sorted = SortMembers(ordered); + + return new TypeModel(typeName, serializerName, kind, ordered, sorted); + } + + private static MemberModel? BuildMemberModel( + string name, + ITypeSymbol memberType, + ISymbol memberSymbol, + MemberDeclKind memberDeclKind) + { + (bool isOptional, ITypeSymbol unwrappedType) = UnwrapNullable(memberType); + FieldEncoding fieldEncoding = FieldEncoding.None; + foreach (AttributeData attribute in memberSymbol.GetAttributes()) + { + string? attrName = attribute.AttributeClass?.ToDisplayString(); + if (!string.Equals(attrName, "Apache.Fory.ForyFieldAttribute", StringComparison.Ordinal)) + { + continue; + } + + if (attribute.ConstructorArguments.Length == 1) + { + fieldEncoding = (FieldEncoding)(int)attribute.ConstructorArguments[0].Value!; + } + } + + DynamicAnyKind dynamicAnyKind = ResolveDynamicAnyKind(unwrappedType); + if (dynamicAnyKind == DynamicAnyKind.Unsupported) + { + return null; + } + + TypeResolution resolution = ResolveTypeResolution(unwrappedType, fieldEncoding); + if (!resolution.Supported) + { + return null; + } + + TypeClassification classification = resolution.Classification; + int group = classification.IsPrimitive + ? (isOptional ? 2 : 1) + : classification.IsMap + ? 5 + : classification.IsCollection + ? 4 + : classification.IsBuiltIn + ? 3 + : 6; + + int index = int.MaxValue; + Location? sourceLocation = memberSymbol.Locations.FirstOrDefault(loc => loc.IsInSource); + if (sourceLocation is not null) + { + index = sourceLocation.SourceSpan.Start; + } + + string typeName = memberType.ToDisplayString(FullNameFormat); + TypeMetaFieldTypeModel typeMeta = BuildTypeMetaFieldTypeModel( + memberType, + isOptional, + dynamicAnyKind, + resolution.CustomWrapperTypeName, + resolution.Classification.TypeId); + + return new MemberModel( + name, + ToSnakeCase(name), + index, + memberDeclKind, + typeName, + isOptional, + memberType is INamedTypeSymbol nts && + nts.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T, + classification, + group, + classification.IsCollection || classification.IsMap, + resolution.CustomWrapperTypeName, + dynamicAnyKind == DynamicAnyKind.None ? DynamicAnyKind.None : dynamicAnyKind, + typeMeta); + } + + private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel( + ITypeSymbol memberType, + bool nullable, + DynamicAnyKind dynamicAnyKind, + string? customWrapperTypeName, + uint explicitTypeId) + { + (bool _, ITypeSymbol unwrapped) = UnwrapNullable(memberType); + + if (TryGetListElementType(unwrapped, out ITypeSymbol? listElementType)) + { + bool elementNullable = GenericNullable(listElementType!); + TypeMetaFieldTypeModel element = BuildTypeMetaFieldTypeModel( + listElementType!, + elementNullable, + ResolveDynamicAnyKind(UnwrapNullable(listElementType!).Item2), + null, + 0); + return new TypeMetaFieldTypeModel( + "(uint)global::Apache.Fory.ForyTypeId.List", + nullable, + false, + ImmutableArray.Create(element)); + } + + if (TryGetSetElementType(unwrapped, out ITypeSymbol? setElementType)) + { + bool elementNullable = GenericNullable(setElementType!); + TypeMetaFieldTypeModel element = BuildTypeMetaFieldTypeModel( + setElementType!, + elementNullable, + ResolveDynamicAnyKind(UnwrapNullable(setElementType!).Item2), + null, + 0); + return new TypeMetaFieldTypeModel( + "(uint)global::Apache.Fory.ForyTypeId.Set", + nullable, + false, + ImmutableArray.Create(element)); + } + + if (TryGetMapTypeArguments(unwrapped, out ITypeSymbol? keyType, out ITypeSymbol? valueType)) + { + bool keyNullable = GenericNullable(keyType!); + bool valueNullable = GenericNullable(valueType!); + TypeMetaFieldTypeModel key = BuildTypeMetaFieldTypeModel( + keyType!, + keyNullable, + ResolveDynamicAnyKind(UnwrapNullable(keyType!).Item2), + null, + 0); + TypeMetaFieldTypeModel value = BuildTypeMetaFieldTypeModel( + valueType!, + valueNullable, + ResolveDynamicAnyKind(UnwrapNullable(valueType!).Item2), + null, + 0); + return new TypeMetaFieldTypeModel( + "(uint)global::Apache.Fory.ForyTypeId.Map", + nullable, + false, + ImmutableArray.Create(key, value)); + } + + if (customWrapperTypeName is not null) + { + return new TypeMetaFieldTypeModel( + explicitTypeId.ToString(), + nullable, + false, + ImmutableArray.Empty); + } + + if (IsUnionType(unwrapped)) + { + return new TypeMetaFieldTypeModel( + "(uint)global::Apache.Fory.ForyTypeId.Union", + nullable, + true, + ImmutableArray.Empty); + } + + if (dynamicAnyKind == DynamicAnyKind.AnyValue) + { + return new TypeMetaFieldTypeModel( + "(uint)global::Apache.Fory.ForyTypeId.Unknown", + nullable, + true, + ImmutableArray.Empty); + } + + string typeName = memberType.ToDisplayString(FullNameFormat); + TypeClassification classification = ClassifyType(unwrapped); + return new TypeMetaFieldTypeModel( + $"(uint)global::Apache.Fory.SerializerRegistry.Get<{typeName}>().StaticTypeId", + nullable, + !classification.IsBuiltIn && unwrapped.TypeKind != TypeKind.Enum, + ImmutableArray.Empty); + } + + private static ImmutableArray SortMembers(ImmutableArray members) + { + return members + .OrderBy(m => m.Group) + .ThenBy(m => + { + if (m.Group is 1 or 2) + { + return m.Classification.IsCompressedNumeric ? 1 : 0; + } + + return 0; + }) + .ThenByDescending(m => m.Group is 1 or 2 ? m.Classification.PrimitiveSize : 0) + .ThenBy(m => + { + if (m.Group is 1 or 2) + { + return (int)(uint.MaxValue - m.Classification.TypeId); + } + + if (m.Group is 3 or 4 or 5) + { + return (int)m.Classification.TypeId; + } + + return 0; + }) + .ThenBy(m => m.FieldIdentifier, StringComparer.Ordinal) + .ThenBy(m => m.Name, StringComparer.Ordinal) + .ThenBy(m => m.OriginalIndex) + .ToImmutableArray(); + } + + private static bool GenericNullable(ITypeSymbol type) + { + (bool optional, ITypeSymbol unwrapped) = UnwrapNullable(type); + if (optional) + { + return true; + } + + TypeClassification c = ClassifyType(unwrapped); + return !c.IsPrimitive; + } + + private static bool IsReadableWritableAccessibility(Accessibility accessibility) + { + return accessibility is Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal; + } + + private static bool HasAccessibleParameterlessCtor(INamedTypeSymbol type) + { + foreach (IMethodSymbol ctor in type.InstanceConstructors) + { + if (ctor.Parameters.Length != 0) + { + continue; + } + + if (ctor.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal) + { + return true; + } + } + + return false; + } + + private static TypeResolution ResolveTypeResolution(ITypeSymbol type, FieldEncoding encoding) + { + TypeClassification baseType = ClassifyType(type); + if (encoding == FieldEncoding.None) + { + return new TypeResolution(true, baseType, null); + } + + bool isInt32 = type.SpecialType == SpecialType.System_Int32; + bool isUInt32 = type.SpecialType == SpecialType.System_UInt32; + bool isInt64 = type.SpecialType == SpecialType.System_Int64; + bool isUInt64 = type.SpecialType == SpecialType.System_UInt64; + + if (isInt32) + { + return encoding switch + { + FieldEncoding.Varint => new TypeResolution(true, baseType, null), + FieldEncoding.Fixed => new TypeResolution( + true, + new TypeClassification(4, true, true, false, false, false, 4), + "global::Apache.Fory.ForyInt32Fixed"), + _ => new TypeResolution(false, baseType, null), + }; + } + + if (isUInt32) + { + return encoding switch + { + FieldEncoding.Varint => new TypeResolution(true, baseType, null), + FieldEncoding.Fixed => new TypeResolution( + true, + new TypeClassification(11, true, true, false, false, false, 4), + "global::Apache.Fory.ForyUInt32Fixed"), + _ => new TypeResolution(false, baseType, null), + }; + } + + if (isInt64) + { + return encoding switch + { + FieldEncoding.Varint => new TypeResolution(true, baseType, null), + FieldEncoding.Fixed => new TypeResolution( + true, + new TypeClassification(6, true, true, false, false, false, 8), + "global::Apache.Fory.ForyInt64Fixed"), + FieldEncoding.Tagged => new TypeResolution( + true, + new TypeClassification(8, true, true, false, false, true, 8), + "global::Apache.Fory.ForyInt64Tagged"), + _ => new TypeResolution(false, baseType, null), + }; + } + + if (isUInt64) + { + return encoding switch + { + FieldEncoding.Varint => new TypeResolution(true, baseType, null), + FieldEncoding.Fixed => new TypeResolution( + true, + new TypeClassification(13, true, true, false, false, false, 8), + "global::Apache.Fory.ForyUInt64Fixed"), + FieldEncoding.Tagged => new TypeResolution( + true, + new TypeClassification(15, true, true, false, false, true, 8), + "global::Apache.Fory.ForyUInt64Tagged"), + _ => new TypeResolution(false, baseType, null), + }; + } + + return new TypeResolution(false, baseType, null); + } + + private static TypeClassification ClassifyType(ITypeSymbol type) + { + if (ResolveDynamicAnyKind(type) == DynamicAnyKind.AnyValue) + { + return new TypeClassification(0, false, true, false, false, false, 0); + } + + if (type.SpecialType == SpecialType.System_Boolean) + { + return new TypeClassification(1, true, true, false, false, false, 1); + } + + if (type.SpecialType == SpecialType.System_SByte) + { + return new TypeClassification(2, true, true, false, false, false, 1); + } + + if (type.SpecialType == SpecialType.System_Int16) + { + return new TypeClassification(3, true, true, false, false, false, 2); + } + + if (type.SpecialType == SpecialType.System_Int32) + { + return new TypeClassification(5, true, true, false, false, true, 4); + } + + if (type.SpecialType == SpecialType.System_Int64) + { + return new TypeClassification(7, true, true, false, false, true, 8); + } + + if (type.SpecialType == SpecialType.System_Byte) + { + return new TypeClassification(9, true, true, false, false, false, 1); + } + + if (type.SpecialType == SpecialType.System_UInt16) + { + return new TypeClassification(10, true, true, false, false, false, 2); + } + + if (type.SpecialType == SpecialType.System_UInt32) + { + return new TypeClassification(12, true, true, false, false, true, 4); + } + + if (type.SpecialType == SpecialType.System_UInt64) + { + return new TypeClassification(14, true, true, false, false, true, 8); + } + + if (type.SpecialType == SpecialType.System_Single) + { + return new TypeClassification(19, true, true, false, false, false, 4); + } + + if (type.SpecialType == SpecialType.System_Double) + { + return new TypeClassification(20, true, true, false, false, false, 8); + } + + if (type.SpecialType == SpecialType.System_String) + { + return new TypeClassification(21, false, true, false, false, false, 0); + } + + if (IsDateType(type)) + { + return new TypeClassification(39, false, true, false, false, false, 0); + } + + if (IsTimestampType(type)) + { + return new TypeClassification(38, false, true, false, false, false, 0); + } + + if (IsDurationType(type)) + { + return new TypeClassification(37, false, true, false, false, false, 0); + } + + if (type is IArrayTypeSymbol arrayType) + { + TypeClassification elem = ClassifyType(arrayType.ElementType); + uint typeId = elem.TypeId switch + { + 9 => 41, + 1 => 43, + 2 => 44, + 3 => 45, + 5 => 46, + 7 => 47, + 10 => 49, + 12 => 50, + 14 => 51, + 19 => 55, + 20 => 56, + _ => 22, + }; + return new TypeClassification(typeId, false, true, true, false, false, 0); + } + + if (TryGetListElementType(type, out _)) + { + return new TypeClassification(22, false, true, true, false, false, 0); + } + + if (TryGetSetElementType(type, out _)) + { + return new TypeClassification(23, false, true, true, false, false, 0); + } + + if (TryGetMapTypeArguments(type, out _, out _)) + { + return new TypeClassification(24, false, true, false, true, false, 0); + } + + if (IsUnionType(type)) + { + return new TypeClassification(33, false, false, false, false, false, 0); + } + + return new TypeClassification(27, false, false, false, false, false, 0); + } + + private static DynamicAnyKind ResolveDynamicAnyKind(ITypeSymbol type) + { + if (type.SpecialType == SpecialType.System_Object) + { + return DynamicAnyKind.AnyValue; + } + + if (TryGetListElementType(type, out ITypeSymbol? elementType)) + { + (bool _, ITypeSymbol unwrappedElement) = UnwrapNullable(elementType); + if (unwrappedElement.SpecialType == SpecialType.System_Object) + { + return DynamicAnyKind.AnyList; + } + } + + if (TryGetMapTypeArguments(type, out ITypeSymbol? keyType, out ITypeSymbol? valueType)) + { + (bool _, ITypeSymbol unwrappedValue) = UnwrapNullable(valueType); + if (unwrappedValue.SpecialType != SpecialType.System_Object) + { + return DynamicAnyKind.None; + } + + (bool _, ITypeSymbol unwrappedKey) = UnwrapNullable(keyType); + if (unwrappedKey.SpecialType == SpecialType.System_String) + { + return DynamicAnyKind.StringAnyMap; + } + + if (unwrappedKey.SpecialType == SpecialType.System_Int32) + { + return DynamicAnyKind.Int32AnyMap; + } + + return DynamicAnyKind.Unsupported; + } + + return DynamicAnyKind.None; + } + + private static bool IsDateType(ITypeSymbol symbol) + { + return string.Equals(symbol.ToDisplayString(), "System.DateOnly", StringComparison.Ordinal); + } + + private static bool IsTimestampType(ITypeSymbol symbol) + { + string name = symbol.ToDisplayString(); + return string.Equals(name, "System.DateTime", StringComparison.Ordinal) || + string.Equals(name, "System.DateTimeOffset", StringComparison.Ordinal); + } + + private static bool IsDurationType(ITypeSymbol symbol) + { + return string.Equals(symbol.ToDisplayString(), "System.TimeSpan", StringComparison.Ordinal); + } + + private static bool IsUnionType(ITypeSymbol symbol) + { + INamedTypeSymbol? current = symbol as INamedTypeSymbol; + while (current is not null) + { + if (string.Equals(current.ToDisplayString(), "Apache.Fory.Union", StringComparison.Ordinal)) + { + return true; + } + + current = current.BaseType; + } + + return false; + } + + private static bool TryGetListElementType(ITypeSymbol type, out ITypeSymbol? elementType) + { + elementType = null; + if (type is IArrayTypeSymbol arrayType) + { + elementType = arrayType.ElementType; + return true; + } + + if (type is not INamedTypeSymbol named) + { + return false; + } + + string genericName = named.ConstructedFrom.ToDisplayString(); + if (genericName is + "System.Collections.Generic.List" or + "System.Collections.Generic.IList" or + "System.Collections.Generic.IReadOnlyList") + { + elementType = named.TypeArguments[0]; + return true; + } + + return false; + } + + private static bool TryGetSetElementType(ITypeSymbol type, out ITypeSymbol? elementType) + { + elementType = null; + if (type is not INamedTypeSymbol named) + { + return false; + } + + string genericName = named.ConstructedFrom.ToDisplayString(); + if (genericName is + "System.Collections.Generic.HashSet" or + "System.Collections.Generic.ISet" or + "System.Collections.Generic.IReadOnlySet") + { + elementType = named.TypeArguments[0]; + return true; + } + + return false; + } + + private static bool TryGetMapTypeArguments(ITypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? valueType) + { + keyType = null; + valueType = null; + if (type is not INamedTypeSymbol named) + { + return false; + } + + string genericName = named.ConstructedFrom.ToDisplayString(); + if (genericName is + "System.Collections.Generic.Dictionary" or + "System.Collections.Generic.IDictionary" or + "System.Collections.Generic.IReadOnlyDictionary" or + "Apache.Fory.ForyMap") + { + keyType = named.TypeArguments[0]; + valueType = named.TypeArguments[1]; + return true; + } + + return false; + } + + private static (bool, ITypeSymbol) UnwrapNullable(ITypeSymbol type) + { + if (type is INamedTypeSymbol named && + named.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + return (true, named.TypeArguments[0]); + } + + if (type.IsReferenceType && type.NullableAnnotation == NullableAnnotation.Annotated) + { + return (true, type.WithNullableAnnotation(NullableAnnotation.NotAnnotated)); + } + + return (false, type); + } + + private static string BoolLiteral(bool value) => value ? "true" : "false"; + + private static string EscapeString(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\""); + + private static string ToSnakeCase(string name) + { + if (string.IsNullOrEmpty(name)) + { + return name; + } + + StringBuilder sb = new(name.Length + 4); + for (int i = 0; i < name.Length; i++) + { + char c = name[i]; + if (char.IsUpper(c)) + { + if (i > 0) + { + bool prevUpper = char.IsUpper(name[i - 1]); + bool nextUpperOrEnd = i + 1 >= name.Length || char.IsUpper(name[i + 1]); + if (!prevUpper || !nextUpperOrEnd) + { + sb.Append('_'); + } + } + + sb.Append(char.ToLowerInvariant(c)); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + private static string Sanitize(string name) + { + StringBuilder sb = new(name.Length + 8); + foreach (char c in name) + { + sb.Append(char.IsLetterOrDigit(c) ? c : '_'); + } + + return sb.ToString(); + } + + private sealed class TypeResolution + { + public TypeResolution(bool supported, TypeClassification classification, string? customWrapperTypeName) + { + Supported = supported; + Classification = classification; + CustomWrapperTypeName = customWrapperTypeName; + } + + public bool Supported { get; } + public TypeClassification Classification { get; } + public string? CustomWrapperTypeName { get; } + } + + private sealed class TypeClassification + { + public TypeClassification( + uint typeId, + bool isPrimitive, + bool isBuiltIn, + bool isCollection, + bool isMap, + bool isCompressedNumeric, + int primitiveSize) + { + TypeId = typeId; + IsPrimitive = isPrimitive; + IsBuiltIn = isBuiltIn; + IsCollection = isCollection; + IsMap = isMap; + IsCompressedNumeric = isCompressedNumeric; + PrimitiveSize = primitiveSize; + } + + public uint TypeId { get; } + public bool IsPrimitive { get; } + public bool IsBuiltIn { get; } + public bool IsCollection { get; } + public bool IsMap { get; } + public bool IsCompressedNumeric { get; } + public int PrimitiveSize { get; } + } + + private sealed class TypeMetaFieldTypeModel + { + public TypeMetaFieldTypeModel( + string typeIdExpr, + bool nullable, + bool trackRefByContext, + ImmutableArray generics) + { + TypeIdExpr = typeIdExpr; + Nullable = nullable; + TrackRefByContext = trackRefByContext; + Generics = generics; + } + + public string TypeIdExpr { get; } + public bool Nullable { get; } + public bool TrackRefByContext { get; } + public ImmutableArray Generics { get; } + } + + private sealed class TypeModel + { + public TypeModel( + string typeName, + string serializerName, + DeclKind kind, + ImmutableArray members, + ImmutableArray sortedMembers) + { + TypeName = typeName; + SerializerName = serializerName; + Kind = kind; + Members = members; + SortedMembers = sortedMembers; + } + + public string TypeName { get; } + public string SerializerName { get; } + public DeclKind Kind { get; } + public ImmutableArray Members { get; } + public ImmutableArray SortedMembers { get; } + } + + private sealed class MemberModel + { + public MemberModel( + string name, + string fieldIdentifier, + int originalIndex, + MemberDeclKind declKind, + string typeName, + bool isNullable, + bool isNullableValueType, + TypeClassification classification, + int group, + bool isCollection, + string? customWrapperTypeName, + DynamicAnyKind dynamicAnyKind, + TypeMetaFieldTypeModel typeMeta) + { + Name = name; + FieldIdentifier = fieldIdentifier; + OriginalIndex = originalIndex; + DeclKind = declKind; + TypeName = typeName; + IsNullable = isNullable; + IsNullableValueType = isNullableValueType; + Classification = classification; + Group = group; + IsCollection = isCollection; + CustomWrapperTypeName = customWrapperTypeName; + DynamicAnyKind = dynamicAnyKind; + TypeMeta = typeMeta; + } + + public string Name { get; } + public string FieldIdentifier { get; } + public int OriginalIndex { get; } + public MemberDeclKind DeclKind { get; } + public string TypeName { get; } + public bool IsNullable { get; } + public bool IsNullableValueType { get; } + public TypeClassification Classification { get; } + public int Group { get; } + public bool IsCollection { get; } + public string? CustomWrapperTypeName { get; } + public DynamicAnyKind DynamicAnyKind { get; } + public TypeMetaFieldTypeModel TypeMeta { get; } + } + + private enum MemberDeclKind + { + Field, + Property, + } + + private enum DeclKind + { + Unknown, + Class, + Struct, + Enum, + } + + private enum DynamicAnyKind + { + None, + AnyValue, + AnyList, + StringAnyMap, + Int32AnyMap, + Unsupported, + } + + private enum FieldEncoding + { + None = -1, + Varint = 0, + Fixed = 1, + Tagged = 2, + } +} diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs new file mode 100644 index 0000000000..c4b34010c6 --- /dev/null +++ b/csharp/src/Fory/AnySerializer.cs @@ -0,0 +1,627 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +internal static class DynamicAnyMapBits +{ + public const byte KeyNull = 0b0000_0010; + public const byte DeclaredKeyType = 0b0000_0100; + public const byte ValueNull = 0b0001_0000; +} + +public readonly struct ForyAnyNullValue +{ +} + +public sealed class ForyAnyNullValueSerializer : Serializer +{ + public static ForyAnyNullValueSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.None; + public override bool IsNullableType => true; + public override ForyAnyNullValue DefaultValue => new(); + public override bool IsNone(ForyAnyNullValue value) => true; + public override void WriteData(ref WriteContext context, in ForyAnyNullValue value, bool hasGenerics) + { + _ = context; + _ = value; + _ = hasGenerics; + } + + public override ForyAnyNullValue ReadData(ref ReadContext context) + { + _ = context; + return new ForyAnyNullValue(); + } +} + +public sealed class DynamicAnyObjectSerializer : Serializer +{ + public static DynamicAnyObjectSerializer Instance { get; } = new(); + + public override ForyTypeId StaticTypeId => ForyTypeId.Unknown; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override object? DefaultValue => new ForyAnyNullValue(); + public override bool IsNone(object? value) => value is null || value is ForyAnyNullValue; + + public override void WriteData(ref WriteContext context, in object? value, bool hasGenerics) + { + if (IsNone(value)) + { + return; + } + + DynamicAnyCodec.WriteAnyPayload(value!, ref context, hasGenerics); + } + + public override object? ReadData(ref ReadContext context) + { + DynamicTypeInfo? dynamicTypeInfo = context.DynamicTypeInfo(typeof(object)); + if (dynamicTypeInfo is null) + { + throw new ForyInvalidDataException("dynamic Any value requires type info"); + } + + context.ClearDynamicTypeInfo(typeof(object)); + if (dynamicTypeInfo.WireTypeId == ForyTypeId.None) + { + return new ForyAnyNullValue(); + } + + return context.TypeResolver.ReadDynamicValue(dynamicTypeInfo, ref context); + } + + public override void WriteTypeInfo(ref WriteContext context) + { + throw new ForyInvalidDataException("dynamic Any value type info is runtime-only"); + } + + public override void ReadTypeInfo(ref ReadContext context) + { + DynamicTypeInfo typeInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + context.SetDynamicTypeInfo(typeof(object), typeInfo); + } + + public override void Write(ref WriteContext context, in object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + if (refMode != RefMode.None) + { + if (IsNone(value)) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + return; + } + + bool wroteTrackingRefFlag = false; + if (refMode == RefMode.Tracking && AnyValueIsReferenceTrackable(value!)) + { + if (context.RefWriter.TryWriteReference(context.Writer, value!)) + { + return; + } + + wroteTrackingRefFlag = true; + } + + if (!wroteTrackingRefFlag) + { + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); + } + } + + if (writeTypeInfo) + { + DynamicAnyCodec.WriteAnyTypeInfo(value!, ref context); + } + + WriteData(ref context, value, hasGenerics); + } + + public override object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + if (refMode != RefMode.None) + { + sbyte rawFlag = context.Reader.ReadInt8(); + RefFlag flag = (RefFlag)rawFlag; + switch (flag) + { + case RefFlag.Null: + return new ForyAnyNullValue(); + case RefFlag.Ref: + { + uint refId = context.Reader.ReadVarUInt32(); + object? referenced = context.RefReader.ReadRefValue(refId); + return referenced ?? new ForyAnyNullValue(); + } + case RefFlag.RefValue: + { + uint reservedRefId = context.RefReader.ReserveRefId(); + context.PushPendingReference(reservedRefId); + if (readTypeInfo) + { + ReadTypeInfo(ref context); + } + + object? value = ReadData(ref context); + context.FinishPendingReferenceIfNeeded(value); + context.PopPendingReference(); + return value; + } + case RefFlag.NotNullValue: + break; + default: + throw new ForyRefException($"invalid ref flag {rawFlag}"); + } + } + + if (readTypeInfo) + { + ReadTypeInfo(ref context); + } + + return ReadData(ref context); + } + + private static bool AnyValueIsReferenceTrackable(object value) + { + ISerializer serializer = SerializerRegistry.Get(value.GetType()); + return serializer.IsReferenceTrackableType; + } + +} + +public static class DynamicAnyCodec +{ + internal static void WriteAnyTypeInfo(object value, ref WriteContext context) + { + if (value is ForyAnyNullValue) + { + context.Writer.WriteUInt8((byte)ForyTypeId.None); + return; + } + + if (value is IList) + { + context.Writer.WriteUInt8((byte)ForyTypeId.List); + return; + } + + if (value is IDictionary || value is IDictionary) + { + context.Writer.WriteUInt8((byte)ForyTypeId.Map); + return; + } + + ISerializer serializer = SerializerRegistry.Get(value.GetType()); + serializer.WriteTypeInfo(ref context); + } + + public static object? CastAnyDynamicValue(object? value, Type targetType) + { + if (value is null || value is ForyAnyNullValue) + { + if (targetType == typeof(object) || targetType == typeof(ForyAnyNullValue)) + { + return new ForyAnyNullValue(); + } + + if (!targetType.IsValueType || Nullable.GetUnderlyingType(targetType) is not null) + { + return null; + } + } + + if (value is null) + { + return null; + } + + if (targetType.IsInstanceOfType(value)) + { + return value; + } + + throw new ForyInvalidDataException($"cannot cast dynamic Any value to {targetType}"); + } + + public static void WriteAny(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo = true, bool hasGenerics = false) + { + DynamicAnyObjectSerializer.Instance.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + } + + public static object? ReadAny(ref ReadContext context, RefMode refMode, bool readTypeInfo = true) + { + object? value = DynamicAnyObjectSerializer.Instance.Read(ref context, refMode, readTypeInfo); + return value is ForyAnyNullValue ? null : value; + } + + public static void WriteAnyList(ref WriteContext context, IList? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) + { + List? wrapped = value is null ? null : [.. value]; + Serializer> serializer = new ListSerializer(); + serializer.Write(ref context, wrapped, refMode, writeTypeInfo, hasGenerics); + } + + public static List? ReadAnyList(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) + { + Serializer> serializer = new ListSerializer(); + List? wrapped = serializer.Read(ref context, refMode, readTypeInfo); + if (wrapped is null) + { + return null; + } + + for (int i = 0; i < wrapped.Count; i++) + { + if (wrapped[i] is ForyAnyNullValue) + { + wrapped[i] = null; + } + } + + return wrapped; + } + + public static void WriteStringAnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) + { + Dictionary? wrapped = value is null ? null : new Dictionary(value); + Serializer> serializer = new MapSerializer(); + serializer.Write(ref context, wrapped, refMode, writeTypeInfo, hasGenerics); + } + + public static Dictionary? ReadStringAnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) + { + Serializer> serializer = new MapSerializer(); + Dictionary? wrapped = serializer.Read(ref context, refMode, readTypeInfo); + if (wrapped is null) + { + return null; + } + + Dictionary normalized = new(wrapped.Count); + foreach ((string key, object? value) in wrapped) + { + normalized[key] = value is ForyAnyNullValue ? null : value; + } + + return normalized; + } + + public static void WriteInt32AnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) + { + Dictionary? wrapped = value is null ? null : new Dictionary(value); + Serializer> serializer = new MapSerializer(); + serializer.Write(ref context, wrapped, refMode, writeTypeInfo, hasGenerics); + } + + public static Dictionary? ReadInt32AnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) + { + Serializer> serializer = new MapSerializer(); + Dictionary? wrapped = serializer.Read(ref context, refMode, readTypeInfo); + if (wrapped is null) + { + return null; + } + + Dictionary normalized = new(wrapped.Count); + foreach ((int key, object? value) in wrapped) + { + normalized[key] = value is ForyAnyNullValue ? null : value; + } + + return normalized; + } + + public static object ReadDynamicAnyMapValue(ref ReadContext context) + { + int mapStart = context.Reader.Cursor; + ForyTypeId? keyTypeId = PeekDynamicMapKeyTypeId(ref context); + context.Reader.SetCursor(mapStart); + return keyTypeId switch + { + ForyTypeId.Int32 or ForyTypeId.VarInt32 => ReadInt32AnyMap(ref context, RefMode.None, false) ?? new Dictionary(), + ForyTypeId.String or null => ReadStringAnyMap(ref context, RefMode.None, false) ?? new Dictionary(), + _ => throw new ForyInvalidDataException($"unsupported dynamic map key type {keyTypeId}"), + }; + } + + private static ForyTypeId? PeekDynamicMapKeyTypeId(ref ReadContext context) + { + int start = context.Reader.Cursor; + try + { + int length = checked((int)context.Reader.ReadVarUInt32()); + if (length == 0) + { + return null; + } + + byte header = context.Reader.ReadUInt8(); + bool keyNull = (header & DynamicAnyMapBits.KeyNull) != 0; + bool keyDeclared = (header & DynamicAnyMapBits.DeclaredKeyType) != 0; + bool valueNull = (header & DynamicAnyMapBits.ValueNull) != 0; + if (keyDeclared || keyNull) + { + return null; + } + + if (!valueNull) + { + _ = context.Reader.ReadUInt8(); + } + + uint rawTypeId = context.Reader.ReadVarUInt32(); + return Enum.IsDefined(typeof(ForyTypeId), rawTypeId) ? (ForyTypeId)rawTypeId : null; + } + finally + { + context.Reader.SetCursor(start); + } + } + + public static void WriteAnyPayload(object value, ref WriteContext context, bool hasGenerics) + { + if (value is IList list) + { + WriteAnyList(ref context, list, RefMode.None, false, hasGenerics); + return; + } + + if (value is IDictionary stringMap) + { + WriteStringAnyMap(ref context, stringMap, RefMode.None, false, false); + return; + } + + if (value is IDictionary intMap) + { + WriteInt32AnyMap(ref context, intMap, RefMode.None, false, false); + return; + } + + ISerializer serializer = SerializerRegistry.Get(value.GetType()); + serializer.WriteData(ref context, value, hasGenerics); + } + + public static void WriteCollectionData( + IEnumerable values, + Serializer elementSerializer, + ref WriteContext context, + bool hasGenerics) + { + List list = values as List ?? [.. values]; + context.Writer.WriteVarUInt32((uint)list.Count); + if (list.Count == 0) + { + return; + } + + bool hasNull = elementSerializer.IsNullableType && list.Any(v => elementSerializer.IsNoneObject(v)); + bool trackRef = context.TrackRef && elementSerializer.IsReferenceTrackableType; + bool declaredElementType = hasGenerics && !elementSerializer.StaticTypeId.NeedsTypeInfoForField(); + bool dynamicElementType = elementSerializer.StaticTypeId == ForyTypeId.Unknown; + + byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType; + if (trackRef) + { + header |= CollectionBits.TrackingRef; + } + + if (hasNull) + { + header |= CollectionBits.HasNull; + } + + if (declaredElementType) + { + header |= CollectionBits.DeclaredElementType; + } + + context.Writer.WriteUInt8(header); + if (!dynamicElementType && !declaredElementType) + { + elementSerializer.WriteTypeInfo(ref context); + } + + if (dynamicElementType) + { + RefMode refMode = trackRef ? RefMode.Tracking : hasNull ? RefMode.NullOnly : RefMode.None; + foreach (T element in list) + { + elementSerializer.Write(ref context, element, refMode, true, hasGenerics); + } + + return; + } + + if (trackRef) + { + foreach (T element in list) + { + elementSerializer.Write(ref context, element, RefMode.Tracking, false, hasGenerics); + } + + return; + } + + if (hasNull) + { + foreach (T element in list) + { + if (elementSerializer.IsNoneObject(element)) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + } + else + { + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); + elementSerializer.WriteData(ref context, element, hasGenerics); + } + } + + return; + } + + foreach (T element in list) + { + elementSerializer.WriteData(ref context, element, hasGenerics); + } + } + + public static List ReadCollectionData(Serializer elementSerializer, ref ReadContext context) + { + int length = checked((int)context.Reader.ReadVarUInt32()); + if (length == 0) + { + return []; + } + + byte header = context.Reader.ReadUInt8(); + bool trackRef = (header & CollectionBits.TrackingRef) != 0; + bool hasNull = (header & CollectionBits.HasNull) != 0; + bool declared = (header & CollectionBits.DeclaredElementType) != 0; + bool sameType = (header & CollectionBits.SameType) != 0; + bool canonicalizeElements = context.TrackRef && !trackRef && elementSerializer.IsReferenceTrackableType; + + List values = new(length); + if (!sameType) + { + if (trackRef) + { + for (int i = 0; i < length; i++) + { + values.Add((T)elementSerializer.Read(ref context, RefMode.Tracking, true)!); + } + + return values; + } + + if (hasNull) + { + for (int i = 0; i < length; i++) + { + sbyte refFlag = context.Reader.ReadInt8(); + if (refFlag == (sbyte)RefFlag.Null) + { + values.Add((T)elementSerializer.DefaultObject!); + } + else if (refFlag == (sbyte)RefFlag.NotNullValue) + { + values.Add(ReadCollectionElementWithCanonicalization(elementSerializer, ref context, true, canonicalizeElements)); + } + else + { + throw new ForyRefException($"invalid nullability flag {refFlag}"); + } + } + } + else + { + for (int i = 0; i < length; i++) + { + values.Add(ReadCollectionElementWithCanonicalization(elementSerializer, ref context, true, canonicalizeElements)); + } + } + + return values; + } + + if (!declared) + { + elementSerializer.ReadTypeInfo(ref context); + } + + if (trackRef) + { + for (int i = 0; i < length; i++) + { + values.Add((T)elementSerializer.Read(ref context, RefMode.Tracking, false)!); + } + + if (!declared) + { + context.ClearDynamicTypeInfo(typeof(T)); + } + + return values; + } + + if (hasNull) + { + for (int i = 0; i < length; i++) + { + sbyte refFlag = context.Reader.ReadInt8(); + if (refFlag == (sbyte)RefFlag.Null) + { + values.Add((T)elementSerializer.DefaultObject!); + } + else + { + values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, ref context, canonicalizeElements)); + } + } + } + else + { + for (int i = 0; i < length; i++) + { + values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, ref context, canonicalizeElements)); + } + } + + if (!declared) + { + context.ClearDynamicTypeInfo(typeof(T)); + } + + return values; + } + + private static T ReadCollectionElementWithCanonicalization( + Serializer elementSerializer, + ref ReadContext context, + bool readTypeInfo, + bool canonicalize) + { + if (!canonicalize) + { + return (T)elementSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + } + + int start = context.Reader.Cursor; + T value = (T)elementSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + int end = context.Reader.Cursor; + return context.CanonicalizeNonTrackingReference(value, start, end); + } + + private static T ReadCollectionElementDataWithCanonicalization( + Serializer elementSerializer, + ref ReadContext context, + bool canonicalize) + { + if (!canonicalize) + { + return elementSerializer.ReadData(ref context); + } + + int start = context.Reader.Cursor; + T value = elementSerializer.ReadData(ref context); + int end = context.Reader.Cursor; + return context.CanonicalizeNonTrackingReference(value, start, end); + } +} diff --git a/csharp/src/Fory/Attributes.cs b/csharp/src/Fory/Attributes.cs new file mode 100644 index 0000000000..89fa8854bf --- /dev/null +++ b/csharp/src/Fory/Attributes.cs @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] +public sealed class ForyObjectAttribute : Attribute +{ +} + +public enum ForyFieldEncoding +{ + Varint, + Fixed, + Tagged, +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class ForyFieldAttribute : Attribute +{ + public ForyFieldAttribute(ForyFieldEncoding encoding) + { + Encoding = encoding; + } + + public ForyFieldEncoding Encoding { get; } +} diff --git a/csharp/src/Fory/ByteBuffer.cs b/csharp/src/Fory/ByteBuffer.cs new file mode 100644 index 0000000000..3f32406045 --- /dev/null +++ b/csharp/src/Fory/ByteBuffer.cs @@ -0,0 +1,418 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Buffers.Binary; + +namespace Apache.Fory; + +public sealed class ByteWriter +{ + private readonly List _storage; + + public ByteWriter(int capacity = 256) + { + _storage = new List(capacity); + } + + public int Count => _storage.Count; + + public IReadOnlyList Storage => _storage; + + public void Reserve(int additional) + { + _storage.Capacity = Math.Max(_storage.Capacity, _storage.Count + additional); + } + + public void WriteUInt8(byte value) + { + _storage.Add(value); + } + + public void WriteInt8(sbyte value) + { + _storage.Add(unchecked((byte)value)); + } + + public void WriteUInt16(ushort value) + { + Span tmp = stackalloc byte[2]; + BinaryPrimitives.WriteUInt16LittleEndian(tmp, value); + WriteBytes(tmp); + } + + public void WriteInt16(short value) + { + WriteUInt16(unchecked((ushort)value)); + } + + public void WriteUInt32(uint value) + { + Span tmp = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(tmp, value); + WriteBytes(tmp); + } + + public void WriteInt32(int value) + { + WriteUInt32(unchecked((uint)value)); + } + + public void WriteUInt64(ulong value) + { + Span tmp = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(tmp, value); + WriteBytes(tmp); + } + + public void WriteInt64(long value) + { + WriteUInt64(unchecked((ulong)value)); + } + + public void WriteVarUInt32(uint value) + { + uint remaining = value; + while (remaining >= 0x80) + { + WriteUInt8((byte)((remaining & 0x7F) | 0x80)); + remaining >>= 7; + } + + WriteUInt8((byte)remaining); + } + + public void WriteVarUInt64(ulong value) + { + ulong remaining = value; + for (var i = 0; i < 8; i++) + { + if (remaining < 0x80) + { + WriteUInt8((byte)remaining); + return; + } + + WriteUInt8((byte)((remaining & 0x7F) | 0x80)); + remaining >>= 7; + } + + WriteUInt8((byte)(remaining & 0xFF)); + } + + public void WriteVarUInt36Small(ulong value) + { + if (value >= (1UL << 36)) + { + throw new ForyEncodingException("varuint36small overflow"); + } + + WriteVarUInt64(value); + } + + public void WriteVarInt32(int value) + { + uint zigzag = unchecked((uint)((value << 1) ^ (value >> 31))); + WriteVarUInt32(zigzag); + } + + public void WriteVarInt64(long value) + { + ulong zigzag = unchecked((ulong)((value << 1) ^ (value >> 63))); + WriteVarUInt64(zigzag); + } + + public void WriteTaggedInt64(long value) + { + if (value >= -1_073_741_824L && value <= 1_073_741_823L) + { + WriteInt32(unchecked((int)value << 1)); + return; + } + + WriteUInt8(0x01); + WriteInt64(value); + } + + public void WriteTaggedUInt64(ulong value) + { + if (value <= int.MaxValue) + { + WriteUInt32(unchecked((uint)value << 1)); + return; + } + + WriteUInt8(0x01); + WriteUInt64(value); + } + + public void WriteFloat32(float value) + { + WriteUInt32(unchecked((uint)BitConverter.SingleToInt32Bits(value))); + } + + public void WriteFloat64(double value) + { + WriteUInt64(unchecked((ulong)BitConverter.DoubleToInt64Bits(value))); + } + + public void WriteBytes(ReadOnlySpan bytes) + { + for (int i = 0; i < bytes.Length; i++) + { + _storage.Add(bytes[i]); + } + } + + public void SetByte(int index, byte value) + { + _storage[index] = value; + } + + public void SetBytes(int index, ReadOnlySpan bytes) + { + for (var i = 0; i < bytes.Length; i++) + { + _storage[index + i] = bytes[i]; + } + } + + public byte[] ToArray() + { + return _storage.ToArray(); + } + + public void Reset() + { + _storage.Clear(); + } +} + +public sealed class ByteReader +{ + private readonly byte[] _storage; + private int _cursor; + + public ByteReader(ReadOnlySpan data) + { + _storage = data.ToArray(); + _cursor = 0; + } + + public ByteReader(byte[] bytes) + { + _storage = bytes; + _cursor = 0; + } + + public byte[] Storage => _storage; + + public int Cursor => _cursor; + + public int Remaining => _storage.Length - _cursor; + + public void SetCursor(int value) + { + _cursor = value; + } + + public void MoveBack(int amount) + { + _cursor -= amount; + } + + public void CheckBound(int need) + { + if (_cursor + need > _storage.Length) + { + throw new ForyOutOfBoundsException(_cursor, need, _storage.Length); + } + } + + public byte ReadUInt8() + { + CheckBound(1); + byte value = _storage[_cursor]; + _cursor += 1; + return value; + } + + public sbyte ReadInt8() + { + return unchecked((sbyte)ReadUInt8()); + } + + public ushort ReadUInt16() + { + CheckBound(2); + ushort value = BinaryPrimitives.ReadUInt16LittleEndian(_storage.AsSpan(_cursor, 2)); + _cursor += 2; + return value; + } + + public short ReadInt16() + { + return unchecked((short)ReadUInt16()); + } + + public uint ReadUInt32() + { + CheckBound(4); + uint value = BinaryPrimitives.ReadUInt32LittleEndian(_storage.AsSpan(_cursor, 4)); + _cursor += 4; + return value; + } + + public int ReadInt32() + { + return unchecked((int)ReadUInt32()); + } + + public ulong ReadUInt64() + { + CheckBound(8); + ulong value = BinaryPrimitives.ReadUInt64LittleEndian(_storage.AsSpan(_cursor, 8)); + _cursor += 8; + return value; + } + + public long ReadInt64() + { + return unchecked((long)ReadUInt64()); + } + + public uint ReadVarUInt32() + { + uint result = 0; + var shift = 0; + while (true) + { + byte b = ReadUInt8(); + result |= (uint)(b & 0x7F) << shift; + if ((b & 0x80) == 0) + { + return result; + } + + shift += 7; + if (shift > 28) + { + throw new ForyEncodingException("varuint32 overflow"); + } + } + } + + public ulong ReadVarUInt64() + { + ulong result = 0; + var shift = 0; + for (var i = 0; i < 8; i++) + { + byte b = ReadUInt8(); + result |= (ulong)(b & 0x7F) << shift; + if ((b & 0x80) == 0) + { + return result; + } + + shift += 7; + } + + byte last = ReadUInt8(); + result |= (ulong)last << 56; + return result; + } + + public ulong ReadVarUInt36Small() + { + ulong value = ReadVarUInt64(); + if (value >= (1UL << 36)) + { + throw new ForyEncodingException("varuint36small overflow"); + } + + return value; + } + + public int ReadVarInt32() + { + uint encoded = ReadVarUInt32(); + return unchecked((int)((encoded >> 1) ^ (~(encoded & 1) + 1))); + } + + public long ReadVarInt64() + { + ulong encoded = ReadVarUInt64(); + return unchecked((long)((encoded >> 1) ^ (~(encoded & 1UL) + 1UL))); + } + + public long ReadTaggedInt64() + { + int first = ReadInt32(); + if ((first & 1) == 0) + { + return first >> 1; + } + + MoveBack(3); + return ReadInt64(); + } + + public ulong ReadTaggedUInt64() + { + uint first = ReadUInt32(); + if ((first & 1) == 0) + { + return first >> 1; + } + + MoveBack(3); + return ReadUInt64(); + } + + public float ReadFloat32() + { + return BitConverter.Int32BitsToSingle(unchecked((int)ReadUInt32())); + } + + public double ReadFloat64() + { + return BitConverter.Int64BitsToDouble(unchecked((long)ReadUInt64())); + } + + public byte[] ReadBytes(int count) + { + CheckBound(count); + byte[] result = new byte[count]; + Array.Copy(_storage, _cursor, result, 0, count); + _cursor += count; + return result; + } + + public ReadOnlySpan ReadSpan(int count) + { + CheckBound(count); + ReadOnlySpan span = _storage.AsSpan(_cursor, count); + _cursor += count; + return span; + } + + public void Skip(int count) + { + CheckBound(count); + _cursor += count; + } +} diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs new file mode 100644 index 0000000000..a3601fdaf5 --- /dev/null +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -0,0 +1,837 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +internal static class CollectionBits +{ + public const byte TrackingRef = 0b0000_0001; + public const byte HasNull = 0b0000_0010; + public const byte DeclaredElementType = 0b0000_0100; + public const byte SameType = 0b0000_1000; +} + +internal static class MapBits +{ + public const byte TrackingKeyRef = 0b0000_0001; + public const byte KeyNull = 0b0000_0010; + public const byte DeclaredKeyType = 0b0000_0100; + public const byte TrackingValueRef = 0b0000_1000; + public const byte ValueNull = 0b0001_0000; + public const byte DeclaredValueType = 0b0010_0000; +} + +internal static class PrimitiveArrayCodec +{ + public static ForyTypeId? PrimitiveArrayTypeId(Type elementType) + { + if (elementType == typeof(byte)) + { + return ForyTypeId.Binary; + } + + if (elementType == typeof(bool)) + { + return ForyTypeId.BoolArray; + } + + if (elementType == typeof(sbyte)) + { + return ForyTypeId.Int8Array; + } + + if (elementType == typeof(short)) + { + return ForyTypeId.Int16Array; + } + + if (elementType == typeof(int)) + { + return ForyTypeId.Int32Array; + } + + if (elementType == typeof(long)) + { + return ForyTypeId.Int64Array; + } + + if (elementType == typeof(ushort)) + { + return ForyTypeId.UInt16Array; + } + + if (elementType == typeof(uint)) + { + return ForyTypeId.UInt32Array; + } + + if (elementType == typeof(ulong)) + { + return ForyTypeId.UInt64Array; + } + + if (elementType == typeof(float)) + { + return ForyTypeId.Float32Array; + } + + if (elementType == typeof(double)) + { + return ForyTypeId.Float64Array; + } + + return null; + } + + public static void WritePrimitiveArray(T[] value, ref WriteContext context) + { + if (typeof(T) == typeof(byte)) + { + byte[] bytes = (byte[])(object)value; + context.Writer.WriteVarUInt32((uint)bytes.Length); + context.Writer.WriteBytes(bytes); + return; + } + + if (typeof(T) == typeof(bool)) + { + bool[] values = (bool[])(object)value; + context.Writer.WriteVarUInt32((uint)values.Length); + foreach (bool item in values) + { + context.Writer.WriteUInt8(item ? (byte)1 : (byte)0); + } + + return; + } + + if (typeof(T) == typeof(sbyte)) + { + sbyte[] values = (sbyte[])(object)value; + context.Writer.WriteVarUInt32((uint)values.Length); + foreach (sbyte item in values) + { + context.Writer.WriteInt8(item); + } + + return; + } + + if (typeof(T) == typeof(short)) + { + short[] values = (short[])(object)value; + context.Writer.WriteVarUInt32((uint)(values.Length * 2)); + foreach (short item in values) + { + context.Writer.WriteInt16(item); + } + + return; + } + + if (typeof(T) == typeof(int)) + { + int[] values = (int[])(object)value; + context.Writer.WriteVarUInt32((uint)(values.Length * 4)); + foreach (int item in values) + { + context.Writer.WriteInt32(item); + } + + return; + } + + if (typeof(T) == typeof(uint)) + { + uint[] values = (uint[])(object)value; + context.Writer.WriteVarUInt32((uint)(values.Length * 4)); + foreach (uint item in values) + { + context.Writer.WriteUInt32(item); + } + + return; + } + + if (typeof(T) == typeof(long)) + { + long[] values = (long[])(object)value; + context.Writer.WriteVarUInt32((uint)(values.Length * 8)); + foreach (long item in values) + { + context.Writer.WriteInt64(item); + } + + return; + } + + if (typeof(T) == typeof(ulong)) + { + ulong[] values = (ulong[])(object)value; + context.Writer.WriteVarUInt32((uint)(values.Length * 8)); + foreach (ulong item in values) + { + context.Writer.WriteUInt64(item); + } + + return; + } + + if (typeof(T) == typeof(ushort)) + { + ushort[] values = (ushort[])(object)value; + context.Writer.WriteVarUInt32((uint)(values.Length * 2)); + foreach (ushort item in values) + { + context.Writer.WriteUInt16(item); + } + + return; + } + + if (typeof(T) == typeof(float)) + { + float[] values = (float[])(object)value; + context.Writer.WriteVarUInt32((uint)(values.Length * 4)); + foreach (float item in values) + { + context.Writer.WriteFloat32(item); + } + + return; + } + + double[] doubles = (double[])(object)value; + context.Writer.WriteVarUInt32((uint)(doubles.Length * 8)); + foreach (double item in doubles) + { + context.Writer.WriteFloat64(item); + } + } + + public static T[] ReadPrimitiveArray(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if (typeof(T) == typeof(byte)) + { + return (T[])(object)context.Reader.ReadBytes(payloadSize); + } + + if (typeof(T) == typeof(bool)) + { + bool[] outValues = new bool[payloadSize]; + for (int i = 0; i < payloadSize; i++) + { + outValues[i] = context.Reader.ReadUInt8() != 0; + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(sbyte)) + { + sbyte[] outValues = new sbyte[payloadSize]; + for (int i = 0; i < payloadSize; i++) + { + outValues[i] = context.Reader.ReadInt8(); + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(short)) + { + if ((payloadSize & 1) != 0) + { + throw new ForyInvalidDataException("int16 array payload size mismatch"); + } + + short[] outValues = new short[payloadSize / 2]; + for (int i = 0; i < outValues.Length; i++) + { + outValues[i] = context.Reader.ReadInt16(); + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(int)) + { + if ((payloadSize & 3) != 0) + { + throw new ForyInvalidDataException("int32 array payload size mismatch"); + } + + int[] outValues = new int[payloadSize / 4]; + for (int i = 0; i < outValues.Length; i++) + { + outValues[i] = context.Reader.ReadInt32(); + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(uint)) + { + if ((payloadSize & 3) != 0) + { + throw new ForyInvalidDataException("uint32 array payload size mismatch"); + } + + uint[] outValues = new uint[payloadSize / 4]; + for (int i = 0; i < outValues.Length; i++) + { + outValues[i] = context.Reader.ReadUInt32(); + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(long)) + { + if ((payloadSize & 7) != 0) + { + throw new ForyInvalidDataException("int64 array payload size mismatch"); + } + + long[] outValues = new long[payloadSize / 8]; + for (int i = 0; i < outValues.Length; i++) + { + outValues[i] = context.Reader.ReadInt64(); + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(ulong)) + { + if ((payloadSize & 7) != 0) + { + throw new ForyInvalidDataException("uint64 array payload size mismatch"); + } + + ulong[] outValues = new ulong[payloadSize / 8]; + for (int i = 0; i < outValues.Length; i++) + { + outValues[i] = context.Reader.ReadUInt64(); + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(ushort)) + { + if ((payloadSize & 1) != 0) + { + throw new ForyInvalidDataException("uint16 array payload size mismatch"); + } + + ushort[] outValues = new ushort[payloadSize / 2]; + for (int i = 0; i < outValues.Length; i++) + { + outValues[i] = context.Reader.ReadUInt16(); + } + + return (T[])(object)outValues; + } + + if (typeof(T) == typeof(float)) + { + if ((payloadSize & 3) != 0) + { + throw new ForyInvalidDataException("float32 array payload size mismatch"); + } + + float[] outValues = new float[payloadSize / 4]; + for (int i = 0; i < outValues.Length; i++) + { + outValues[i] = context.Reader.ReadFloat32(); + } + + return (T[])(object)outValues; + } + + if ((payloadSize & 7) != 0) + { + throw new ForyInvalidDataException("float64 array payload size mismatch"); + } + + double[] doubles = new double[payloadSize / 8]; + for (int i = 0; i < doubles.Length; i++) + { + doubles[i] = context.Reader.ReadFloat64(); + } + + return (T[])(object)doubles; + } +} + +public sealed class ArraySerializer : Serializer +{ + private readonly Serializer _elementSerializer = SerializerRegistry.Get(); + private readonly ForyTypeId? _primitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); + + public override ForyTypeId StaticTypeId => _primitiveArrayTypeId ?? ForyTypeId.List; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override T[] DefaultValue => null!; + public override bool IsNone(T[] value) => value is null; + + public override void WriteData(ref WriteContext context, in T[] value, bool hasGenerics) + { + T[] safe = value ?? []; + if (_primitiveArrayTypeId is not null) + { + PrimitiveArrayCodec.WritePrimitiveArray(safe, ref context); + return; + } + + DynamicAnyCodec.WriteCollectionData( + safe, + _elementSerializer, + ref context, + hasGenerics); + } + + public override T[] ReadData(ref ReadContext context) + { + if (_primitiveArrayTypeId is not null) + { + return PrimitiveArrayCodec.ReadPrimitiveArray(ref context); + } + + List values = DynamicAnyCodec.ReadCollectionData(_elementSerializer, ref context); + return values.ToArray(); + } +} + +public sealed class ListSerializer : Serializer> +{ + private readonly Serializer _elementSerializer = SerializerRegistry.Get(); + + public override ForyTypeId StaticTypeId => ForyTypeId.List; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override List DefaultValue => null!; + public override bool IsNone(List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List safe = value ?? []; + DynamicAnyCodec.WriteCollectionData(safe, _elementSerializer, ref context, hasGenerics); + } + + public override List ReadData(ref ReadContext context) + { + return DynamicAnyCodec.ReadCollectionData(_elementSerializer, ref context); + } +} + +public sealed class SetSerializer : Serializer> where T : notnull +{ + private readonly ListSerializer _listSerializer = new(); + + public override ForyTypeId StaticTypeId => ForyTypeId.Set; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override HashSet DefaultValue => null!; + public override bool IsNone(HashSet value) => value is null; + + public override void WriteData(ref WriteContext context, in HashSet value, bool hasGenerics) + { + List list = value is null ? [] : [.. value]; + _listSerializer.WriteData(ref context, list, hasGenerics); + } + + public override HashSet ReadData(ref ReadContext context) + { + return [.. _listSerializer.ReadData(ref context)]; + } +} + +public sealed class MapSerializer : Serializer> + where TKey : notnull +{ + private readonly Serializer _keySerializer = SerializerRegistry.Get(); + private readonly Serializer _valueSerializer = SerializerRegistry.Get(); + + public override ForyTypeId StaticTypeId => ForyTypeId.Map; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override Dictionary DefaultValue => null!; + public override bool IsNone(Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + context.Writer.WriteVarUInt32((uint)map.Count); + if (map.Count == 0) + { + return; + } + + bool trackKeyRef = context.TrackRef && _keySerializer.IsReferenceTrackableType; + bool trackValueRef = context.TrackRef && _valueSerializer.IsReferenceTrackableType; + bool keyDeclared = hasGenerics && !_keySerializer.StaticTypeId.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !_valueSerializer.StaticTypeId.NeedsTypeInfoForField(); + bool keyDynamicType = _keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = _valueSerializer.StaticTypeId == ForyTypeId.Unknown; + + KeyValuePair[] pairs = [.. map]; + if (keyDynamicType || valueDynamicType) + { + WriteDynamicMapPairs(pairs, ref context, hasGenerics, trackKeyRef, trackValueRef, keyDeclared, valueDeclared, keyDynamicType, valueDynamicType); + return; + } + + int index = 0; + while (index < pairs.Length) + { + KeyValuePair pair = pairs[index]; + bool keyIsNull = _keySerializer.IsNoneObject(pair.Key); + bool valueIsNull = _valueSerializer.IsNoneObject(pair.Value); + if (keyIsNull || valueIsNull) + { + byte header = 0; + if (trackKeyRef) + { + header |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + header |= MapBits.TrackingValueRef; + } + + if (keyIsNull) + { + header |= MapBits.KeyNull; + } + + if (valueIsNull) + { + header |= MapBits.ValueNull; + } + + if (!keyIsNull && keyDeclared) + { + header |= MapBits.DeclaredKeyType; + } + + if (!valueIsNull && valueDeclared) + { + header |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + if (!keyIsNull) + { + if (!keyDeclared) + { + _keySerializer.WriteTypeInfo(ref context); + } + + _keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + } + + if (!valueIsNull) + { + if (!valueDeclared) + { + _valueSerializer.WriteTypeInfo(ref context); + } + + _valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + } + + index += 1; + continue; + } + + byte blockHeader = 0; + if (trackKeyRef) + { + blockHeader |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + blockHeader |= MapBits.TrackingValueRef; + } + + if (keyDeclared) + { + blockHeader |= MapBits.DeclaredKeyType; + } + + if (valueDeclared) + { + blockHeader |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(blockHeader); + int chunkSizeOffset = context.Writer.Count; + context.Writer.WriteUInt8(0); + if (!keyDeclared) + { + _keySerializer.WriteTypeInfo(ref context); + } + + if (!valueDeclared) + { + _valueSerializer.WriteTypeInfo(ref context); + } + + byte chunkSize = 0; + while (index < pairs.Length && chunkSize < byte.MaxValue) + { + KeyValuePair current = pairs[index]; + if (_keySerializer.IsNoneObject(current.Key) || _valueSerializer.IsNoneObject(current.Value)) + { + break; + } + + _keySerializer.Write(ref context, current.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + _valueSerializer.Write(ref context, current.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + chunkSize += 1; + index += 1; + } + + context.Writer.SetByte(chunkSizeOffset, chunkSize); + } + } + + public override Dictionary ReadData(ref ReadContext context) + { + int totalLength = checked((int)context.Reader.ReadVarUInt32()); + if (totalLength == 0) + { + return []; + } + + Dictionary map = new(totalLength); + bool keyDynamicType = _keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = _valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool canonicalizeValues = context.TrackRef && _valueSerializer.IsReferenceTrackableType; + + int readCount = 0; + while (readCount < totalLength) + { + byte header = context.Reader.ReadUInt8(); + bool trackKeyRef = (header & MapBits.TrackingKeyRef) != 0; + bool keyNull = (header & MapBits.KeyNull) != 0; + bool keyDeclared = (header & MapBits.DeclaredKeyType) != 0; + bool trackValueRef = (header & MapBits.TrackingValueRef) != 0; + bool valueNull = (header & MapBits.ValueNull) != 0; + bool valueDeclared = (header & MapBits.DeclaredValueType) != 0; + + if (keyNull && valueNull) + { + map[(TKey)_keySerializer.DefaultObject!] = (TValue)_valueSerializer.DefaultObject!; + readCount += 1; + continue; + } + + if (keyNull) + { + TValue value = ReadValueElement( + ref context, + trackValueRef, + valueDynamicType || !valueDeclared, + canonicalizeValues, + _valueSerializer); + map[(TKey)_keySerializer.DefaultObject!] = value; + readCount += 1; + continue; + } + + if (valueNull) + { + TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared)!; + map[key] = (TValue)_valueSerializer.DefaultObject!; + readCount += 1; + continue; + } + + int chunkSize = context.Reader.ReadUInt8(); + if (!keyDeclared) + { + _keySerializer.ReadTypeInfo(ref context); + } + + if (!valueDeclared) + { + _valueSerializer.ReadTypeInfo(ref context); + } + + for (int i = 0; i < chunkSize; i++) + { + TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false)!; + TValue value = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, _valueSerializer); + map[key] = value; + } + + if (!keyDeclared) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + + if (!valueDeclared) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + + readCount += chunkSize; + } + + return map; + } + + private void WriteDynamicMapPairs( + KeyValuePair[] pairs, + ref WriteContext context, + bool hasGenerics, + bool trackKeyRef, + bool trackValueRef, + bool keyDeclared, + bool valueDeclared, + bool keyDynamicType, + bool valueDynamicType) + { + foreach (KeyValuePair pair in pairs) + { + bool keyIsNull = _keySerializer.IsNoneObject(pair.Key); + bool valueIsNull = _valueSerializer.IsNoneObject(pair.Value); + byte header = 0; + if (trackKeyRef) + { + header |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + header |= MapBits.TrackingValueRef; + } + + if (keyIsNull) + { + header |= MapBits.KeyNull; + } + else if (!keyDynamicType && keyDeclared) + { + header |= MapBits.DeclaredKeyType; + } + + if (valueIsNull) + { + header |= MapBits.ValueNull; + } + else if (!valueDynamicType && valueDeclared) + { + header |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + if (keyIsNull && valueIsNull) + { + continue; + } + + if (keyIsNull) + { + if (!valueDeclared) + { + if (valueDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Value!, ref context); + } + else + { + _valueSerializer.WriteTypeInfo(ref context); + } + } + + _valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + continue; + } + + if (valueIsNull) + { + if (!keyDeclared) + { + if (keyDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Key!, ref context); + } + else + { + _keySerializer.WriteTypeInfo(ref context); + } + } + + _keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + continue; + } + + context.Writer.WriteUInt8(1); + if (!keyDeclared) + { + if (keyDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Key!, ref context); + } + else + { + _keySerializer.WriteTypeInfo(ref context); + } + } + + if (!valueDeclared) + { + if (valueDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Value!, ref context); + } + else + { + _valueSerializer.WriteTypeInfo(ref context); + } + } + + _keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + _valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + } + } + + private static TValue ReadValueElement( + ref ReadContext context, + bool trackValueRef, + bool readTypeInfo, + bool canonicalizeValues, + ISerializer valueSerializer) + { + if (trackValueRef || !canonicalizeValues) + { + return (TValue)valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo)!; + } + + int start = context.Reader.Cursor; + TValue value = (TValue)valueSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + int end = context.Reader.Cursor; + return context.CanonicalizeNonTrackingReference(value, start, end); + } +} diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs new file mode 100644 index 0000000000..4f7d5c45d4 --- /dev/null +++ b/csharp/src/Fory/Context.cs @@ -0,0 +1,397 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public sealed class CompatibleTypeDefWriteState +{ + private readonly Dictionary _typeIndexByType = []; + private uint _nextIndex; + + public uint? LookupIndex(Type type) + { + return _typeIndexByType.TryGetValue(type, out uint idx) ? idx : null; + } + + public (uint Index, bool IsNew) AssignIndexIfAbsent(Type type) + { + if (_typeIndexByType.TryGetValue(type, out uint existing)) + { + return (existing, false); + } + + uint index = _nextIndex; + _nextIndex += 1; + _typeIndexByType[type] = index; + return (index, true); + } + + public void Reset() + { + _typeIndexByType.Clear(); + _nextIndex = 0; + } +} + +public sealed class CompatibleTypeDefReadState +{ + private readonly List _typeMetas = []; + + public TypeMeta? TypeMetaAt(int index) + { + return index >= 0 && index < _typeMetas.Count ? _typeMetas[index] : null; + } + + public void StoreTypeMeta(TypeMeta typeMeta, int index) + { + if (index < 0) + { + throw new ForyInvalidDataException("negative compatible type definition index"); + } + + if (index == _typeMetas.Count) + { + _typeMetas.Add(typeMeta); + return; + } + + if (index < _typeMetas.Count) + { + _typeMetas[index] = typeMeta; + return; + } + + throw new ForyInvalidDataException( + $"compatible type definition index gap: index={index}, count={_typeMetas.Count}"); + } + + public void Reset() + { + _typeMetas.Clear(); + } +} + +public sealed class MetaStringWriteState +{ + private readonly Dictionary _stringIndexByKey = []; + private uint _nextIndex; + + public uint? Index(MetaString value) + { + return _stringIndexByKey.TryGetValue(value, out uint index) ? index : null; + } + + public (uint Index, bool IsNew) AssignIndexIfAbsent(MetaString value) + { + if (_stringIndexByKey.TryGetValue(value, out uint existing)) + { + return (existing, false); + } + + uint index = _nextIndex; + _nextIndex += 1; + _stringIndexByKey[value] = index; + return (index, true); + } + + public void Reset() + { + _stringIndexByKey.Clear(); + _nextIndex = 0; + } +} + +public sealed class MetaStringReadState +{ + private readonly List _values = []; + + public MetaString? ValueAt(int index) + { + return index >= 0 && index < _values.Count ? _values[index] : null; + } + + public void Append(MetaString value) + { + _values.Add(value); + } + + public void Reset() + { + _values.Clear(); + } +} + +public sealed record DynamicTypeInfo( + ForyTypeId WireTypeId, + uint? UserTypeId, + MetaString? NamespaceName, + MetaString? TypeName, + TypeMeta? CompatibleTypeMeta); + +public readonly struct WriteContext +{ + public WriteContext( + ByteWriter writer, + TypeResolver typeResolver, + bool trackRef, + bool compatible = false, + CompatibleTypeDefWriteState? compatibleTypeDefState = null, + MetaStringWriteState? metaStringWriteState = null) + { + Writer = writer; + TypeResolver = typeResolver; + TrackRef = trackRef; + Compatible = compatible; + RefWriter = new RefWriter(); + CompatibleTypeDefState = compatibleTypeDefState ?? new CompatibleTypeDefWriteState(); + MetaStringWriteState = metaStringWriteState ?? new MetaStringWriteState(); + } + + public ByteWriter Writer { get; } + + public TypeResolver TypeResolver { get; } + + public bool TrackRef { get; } + + public bool Compatible { get; } + + public RefWriter RefWriter { get; } + + public CompatibleTypeDefWriteState CompatibleTypeDefState { get; } + + public MetaStringWriteState MetaStringWriteState { get; } + + public void WriteCompatibleTypeMeta(Type type, TypeMeta typeMeta) + { + (uint index, bool isNew) = CompatibleTypeDefState.AssignIndexIfAbsent(type); + if (isNew) + { + Writer.WriteVarUInt32(index << 1); + Writer.WriteBytes(typeMeta.Encode()); + } + else + { + Writer.WriteVarUInt32((index << 1) | 1); + } + } + + public void ResetObjectState() + { + RefWriter.Reset(); + } + + public void Reset() + { + ResetObjectState(); + CompatibleTypeDefState.Reset(); + MetaStringWriteState.Reset(); + } +} + +internal readonly record struct PendingRefSlot(uint RefId, bool Bound); + +internal readonly record struct CanonicalReferenceSignature( + Type Type, + ulong HashLo, + ulong HashHi, + int Length); + +internal sealed class CanonicalReferenceEntry +{ + public required byte[] Bytes { get; init; } + public required object Object { get; init; } +} + +public sealed class ReadContext +{ + private readonly List _pendingRefStack = []; + private readonly Dictionary> _pendingCompatibleTypeMeta = []; + private readonly Dictionary _pendingDynamicTypeInfo = []; + private readonly Dictionary> _canonicalReferenceCache = []; + + public ReadContext( + ByteReader reader, + TypeResolver typeResolver, + bool trackRef, + bool compatible = false, + CompatibleTypeDefReadState? compatibleTypeDefState = null, + MetaStringReadState? metaStringReadState = null) + { + Reader = reader; + TypeResolver = typeResolver; + TrackRef = trackRef; + Compatible = compatible; + RefReader = new RefReader(); + CompatibleTypeDefState = compatibleTypeDefState ?? new CompatibleTypeDefReadState(); + MetaStringReadState = metaStringReadState ?? new MetaStringReadState(); + } + + public ByteReader Reader { get; } + + public TypeResolver TypeResolver { get; } + + public bool TrackRef { get; } + + public bool Compatible { get; } + + public RefReader RefReader { get; } + + public CompatibleTypeDefReadState CompatibleTypeDefState { get; } + + public MetaStringReadState MetaStringReadState { get; } + + public void PushPendingReference(uint refId) + { + _pendingRefStack.Add(new PendingRefSlot(refId, false)); + } + + public void BindPendingReference(object? value) + { + if (_pendingRefStack.Count == 0) + { + return; + } + + PendingRefSlot last = _pendingRefStack[^1]; + _pendingRefStack.RemoveAt(_pendingRefStack.Count - 1); + _pendingRefStack.Add(last with { Bound = true }); + RefReader.StoreRef(value, last.RefId); + } + + public void FinishPendingReferenceIfNeeded(object? value) + { + if (_pendingRefStack.Count == 0) + { + return; + } + + PendingRefSlot last = _pendingRefStack[^1]; + if (!last.Bound) + { + RefReader.StoreRef(value, last.RefId); + } + } + + public void PopPendingReference() + { + if (_pendingRefStack.Count > 0) + { + _pendingRefStack.RemoveAt(_pendingRefStack.Count - 1); + } + } + + public TypeMeta ReadCompatibleTypeMeta() + { + uint indexMarker = Reader.ReadVarUInt32(); + bool isRef = (indexMarker & 1) == 1; + int index = checked((int)(indexMarker >> 1)); + if (isRef) + { + TypeMeta? cached = CompatibleTypeDefState.TypeMetaAt(index); + if (cached is null) + { + throw new ForyInvalidDataException($"unknown compatible type definition ref index {index}"); + } + + return cached; + } + + TypeMeta typeMeta = TypeMeta.Decode(Reader); + CompatibleTypeDefState.StoreTypeMeta(typeMeta, index); + return typeMeta; + } + + public void PushCompatibleTypeMeta(Type type, TypeMeta typeMeta) + { + _pendingCompatibleTypeMeta[type] = [typeMeta]; + } + + public TypeMeta ConsumeCompatibleTypeMeta(Type type) + { + if (!_pendingCompatibleTypeMeta.TryGetValue(type, out List? stack) || stack.Count == 0) + { + throw new ForyInvalidDataException($"missing compatible type metadata for {type}"); + } + + return stack[^1]; + } + + public void SetDynamicTypeInfo(Type type, DynamicTypeInfo typeInfo) + { + _pendingDynamicTypeInfo[type] = typeInfo; + } + + public DynamicTypeInfo? DynamicTypeInfo(Type type) + { + return _pendingDynamicTypeInfo.TryGetValue(type, out DynamicTypeInfo? typeInfo) ? typeInfo : null; + } + + public void ClearDynamicTypeInfo(Type type) + { + _pendingDynamicTypeInfo.Remove(type); + } + + public T CanonicalizeNonTrackingReference(T value, int start, int end) + { + if (!TrackRef || end <= start || value is null || value is not object obj) + { + return value; + } + + byte[] bytes = new byte[end - start]; + Array.Copy(Reader.Storage, start, bytes, 0, bytes.Length); + (ulong hashLo, ulong hashHi) = MurmurHash3.X64_128(bytes, 47); + CanonicalReferenceSignature signature = new(obj.GetType(), hashLo, hashHi, bytes.Length); + + if (_canonicalReferenceCache.TryGetValue(signature, out List? bucket)) + { + foreach (CanonicalReferenceEntry entry in bucket) + { + if (entry.Bytes.AsSpan().SequenceEqual(bytes)) + { + return (T)entry.Object; + } + } + + bucket.Add(new CanonicalReferenceEntry { Bytes = bytes, Object = obj }); + return value; + } + + _canonicalReferenceCache[signature] = + [ + new CanonicalReferenceEntry { Bytes = bytes, Object = obj }, + ]; + return value; + } + + public void ResetObjectState() + { + RefReader.Reset(); + _pendingRefStack.Clear(); + _pendingCompatibleTypeMeta.Clear(); + _pendingDynamicTypeInfo.Clear(); + _canonicalReferenceCache.Clear(); + } + + public void Reset() + { + ResetObjectState(); + CompatibleTypeDefState.Reset(); + MetaStringReadState.Reset(); + } +} + diff --git a/csharp/src/Fory/EnumSerializer.cs b/csharp/src/Fory/EnumSerializer.cs new file mode 100644 index 0000000000..1d73180b50 --- /dev/null +++ b/csharp/src/Fory/EnumSerializer.cs @@ -0,0 +1,44 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public sealed class EnumSerializer : Serializer where TEnum : struct, Enum +{ + public override ForyTypeId StaticTypeId => ForyTypeId.Enum; + public override TEnum DefaultValue => default; + + public override void WriteData(ref WriteContext context, in TEnum value, bool hasGenerics) + { + _ = hasGenerics; + uint ordinal = Convert.ToUInt32(value); + context.Writer.WriteVarUInt32(ordinal); + } + + public override TEnum ReadData(ref ReadContext context) + { + uint ordinal = context.Reader.ReadVarUInt32(); + TEnum value = (TEnum)Enum.ToObject(typeof(TEnum), ordinal); + if (!Enum.IsDefined(typeof(TEnum), value)) + { + throw new ForyInvalidDataException($"unknown enum ordinal {ordinal}"); + } + + return value; + } +} + diff --git a/csharp/src/Fory/FieldSkipper.cs b/csharp/src/Fory/FieldSkipper.cs new file mode 100644 index 0000000000..bb5773d877 --- /dev/null +++ b/csharp/src/Fory/FieldSkipper.cs @@ -0,0 +1,114 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public static class FieldSkipper +{ + public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fieldType) + { + _ = ReadFieldValue(ref context, fieldType); + } + + private static uint? ReadEnumOrdinal(ref ReadContext context, RefMode refMode) + { + return refMode switch + { + RefMode.None => context.Reader.ReadVarUInt32(), + RefMode.NullOnly => ReadNullableEnumOrdinal(ref context), + RefMode.Tracking => throw new ForyInvalidDataException("enum tracking ref mode is not supported"), + _ => throw new ForyInvalidDataException($"unsupported ref mode {refMode}"), + }; + } + + private static uint? ReadNullableEnumOrdinal(ref ReadContext context) + { + sbyte flag = context.Reader.ReadInt8(); + if (flag == (sbyte)RefFlag.Null) + { + return null; + } + + if (flag != (sbyte)RefFlag.NotNullValue) + { + throw new ForyInvalidDataException($"unexpected enum nullOnly flag {flag}"); + } + + return context.Reader.ReadVarUInt32(); + } + + private static object? ReadFieldValue(ref ReadContext context, TypeMetaFieldType fieldType) + { + RefMode refMode = RefModeExtensions.From(fieldType.Nullable, fieldType.TrackRef); + switch (fieldType.TypeId) + { + case (uint)ForyTypeId.Bool: + return BoolSerializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.Int8: + return Int8Serializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.Int16: + return Int16Serializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.VarInt32: + return Int32Serializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.VarInt64: + return Int64Serializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.Float32: + return Float32Serializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.Float64: + return Float64Serializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.String: + return StringSerializer.Instance.Read(ref context, refMode, false); + case (uint)ForyTypeId.List: + { + if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)ForyTypeId.String) + { + throw new ForyInvalidDataException("unsupported compatible list element type"); + } + + return new ListSerializer().Read(ref context, refMode, false); + } + case (uint)ForyTypeId.Set: + { + if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)ForyTypeId.String) + { + throw new ForyInvalidDataException("unsupported compatible set element type"); + } + + return new SetSerializer().Read(ref context, refMode, false); + } + case (uint)ForyTypeId.Map: + { + if (fieldType.Generics.Count != 2 || + fieldType.Generics[0].TypeId != (uint)ForyTypeId.String || + fieldType.Generics[1].TypeId != (uint)ForyTypeId.String) + { + throw new ForyInvalidDataException("unsupported compatible map key/value type"); + } + + return new MapSerializer().Read(ref context, refMode, false); + } + case (uint)ForyTypeId.Enum: + return ReadEnumOrdinal(ref context, refMode); + case (uint)ForyTypeId.Union: + case (uint)ForyTypeId.TypedUnion: + case (uint)ForyTypeId.NamedUnion: + return SerializerRegistry.Get().Read(ref context, refMode, false); + default: + throw new ForyInvalidDataException($"unsupported compatible field type id {fieldType.TypeId}"); + } + } +} diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs new file mode 100644 index 0000000000..2f0a881dbd --- /dev/null +++ b/csharp/src/Fory/Fory.cs @@ -0,0 +1,244 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Buffers; + +namespace Apache.Fory; + +public sealed class Fory +{ + private readonly TypeResolver _typeResolver; + + internal Fory(ForyConfig config) + { + Config = config; + _typeResolver = new TypeResolver(); + } + + public ForyConfig Config { get; } + + public static ForyBuilder Builder() + { + return new ForyBuilder(); + } + + public Fory Register(uint typeId) + { + _typeResolver.Register(typeof(T), typeId); + return this; + } + + public Fory Register(string typeName) + { + _typeResolver.Register(typeof(T), string.Empty, typeName); + return this; + } + + public Fory Register(string typeNamespace, string typeName) + { + _typeResolver.Register(typeof(T), typeNamespace, typeName); + return this; + } + + public Fory Register(uint typeId) + where TSerializer : Serializer, new() + { + TSerializer serializer = new(); + SerializerRegistry.RegisterCustom(typeof(T), serializer); + _typeResolver.Register(typeof(T), typeId, serializer); + return this; + } + + public Fory Register(string typeNamespace, string typeName) + where TSerializer : Serializer, new() + { + TSerializer serializer = new(); + SerializerRegistry.RegisterCustom(typeof(T), serializer); + _typeResolver.Register(typeof(T), typeNamespace, typeName, serializer); + return this; + } + + public byte[] Serialize(in T value) + { + ByteWriter writer = new(); + Serializer serializer = SerializerRegistry.Get(); + bool isNone = serializer.IsNoneObject(value); + WriteHead(writer, isNone); + if (!isNone) + { + WriteContext context = new( + writer, + _typeResolver, + Config.TrackRef, + Config.Compatible, + new CompatibleTypeDefWriteState(), + new MetaStringWriteState()); + RefMode refMode = Config.TrackRef ? RefMode.Tracking : RefMode.NullOnly; + serializer.Write(ref context, value, refMode, true, false); + context.ResetObjectState(); + } + + return writer.ToArray(); + } + + public void Serialize(IBufferWriter output, in T value) + { + byte[] payload = Serialize(value); + output.Write(payload); + } + + public T Deserialize(ReadOnlySpan payload) + { + ByteReader reader = new(payload); + T value = DeserializeFromReader(reader); + if (reader.Remaining != 0) + { + throw new ForyInvalidDataException($"unexpected trailing bytes after deserializing {typeof(T)}"); + } + + return value; + } + + public T Deserialize(ref ReadOnlySequence payload) + { + byte[] bytes = payload.ToArray(); + ByteReader reader = new(bytes); + T value = DeserializeFromReader(reader); + payload = payload.Slice(reader.Cursor); + return value; + } + + public byte[] SerializeObject(object? value) + { + ByteWriter writer = new(); + bool isNone = value is null; + WriteHead(writer, isNone); + if (!isNone) + { + WriteContext context = new( + writer, + _typeResolver, + Config.TrackRef, + Config.Compatible, + new CompatibleTypeDefWriteState(), + new MetaStringWriteState()); + RefMode refMode = Config.TrackRef ? RefMode.Tracking : RefMode.NullOnly; + DynamicAnyCodec.WriteAny(ref context, value, refMode, true, false); + context.ResetObjectState(); + } + + return writer.ToArray(); + } + + public void SerializeObject(IBufferWriter output, object? value) + { + byte[] payload = SerializeObject(value); + output.Write(payload); + } + + public object? DeserializeObject(ReadOnlySpan payload) + { + ByteReader reader = new(payload); + object? value = DeserializeObjectFromReader(reader); + if (reader.Remaining != 0) + { + throw new ForyInvalidDataException("unexpected trailing bytes after deserializing dynamic object"); + } + + return value; + } + + public object? DeserializeObject(ref ReadOnlySequence payload) + { + byte[] bytes = payload.ToArray(); + ByteReader reader = new(bytes); + object? value = DeserializeObjectFromReader(reader); + payload = payload.Slice(reader.Cursor); + return value; + } + + public void WriteHead(ByteWriter writer, bool isNone) + { + byte bitmap = 0; + if (Config.Xlang) + { + bitmap |= ForyHeaderFlag.IsXlang; + } + + if (isNone) + { + bitmap |= ForyHeaderFlag.IsNull; + } + + writer.WriteUInt8(bitmap); + } + + public bool ReadHead(ByteReader reader) + { + byte bitmap = reader.ReadUInt8(); + bool peerIsXlang = (bitmap & ForyHeaderFlag.IsXlang) != 0; + if (peerIsXlang != Config.Xlang) + { + throw new ForyInvalidDataException("xlang bitmap mismatch"); + } + + return (bitmap & ForyHeaderFlag.IsNull) != 0; + } + + private T DeserializeFromReader(ByteReader reader) + { + bool isNone = ReadHead(reader); + Serializer serializer = SerializerRegistry.Get(); + if (isNone) + { + return serializer.DefaultValue; + } + + ReadContext context = new( + reader, + _typeResolver, + Config.TrackRef, + Config.Compatible, + new CompatibleTypeDefReadState(), + new MetaStringReadState()); + RefMode refMode = Config.TrackRef ? RefMode.Tracking : RefMode.NullOnly; + T value = serializer.Read(ref context, refMode, true); + context.ResetObjectState(); + return value; + } + + private object? DeserializeObjectFromReader(ByteReader reader) + { + bool isNone = ReadHead(reader); + if (isNone) + { + return null; + } + + ReadContext context = new( + reader, + _typeResolver, + Config.TrackRef, + Config.Compatible, + new CompatibleTypeDefReadState(), + new MetaStringReadState()); + RefMode refMode = Config.TrackRef ? RefMode.Tracking : RefMode.NullOnly; + object? value = DynamicAnyCodec.ReadAny(ref context, refMode, true); + context.ResetObjectState(); + return value; + } +} diff --git a/csharp/src/Fory/Fory.csproj b/csharp/src/Fory/Fory.csproj new file mode 100644 index 0000000000..2082c7f5e4 --- /dev/null +++ b/csharp/src/Fory/Fory.csproj @@ -0,0 +1,8 @@ + + + net8.0 + 12.0 + enable + enable + + diff --git a/csharp/src/Fory/ForyConfig.cs b/csharp/src/Fory/ForyConfig.cs new file mode 100644 index 0000000000..434a2356cd --- /dev/null +++ b/csharp/src/Fory/ForyConfig.cs @@ -0,0 +1,90 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public sealed record ForyConfig( + bool Xlang = true, + bool TrackRef = false, + bool Compatible = false, + bool CheckStructVersion = false, + bool EnableReflectionFallback = false, + int MaxDepth = 512); + +public sealed class ForyBuilder +{ + private bool _xlang = true; + private bool _trackRef; + private bool _compatible; + private bool _checkStructVersion; + private bool _enableReflectionFallback; + private int _maxDepth = 512; + + public ForyBuilder Xlang(bool enabled = true) + { + _xlang = enabled; + return this; + } + + public ForyBuilder TrackRef(bool enabled = false) + { + _trackRef = enabled; + return this; + } + + public ForyBuilder Compatible(bool enabled = false) + { + _compatible = enabled; + return this; + } + + public ForyBuilder CheckStructVersion(bool enabled = false) + { + _checkStructVersion = enabled; + return this; + } + + public ForyBuilder EnableReflectionFallback(bool enabled = false) + { + _enableReflectionFallback = enabled; + return this; + } + + public ForyBuilder MaxDepth(int value) + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "MaxDepth must be greater than 0."); + } + + _maxDepth = value; + return this; + } + + public Fory Build() + { + return new Fory( + new ForyConfig( + Xlang: _xlang, + TrackRef: _trackRef, + Compatible: _compatible, + CheckStructVersion: _checkStructVersion, + EnableReflectionFallback: _enableReflectionFallback, + MaxDepth: _maxDepth)); + } +} + diff --git a/csharp/src/Fory/ForyException.cs b/csharp/src/Fory/ForyException.cs new file mode 100644 index 0000000000..3d0199688f --- /dev/null +++ b/csharp/src/Fory/ForyException.cs @@ -0,0 +1,70 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public class ForyException : Exception +{ + public ForyException(string message) : base(message) + { + } +} + +public sealed class ForyInvalidDataException : ForyException +{ + public ForyInvalidDataException(string message) : base($"Invalid data: {message}") + { + } +} + +public sealed class ForyTypeMismatchException : ForyException +{ + public ForyTypeMismatchException(uint expected, uint actual) + : base($"Type mismatch: expected {expected}, got {actual}") + { + } +} + +public sealed class ForyTypeNotRegisteredException : ForyException +{ + public ForyTypeNotRegisteredException(string message) : base($"Type not registered: {message}") + { + } +} + +public sealed class ForyRefException : ForyException +{ + public ForyRefException(string message) : base($"Reference error: {message}") + { + } +} + +public sealed class ForyEncodingException : ForyException +{ + public ForyEncodingException(string message) : base($"Encoding error: {message}") + { + } +} + +public sealed class ForyOutOfBoundsException : ForyException +{ + public ForyOutOfBoundsException(int cursor, int need, int length) + : base($"Buffer out of bounds: cursor={cursor}, need={need}, length={length}") + { + } +} + diff --git a/csharp/src/Fory/ForyFlags.cs b/csharp/src/Fory/ForyFlags.cs new file mode 100644 index 0000000000..d5b1819579 --- /dev/null +++ b/csharp/src/Fory/ForyFlags.cs @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public enum RefFlag : sbyte +{ + Null = -3, + Ref = -2, + NotNullValue = -1, + RefValue = 0, +} + +public enum RefMode : byte +{ + None = 0, + NullOnly = 1, + Tracking = 2, +} + +internal static class RefModeExtensions +{ + public static RefMode From(bool nullable, bool trackRef) + { + if (trackRef) + { + return RefMode.Tracking; + } + + return nullable ? RefMode.NullOnly : RefMode.None; + } +} + +public static class ForyHeaderFlag +{ + public const byte IsNull = 0x01; + public const byte IsXlang = 0x02; + public const byte IsOutOfBand = 0x04; +} + diff --git a/csharp/src/Fory/ForyMap.cs b/csharp/src/Fory/ForyMap.cs new file mode 100644 index 0000000000..82b7e51aa8 --- /dev/null +++ b/csharp/src/Fory/ForyMap.cs @@ -0,0 +1,330 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Collections; + +namespace Apache.Fory; + +#pragma warning disable CS8714 +public sealed class ForyMap : IEnumerable> +{ + private readonly Dictionary _nonNullEntries; + private bool _hasNullKey; + private TValue _nullValue = default!; + + public ForyMap() + : this(null) + { + } + + public ForyMap(IEqualityComparer? comparer) + { + _nonNullEntries = comparer is null + ? new Dictionary() + : new Dictionary(comparer); + } + + public int Count => _nonNullEntries.Count + (_hasNullKey ? 1 : 0); + + public bool HasNullKey => _hasNullKey; + + public TValue NullKeyValue => _nullValue; + + public IEnumerable> NonNullEntries => _nonNullEntries; + + public void Add(TKey? key, TValue value) + { + if (key is null) + { + _hasNullKey = true; + _nullValue = value; + return; + } + + _nonNullEntries[key] = value; + } + + public bool TryGetValue(TKey? key, out TValue value) + { + if (key is null) + { + if (_hasNullKey) + { + value = _nullValue; + return true; + } + + value = default!; + return false; + } + + return _nonNullEntries.TryGetValue(key, out value!); + } + + public void Clear() + { + _nonNullEntries.Clear(); + _hasNullKey = false; + _nullValue = default!; + } + + public IEnumerator> GetEnumerator() + { + if (_hasNullKey) + { + yield return new KeyValuePair(default, _nullValue); + } + + foreach (KeyValuePair entry in _nonNullEntries) + { + yield return new KeyValuePair(entry.Key, entry.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} + +public sealed class ForyMapSerializer : Serializer> +{ + private readonly Serializer _keySerializer = SerializerRegistry.Get(); + private readonly Serializer _valueSerializer = SerializerRegistry.Get(); + + public override ForyTypeId StaticTypeId => ForyTypeId.Map; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override ForyMap DefaultValue => null!; + public override bool IsNone(ForyMap value) => value is null; + + public override void WriteData(ref WriteContext context, in ForyMap value, bool hasGenerics) + { + ForyMap map = value ?? new ForyMap(); + context.Writer.WriteVarUInt32((uint)map.Count); + if (map.Count == 0) + { + return; + } + + bool trackKeyRef = context.TrackRef && _keySerializer.IsReferenceTrackableType; + bool trackValueRef = context.TrackRef && _valueSerializer.IsReferenceTrackableType; + bool keyDeclared = hasGenerics && !_keySerializer.StaticTypeId.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !_valueSerializer.StaticTypeId.NeedsTypeInfoForField(); + + foreach (KeyValuePair entry in map) + { + bool keyIsNull = entry.Key is null || _keySerializer.IsNoneObject(entry.Key); + bool valueIsNull = _valueSerializer.IsNoneObject(entry.Value); + byte header = 0; + if (trackKeyRef) + { + header |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + header |= MapBits.TrackingValueRef; + } + + if (keyIsNull) + { + header |= MapBits.KeyNull; + } + else if (keyDeclared) + { + header |= MapBits.DeclaredKeyType; + } + + if (valueIsNull) + { + header |= MapBits.ValueNull; + } + else if (valueDeclared) + { + header |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + if (keyIsNull && valueIsNull) + { + continue; + } + + if (keyIsNull) + { + if (!valueDeclared) + { + _valueSerializer.WriteTypeInfo(ref context); + } + + _valueSerializer.Write( + ref context, + entry.Value, + trackValueRef ? RefMode.Tracking : RefMode.None, + false, + hasGenerics); + continue; + } + + if (valueIsNull) + { + if (!keyDeclared) + { + _keySerializer.WriteTypeInfo(ref context); + } + + _keySerializer.Write( + ref context, + entry.Key!, + trackKeyRef ? RefMode.Tracking : RefMode.None, + false, + hasGenerics); + continue; + } + + context.Writer.WriteUInt8(1); + if (!keyDeclared) + { + _keySerializer.WriteTypeInfo(ref context); + } + + if (!valueDeclared) + { + _valueSerializer.WriteTypeInfo(ref context); + } + + _keySerializer.Write( + ref context, + entry.Key!, + trackKeyRef ? RefMode.Tracking : RefMode.None, + false, + hasGenerics); + _valueSerializer.Write( + ref context, + entry.Value, + trackValueRef ? RefMode.Tracking : RefMode.None, + false, + hasGenerics); + } + } + + public override ForyMap ReadData(ref ReadContext context) + { + int totalLength = checked((int)context.Reader.ReadVarUInt32()); + if (totalLength == 0) + { + return new ForyMap(); + } + + ForyMap map = new(); + bool keyDynamicType = _keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = _valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool canonicalizeValues = context.TrackRef && _valueSerializer.IsReferenceTrackableType; + + int readCount = 0; + while (readCount < totalLength) + { + byte header = context.Reader.ReadUInt8(); + bool trackKeyRef = (header & MapBits.TrackingKeyRef) != 0; + bool keyNull = (header & MapBits.KeyNull) != 0; + bool keyDeclared = (header & MapBits.DeclaredKeyType) != 0; + bool trackValueRef = (header & MapBits.TrackingValueRef) != 0; + bool valueNull = (header & MapBits.ValueNull) != 0; + bool valueDeclared = (header & MapBits.DeclaredValueType) != 0; + + if (keyNull && valueNull) + { + map.Add(default, (TValue)_valueSerializer.DefaultObject!); + readCount += 1; + continue; + } + + if (keyNull) + { + TValue value = ReadValueElement( + ref context, + trackValueRef, + valueDynamicType || !valueDeclared, + canonicalizeValues, + _valueSerializer); + map.Add(default, value); + readCount += 1; + continue; + } + + if (valueNull) + { + TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared)!; + map.Add(key, (TValue)_valueSerializer.DefaultObject!); + readCount += 1; + continue; + } + + int chunkSize = context.Reader.ReadUInt8(); + if (!keyDeclared) + { + _keySerializer.ReadTypeInfo(ref context); + } + + if (!valueDeclared) + { + _valueSerializer.ReadTypeInfo(ref context); + } + + for (int i = 0; i < chunkSize; i++) + { + TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false)!; + TValue value = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, _valueSerializer); + map.Add(key, value); + } + + if (!keyDeclared) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + + if (!valueDeclared) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + + readCount += chunkSize; + } + + return map; + } + + private static TValue ReadValueElement( + ref ReadContext context, + bool trackValueRef, + bool readTypeInfo, + bool canonicalizeValues, + ISerializer valueSerializer) + { + if (trackValueRef || !canonicalizeValues) + { + return (TValue)valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo)!; + } + + int start = context.Reader.Cursor; + TValue value = (TValue)valueSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + int end = context.Reader.Cursor; + return context.CanonicalizeNonTrackingReference(value, start, end); + } +} +#pragma warning restore CS8714 diff --git a/csharp/src/Fory/ForyTypeId.cs b/csharp/src/Fory/ForyTypeId.cs new file mode 100644 index 0000000000..c6e2443fb0 --- /dev/null +++ b/csharp/src/Fory/ForyTypeId.cs @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public enum ForyTypeId : uint +{ + Unknown = 0, + Bool = 1, + Int8 = 2, + Int16 = 3, + Int32 = 4, + VarInt32 = 5, + Int64 = 6, + VarInt64 = 7, + TaggedInt64 = 8, + UInt8 = 9, + UInt16 = 10, + UInt32 = 11, + VarUInt32 = 12, + UInt64 = 13, + VarUInt64 = 14, + TaggedUInt64 = 15, + Float8 = 16, + Float16 = 17, + BFloat16 = 18, + Float32 = 19, + Float64 = 20, + String = 21, + List = 22, + Set = 23, + Map = 24, + Enum = 25, + NamedEnum = 26, + Struct = 27, + CompatibleStruct = 28, + NamedStruct = 29, + NamedCompatibleStruct = 30, + Ext = 31, + NamedExt = 32, + Union = 33, + TypedUnion = 34, + NamedUnion = 35, + None = 36, + Duration = 37, + Timestamp = 38, + Date = 39, + Decimal = 40, + Binary = 41, + Array = 42, + BoolArray = 43, + Int8Array = 44, + Int16Array = 45, + Int32Array = 46, + Int64Array = 47, + UInt8Array = 48, + UInt16Array = 49, + UInt32Array = 50, + UInt64Array = 51, + Float8Array = 52, + Float16Array = 53, + BFloat16Array = 54, + Float32Array = 55, + Float64Array = 56, +} + +internal static class ForyTypeIdExtensions +{ + public static bool IsUserTypeKind(this ForyTypeId typeId) + { + return typeId switch + { + ForyTypeId.Enum or + ForyTypeId.NamedEnum or + ForyTypeId.Struct or + ForyTypeId.CompatibleStruct or + ForyTypeId.NamedStruct or + ForyTypeId.NamedCompatibleStruct or + ForyTypeId.Ext or + ForyTypeId.NamedExt or + ForyTypeId.TypedUnion or + ForyTypeId.NamedUnion => true, + _ => false, + }; + } + + public static bool NeedsTypeInfoForField(this ForyTypeId typeId) + { + return typeId switch + { + ForyTypeId.Struct or + ForyTypeId.CompatibleStruct or + ForyTypeId.NamedStruct or + ForyTypeId.NamedCompatibleStruct or + ForyTypeId.Ext or + ForyTypeId.NamedExt or + ForyTypeId.Unknown => true, + _ => false, + }; + } +} + diff --git a/csharp/src/Fory/MetaString.cs b/csharp/src/Fory/MetaString.cs new file mode 100644 index 0000000000..a863f590bf --- /dev/null +++ b/csharp/src/Fory/MetaString.cs @@ -0,0 +1,533 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Text; + +namespace Apache.Fory; + +public enum MetaStringEncoding : byte +{ + Utf8 = 0, + LowerSpecial = 1, + LowerUpperDigitSpecial = 2, + FirstToLowerSpecial = 3, + AllToLowerSpecial = 4, +} + +public readonly struct MetaString : IEquatable +{ + private const int MaxMetaStringLength = 32_767; + + public MetaString( + string value, + MetaStringEncoding encoding, + char specialChar1, + char specialChar2, + byte[] bytes) + { + if (value.Length >= MaxMetaStringLength) + { + throw new ForyEncodingException("meta string too long"); + } + + if (encoding != MetaStringEncoding.Utf8 && bytes.Length == 0) + { + throw new ForyEncodingException("encoded meta string cannot be empty"); + } + + Value = value; + Encoding = encoding; + SpecialChar1 = specialChar1; + SpecialChar2 = specialChar2; + Bytes = bytes; + StripLastChar = encoding != MetaStringEncoding.Utf8 && (bytes[0] & 0x80) != 0; + } + + public string Value { get; } + + public MetaStringEncoding Encoding { get; } + + public char SpecialChar1 { get; } + + public char SpecialChar2 { get; } + + public byte[] Bytes { get; } + + public bool StripLastChar { get; } + + public static MetaString Empty(char specialChar1, char specialChar2) + { + return new MetaString(string.Empty, MetaStringEncoding.Utf8, specialChar1, specialChar2, []); + } + + public bool Equals(MetaString other) + { + return Value == other.Value && + Encoding == other.Encoding && + SpecialChar1 == other.SpecialChar1 && + SpecialChar2 == other.SpecialChar2 && + Bytes.AsSpan().SequenceEqual(other.Bytes); + } + + public override bool Equals(object? obj) + { + return obj is MetaString other && Equals(other); + } + + public override int GetHashCode() + { + HashCode hc = new(); + hc.Add(Value); + hc.Add(Encoding); + hc.Add(SpecialChar1); + hc.Add(SpecialChar2); + foreach (byte b in Bytes) + { + hc.Add(b); + } + + return hc.ToHashCode(); + } +} + +public sealed class MetaStringEncoder +{ + private const int MaxMetaStringLength = 32_767; + + public MetaStringEncoder(char specialChar1, char specialChar2) + { + SpecialChar1 = specialChar1; + SpecialChar2 = specialChar2; + } + + public char SpecialChar1 { get; } + + public char SpecialChar2 { get; } + + public static MetaStringEncoder Namespace { get; } = new('.', '_'); + + public static MetaStringEncoder TypeName { get; } = new('$', '_'); + + public static MetaStringEncoder FieldName { get; } = new('$', '_'); + + public MetaString Encode(string input) + { + return EncodeAuto(input, null); + } + + public MetaString Encode(string input, IReadOnlyList allowedEncodings) + { + return EncodeAuto(input, allowedEncodings); + } + + public MetaString Encode(string input, MetaStringEncoding encoding) + { + if (input.Length >= MaxMetaStringLength) + { + throw new ForyEncodingException("meta string too long"); + } + + if (input.Length == 0) + { + return MetaString.Empty(SpecialChar1, SpecialChar2); + } + + if (encoding != MetaStringEncoding.Utf8 && !IsLatin(input)) + { + throw new ForyEncodingException("non-ASCII characters are not allowed for packed meta string"); + } + + return encoding switch + { + MetaStringEncoding.Utf8 => new MetaString( + input, + MetaStringEncoding.Utf8, + SpecialChar1, + SpecialChar2, + Encoding.UTF8.GetBytes(input)), + MetaStringEncoding.LowerSpecial => new MetaString( + input, + MetaStringEncoding.LowerSpecial, + SpecialChar1, + SpecialChar2, + EncodeGeneric(input, 5, MapLowerSpecial)), + MetaStringEncoding.LowerUpperDigitSpecial => new MetaString( + input, + MetaStringEncoding.LowerUpperDigitSpecial, + SpecialChar1, + SpecialChar2, + EncodeGeneric(input, 6, MapLowerUpperDigitSpecial)), + MetaStringEncoding.FirstToLowerSpecial => new MetaString( + input, + MetaStringEncoding.FirstToLowerSpecial, + SpecialChar1, + SpecialChar2, + EncodeGeneric(LowerFirstAscii(input), 5, MapLowerSpecial)), + MetaStringEncoding.AllToLowerSpecial => new MetaString( + input, + MetaStringEncoding.AllToLowerSpecial, + SpecialChar1, + SpecialChar2, + EncodeGeneric(EscapeAllUpper(input), 5, MapLowerSpecial)), + _ => throw new ForyEncodingException($"unsupported meta string encoding: {encoding}"), + }; + } + + private MetaString EncodeAuto(string input, IReadOnlyList? allowedEncodings) + { + if (input.Length >= MaxMetaStringLength) + { + throw new ForyEncodingException("meta string too long"); + } + + if (input.Length == 0) + { + return MetaString.Empty(SpecialChar1, SpecialChar2); + } + + if (!IsLatin(input)) + { + return new MetaString(input, MetaStringEncoding.Utf8, SpecialChar1, SpecialChar2, Encoding.UTF8.GetBytes(input)); + } + + MetaStringEncoding encoding = ChooseEncoding(input, allowedEncodings); + return Encode(input, encoding); + } + + private MetaStringEncoding ChooseEncoding(string input, IReadOnlyList? allowedEncodings) + { + bool Allow(MetaStringEncoding encoding) + { + return allowedEncodings is null || allowedEncodings.Contains(encoding); + } + + int digitCount = 0; + int upperCount = 0; + bool canLowerSpecial = true; + bool canLowerUpperDigitSpecial = true; + + foreach (char c in input) + { + if (canLowerSpecial) + { + bool isValid = c is >= 'a' and <= 'z' || c is '.' or '_' or '$' or '|'; + if (!isValid) + { + canLowerSpecial = false; + } + } + + if (canLowerUpperDigitSpecial) + { + bool isLower = c is >= 'a' and <= 'z'; + bool isUpper = c is >= 'A' and <= 'Z'; + bool isDigit = c is >= '0' and <= '9'; + bool isSpecial = c == SpecialChar1 || c == SpecialChar2; + if (!(isLower || isUpper || isDigit || isSpecial)) + { + canLowerUpperDigitSpecial = false; + } + } + + if (c is >= '0' and <= '9') + { + digitCount++; + } + + if (c is >= 'A' and <= 'Z') + { + upperCount++; + } + } + + if (canLowerSpecial && Allow(MetaStringEncoding.LowerSpecial)) + { + return MetaStringEncoding.LowerSpecial; + } + + if (canLowerUpperDigitSpecial) + { + if (digitCount != 0 && Allow(MetaStringEncoding.LowerUpperDigitSpecial)) + { + return MetaStringEncoding.LowerUpperDigitSpecial; + } + + if (upperCount == 1 && + char.IsUpper(input[0]) && + Allow(MetaStringEncoding.FirstToLowerSpecial)) + { + return MetaStringEncoding.FirstToLowerSpecial; + } + + if ((input.Length + upperCount) * 5 < input.Length * 6 && Allow(MetaStringEncoding.AllToLowerSpecial)) + { + return MetaStringEncoding.AllToLowerSpecial; + } + + if (Allow(MetaStringEncoding.LowerUpperDigitSpecial)) + { + return MetaStringEncoding.LowerUpperDigitSpecial; + } + } + + return MetaStringEncoding.Utf8; + } + + private byte[] EncodeGeneric(string input, int bitsPerChar, Func mapper) + { + int totalBits = input.Length * bitsPerChar + 1; + int byteLength = (totalBits + 7) / 8; + byte[] bytes = new byte[byteLength]; + int currentBit = 1; + + foreach (char c in input) + { + byte value = mapper(c); + for (int i = bitsPerChar - 1; i >= 0; i--) + { + if (((value >> i) & 0x01) != 0) + { + int bytePos = currentBit / 8; + int bitPos = currentBit % 8; + bytes[bytePos] |= (byte)(1 << (7 - bitPos)); + } + + currentBit++; + } + } + + if (byteLength * 8 >= totalBits + bitsPerChar) + { + bytes[0] |= 0x80; + } + + return bytes; + } + + private static byte MapLowerSpecial(char c) + { + if (c is >= 'a' and <= 'z') + { + return (byte)(c - 'a'); + } + + return c switch + { + '.' => 26, + '_' => 27, + '$' => 28, + '|' => 29, + _ => throw new ForyEncodingException("unsupported character in LOWER_SPECIAL"), + }; + } + + private byte MapLowerUpperDigitSpecial(char c) + { + if (c is >= 'a' and <= 'z') + { + return (byte)(c - 'a'); + } + + if (c is >= 'A' and <= 'Z') + { + return (byte)(26 + c - 'A'); + } + + if (c is >= '0' and <= '9') + { + return (byte)(52 + c - '0'); + } + + if (c == SpecialChar1) + { + return 62; + } + + if (c == SpecialChar2) + { + return 63; + } + + throw new ForyEncodingException("unsupported character in LOWER_UPPER_DIGIT_SPECIAL"); + } + + private static string LowerFirstAscii(string input) + { + if (input.Length == 0) + { + return input; + } + + return char.ToLowerInvariant(input[0]) + input[1..]; + } + + private static string EscapeAllUpper(string input) + { + StringBuilder sb = new(input.Length * 2); + foreach (char c in input) + { + if (char.IsUpper(c)) + { + sb.Append('|'); + sb.Append(char.ToLowerInvariant(c)); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + private static bool IsLatin(string input) + { + foreach (char c in input) + { + if (c > 255) + { + return false; + } + } + + return true; + } +} + +public sealed class MetaStringDecoder +{ + public MetaStringDecoder(char specialChar1, char specialChar2) + { + SpecialChar1 = specialChar1; + SpecialChar2 = specialChar2; + } + + public char SpecialChar1 { get; } + + public char SpecialChar2 { get; } + + public static MetaStringDecoder Namespace { get; } = new('.', '_'); + + public static MetaStringDecoder TypeName { get; } = new('$', '_'); + + public static MetaStringDecoder FieldName { get; } = new('$', '_'); + + public MetaString Decode(byte[] bytes, MetaStringEncoding encoding) + { + string value = encoding switch + { + MetaStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes), + MetaStringEncoding.LowerSpecial => DecodeGeneric(bytes, 5, UnmapLowerSpecial), + MetaStringEncoding.LowerUpperDigitSpecial => DecodeGeneric(bytes, 6, UnmapLowerUpperDigitSpecial), + MetaStringEncoding.FirstToLowerSpecial => + DecodeFirstToLowerSpecial(bytes), + MetaStringEncoding.AllToLowerSpecial => + UnescapeAllUpper(DecodeGeneric(bytes, 5, UnmapLowerSpecial)), + _ => throw new ForyEncodingException($"unsupported meta string encoding: {encoding}"), + }; + + return new MetaString(value, encoding, SpecialChar1, SpecialChar2, bytes); + } + + private string DecodeFirstToLowerSpecial(byte[] bytes) + { + string decoded = DecodeGeneric(bytes, 5, UnmapLowerSpecial); + if (decoded.Length == 0) + { + return decoded; + } + + return char.ToUpperInvariant(decoded[0]) + decoded[1..]; + } + + private string DecodeGeneric(byte[] bytes, int bitsPerChar, Func mapper) + { + if (bytes.Length == 0) + { + return string.Empty; + } + + bool stripLast = (bytes[0] & 0x80) != 0; + int totalBits = bytes.Length * 8; + int bitIndex = 1; + StringBuilder sb = new(bytes.Length); + while (bitIndex + bitsPerChar <= totalBits && + !(stripLast && (bitIndex + 2 * bitsPerChar > totalBits))) + { + byte value = 0; + for (var i = 0; i < bitsPerChar; i++) + { + int byteIndex = bitIndex / 8; + int intra = bitIndex % 8; + byte bit = (byte)((bytes[byteIndex] >> (7 - intra)) & 0x01); + value = (byte)((value << 1) | bit); + bitIndex++; + } + + sb.Append(mapper(value)); + } + + return sb.ToString(); + } + + private static char UnmapLowerSpecial(byte value) + { + return value switch + { + <= 25 => (char)('a' + value), + 26 => '.', + 27 => '_', + 28 => '$', + 29 => '|', + _ => throw new ForyEncodingException("invalid LOWER_SPECIAL value"), + }; + } + + private char UnmapLowerUpperDigitSpecial(byte value) + { + return value switch + { + <= 25 => (char)('a' + value), + <= 51 => (char)('A' + value - 26), + <= 61 => (char)('0' + value - 52), + 62 => SpecialChar1, + 63 => SpecialChar2, + _ => throw new ForyEncodingException("invalid LOWER_UPPER_DIGIT_SPECIAL value"), + }; + } + + private static string UnescapeAllUpper(string input) + { + StringBuilder sb = new(input.Length); + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + if (c == '|' && i + 1 < input.Length) + { + i++; + sb.Append(char.ToUpperInvariant(input[i])); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } +} + diff --git a/csharp/src/Fory/MurmurHash3.cs b/csharp/src/Fory/MurmurHash3.cs new file mode 100644 index 0000000000..b934316b41 --- /dev/null +++ b/csharp/src/Fory/MurmurHash3.cs @@ -0,0 +1,146 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Buffers.Binary; + +namespace Apache.Fory; + +internal static class MurmurHash3 +{ + public static (ulong H1, ulong H2) X64_128(ReadOnlySpan bytes, ulong seed = 47) + { + const ulong c1 = 0x87c37b91114253d5; + const ulong c2 = 0x4cf5ad432745937f; + + ulong h1 = seed; + ulong h2 = seed; + + int length = bytes.Length; + int nblocks = length / 16; + for (int i = 0; i < nblocks; i++) + { + int offset = i * 16; + ulong k1 = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(offset, 8)); + ulong k2 = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(offset + 8, 8)); + + k1 *= c1; + k1 = RotateLeft(k1, 31); + k1 *= c2; + h1 ^= k1; + + h1 = RotateLeft(h1, 27); + h1 += h2; + h1 = h1 * 5 + 0x52dce729; + + k2 *= c2; + k2 = RotateLeft(k2, 33); + k2 *= c1; + h2 ^= k2; + + h2 = RotateLeft(h2, 31); + h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + ulong tk1 = 0; + ulong tk2 = 0; + int tailStart = nblocks * 16; + ReadOnlySpan tail = bytes.Slice(tailStart); + switch (length & 15) + { + case 15: + tk2 ^= (ulong)tail[14] << 48; + goto case 14; + case 14: + tk2 ^= (ulong)tail[13] << 40; + goto case 13; + case 13: + tk2 ^= (ulong)tail[12] << 32; + goto case 12; + case 12: + tk2 ^= (ulong)tail[11] << 24; + goto case 11; + case 11: + tk2 ^= (ulong)tail[10] << 16; + goto case 10; + case 10: + tk2 ^= (ulong)tail[9] << 8; + goto case 9; + case 9: + tk2 ^= tail[8]; + tk2 *= c2; + tk2 = RotateLeft(tk2, 33); + tk2 *= c1; + h2 ^= tk2; + goto case 8; + case 8: + tk1 ^= (ulong)tail[7] << 56; + goto case 7; + case 7: + tk1 ^= (ulong)tail[6] << 48; + goto case 6; + case 6: + tk1 ^= (ulong)tail[5] << 40; + goto case 5; + case 5: + tk1 ^= (ulong)tail[4] << 32; + goto case 4; + case 4: + tk1 ^= (ulong)tail[3] << 24; + goto case 3; + case 3: + tk1 ^= (ulong)tail[2] << 16; + goto case 2; + case 2: + tk1 ^= (ulong)tail[1] << 8; + goto case 1; + case 1: + tk1 ^= tail[0]; + tk1 *= c1; + tk1 = RotateLeft(tk1, 31); + tk1 *= c2; + h1 ^= tk1; + break; + } + + h1 ^= (ulong)length; + h2 ^= (ulong)length; + h1 += h2; + h2 += h1; + h1 = Fmix64(h1); + h2 = Fmix64(h2); + h1 += h2; + h2 += h1; + return (h1, h2); + } + + private static ulong RotateLeft(ulong x, int r) + { + return (x << r) | (x >> (64 - r)); + } + + private static ulong Fmix64(ulong x) + { + x ^= x >> 33; + x *= 0xff51afd7ed558ccd; + x ^= x >> 33; + x *= 0xc4ceb9fe1a85ec53; + x ^= x >> 33; + return x; + } +} + diff --git a/csharp/src/Fory/OptionalSerializer.cs b/csharp/src/Fory/OptionalSerializer.cs new file mode 100644 index 0000000000..114815c253 --- /dev/null +++ b/csharp/src/Fory/OptionalSerializer.cs @@ -0,0 +1,140 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public sealed class NullableSerializer : Serializer where T : struct +{ + private readonly Serializer _wrappedSerializer; + + public NullableSerializer() + { + _wrappedSerializer = SerializerRegistry.Get(); + } + + public override ForyTypeId StaticTypeId => _wrappedSerializer.StaticTypeId; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => _wrappedSerializer.IsReferenceTrackableType; + + public override T? DefaultValue => null; + + public override bool IsNone(T? value) + { + return !value.HasValue; + } + + public override void WriteData(ref WriteContext context, in T? value, bool hasGenerics) + { + if (!value.HasValue) + { + throw new ForyInvalidDataException("Nullable.None cannot write raw payload"); + } + + T wrapped = value.Value; + _wrappedSerializer.WriteData(ref context, wrapped, hasGenerics); + } + + public override T? ReadData(ref ReadContext context) + { + return _wrappedSerializer.ReadData(ref context); + } + + public override void WriteTypeInfo(ref WriteContext context) + { + _wrappedSerializer.WriteTypeInfo(ref context); + } + + public override void ReadTypeInfo(ref ReadContext context) + { + _wrappedSerializer.ReadTypeInfo(ref context); + } + + public override IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + { + return _wrappedSerializer.CompatibleTypeMetaFields(trackRef); + } + + public override void Write(ref WriteContext context, in T? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + switch (refMode) + { + case RefMode.None: + if (!value.HasValue) + { + throw new ForyInvalidDataException("Nullable.None with RefMode.None"); + } + + T wrapped = value.Value; + _wrappedSerializer.Write(ref context, wrapped, RefMode.None, writeTypeInfo, hasGenerics); + break; + case RefMode.NullOnly: + if (!value.HasValue) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + return; + } + + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); + _wrappedSerializer.Write(ref context, value.Value, RefMode.None, writeTypeInfo, hasGenerics); + break; + case RefMode.Tracking: + if (!value.HasValue) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + return; + } + + _wrappedSerializer.Write(ref context, value.Value, RefMode.Tracking, writeTypeInfo, hasGenerics); + break; + } + } + + public override T? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + switch (refMode) + { + case RefMode.None: + return _wrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); + case RefMode.NullOnly: + { + sbyte refFlag = context.Reader.ReadInt8(); + if (refFlag == (sbyte)RefFlag.Null) + { + return null; + } + + return _wrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); + } + case RefMode.Tracking: + { + sbyte refFlag = context.Reader.ReadInt8(); + if (refFlag == (sbyte)RefFlag.Null) + { + return null; + } + + context.Reader.MoveBack(1); + return _wrappedSerializer.Read(ref context, RefMode.Tracking, readTypeInfo); + } + default: + throw new ForyInvalidDataException($"unsupported ref mode {refMode}"); + } + } +} + diff --git a/csharp/src/Fory/PrimitiveSerializers.cs b/csharp/src/Fory/PrimitiveSerializers.cs new file mode 100644 index 0000000000..0404aacf81 --- /dev/null +++ b/csharp/src/Fory/PrimitiveSerializers.cs @@ -0,0 +1,530 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Text; + +namespace Apache.Fory; + +internal enum ForyStringEncoding : ulong +{ + Latin1 = 0, + Utf16 = 1, + Utf8 = 2, +} + +public readonly record struct ForyInt32Fixed(int RawValue); + +public readonly record struct ForyInt64Fixed(long RawValue); + +public readonly record struct ForyInt64Tagged(long RawValue); + +public readonly record struct ForyUInt32Fixed(uint RawValue); + +public readonly record struct ForyUInt64Fixed(ulong RawValue); + +public readonly record struct ForyUInt64Tagged(ulong RawValue); + +public readonly record struct ForyDate(int DaysSinceEpoch); + +public readonly record struct ForyTimestamp(long Seconds, uint Nanos) +{ + public static ForyTimestamp FromDateTimeOffset(DateTimeOffset value) + { + long seconds = value.ToUnixTimeSeconds(); + long nanos = (value.Ticks % TimeSpan.TicksPerSecond) * 100; + return Normalize(seconds, nanos); + } + + public DateTimeOffset ToDateTimeOffset() + { + return DateTimeOffset.FromUnixTimeSeconds(Seconds).AddTicks(Nanos / 100); + } + + private static ForyTimestamp Normalize(long seconds, long nanos) + { + long normalizedSeconds = seconds + nanos / 1_000_000_000L; + long normalizedNanos = nanos % 1_000_000_000L; + if (normalizedNanos < 0) + { + normalizedNanos += 1_000_000_000L; + normalizedSeconds -= 1; + } + + return new ForyTimestamp(normalizedSeconds, unchecked((uint)normalizedNanos)); + } +} + +public sealed class BoolSerializer : Serializer +{ + public static BoolSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Bool; + public override bool DefaultValue => false; + public override void WriteData(ref WriteContext context, in bool value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteUInt8(value ? (byte)1 : (byte)0); + } + + public override bool ReadData(ref ReadContext context) + { + return context.Reader.ReadUInt8() != 0; + } +} + +public sealed class Int8Serializer : Serializer +{ + public static Int8Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Int8; + public override sbyte DefaultValue => 0; + public override void WriteData(ref WriteContext context, in sbyte value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteInt8(value); + } + + public override sbyte ReadData(ref ReadContext context) + { + return context.Reader.ReadInt8(); + } +} + +public sealed class Int16Serializer : Serializer +{ + public static Int16Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Int16; + public override short DefaultValue => 0; + public override void WriteData(ref WriteContext context, in short value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteInt16(value); + } + + public override short ReadData(ref ReadContext context) + { + return context.Reader.ReadInt16(); + } +} + +public sealed class Int32Serializer : Serializer +{ + public static Int32Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.VarInt32; + public override int DefaultValue => 0; + public override void WriteData(ref WriteContext context, in int value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteVarInt32(value); + } + + public override int ReadData(ref ReadContext context) + { + return context.Reader.ReadVarInt32(); + } +} + +public sealed class Int64Serializer : Serializer +{ + public static Int64Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.VarInt64; + public override long DefaultValue => 0; + public override void WriteData(ref WriteContext context, in long value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteVarInt64(value); + } + + public override long ReadData(ref ReadContext context) + { + return context.Reader.ReadVarInt64(); + } +} + +public sealed class UInt8Serializer : Serializer +{ + public static UInt8Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.UInt8; + public override byte DefaultValue => 0; + public override void WriteData(ref WriteContext context, in byte value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteUInt8(value); + } + + public override byte ReadData(ref ReadContext context) + { + return context.Reader.ReadUInt8(); + } +} + +public sealed class UInt16Serializer : Serializer +{ + public static UInt16Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.UInt16; + public override ushort DefaultValue => 0; + public override void WriteData(ref WriteContext context, in ushort value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteUInt16(value); + } + + public override ushort ReadData(ref ReadContext context) + { + return context.Reader.ReadUInt16(); + } +} + +public sealed class UInt32Serializer : Serializer +{ + public static UInt32Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.VarUInt32; + public override uint DefaultValue => 0; + public override void WriteData(ref WriteContext context, in uint value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteVarUInt32(value); + } + + public override uint ReadData(ref ReadContext context) + { + return context.Reader.ReadVarUInt32(); + } +} + +public sealed class UInt64Serializer : Serializer +{ + public static UInt64Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.VarUInt64; + public override ulong DefaultValue => 0; + public override void WriteData(ref WriteContext context, in ulong value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteVarUInt64(value); + } + + public override ulong ReadData(ref ReadContext context) + { + return context.Reader.ReadVarUInt64(); + } +} + +public sealed class Float32Serializer : Serializer +{ + public static Float32Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Float32; + public override float DefaultValue => 0; + public override void WriteData(ref WriteContext context, in float value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteFloat32(value); + } + + public override float ReadData(ref ReadContext context) + { + return context.Reader.ReadFloat32(); + } +} + +public sealed class Float64Serializer : Serializer +{ + public static Float64Serializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Float64; + public override double DefaultValue => 0; + public override void WriteData(ref WriteContext context, in double value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteFloat64(value); + } + + public override double ReadData(ref ReadContext context) + { + return context.Reader.ReadFloat64(); + } +} + +public sealed class StringSerializer : Serializer +{ + public static StringSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.String; + public override bool IsNullableType => true; + public override string DefaultValue => null!; + public override bool IsNone(string value) => value is null; + + public override void WriteData(ref WriteContext context, in string value, bool hasGenerics) + { + _ = hasGenerics; + string safe = value ?? string.Empty; + byte[] utf8 = Encoding.UTF8.GetBytes(safe); + ulong header = ((ulong)utf8.Length << 2) | (ulong)ForyStringEncoding.Utf8; + context.Writer.WriteVarUInt36Small(header); + context.Writer.WriteBytes(utf8); + } + + public override string ReadData(ref ReadContext context) + { + ulong header = context.Reader.ReadVarUInt36Small(); + ulong encoding = header & 0x03; + int byteLength = checked((int)(header >> 2)); + byte[] bytes = context.Reader.ReadBytes(byteLength); + return encoding switch + { + (ulong)ForyStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes), + (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes), + (ulong)ForyStringEncoding.Utf16 => DecodeUtf16(bytes), + _ => throw new ForyEncodingException($"unsupported string encoding {encoding}"), + }; + } + + private static string DecodeLatin1(byte[] bytes) + { + return string.Create(bytes.Length, bytes, static (span, b) => + { + for (int i = 0; i < b.Length; i++) + { + span[i] = (char)b[i]; + } + }); + } + + private static string DecodeUtf16(byte[] bytes) + { + if ((bytes.Length & 1) != 0) + { + throw new ForyEncodingException("utf16 byte length is not even"); + } + + return Encoding.Unicode.GetString(bytes); + } +} + +public sealed class BinarySerializer : Serializer +{ + public static BinarySerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Binary; + public override bool IsNullableType => true; + public override byte[] DefaultValue => null!; + public override bool IsNone(byte[] value) => value is null; + + public override void WriteData(ref WriteContext context, in byte[] value, bool hasGenerics) + { + _ = hasGenerics; + byte[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)safe.Length); + context.Writer.WriteBytes(safe); + } + + public override byte[] ReadData(ref ReadContext context) + { + uint length = context.Reader.ReadVarUInt32(); + return context.Reader.ReadBytes(checked((int)length)); + } +} + +public sealed class ForyInt32FixedSerializer : Serializer +{ + public static ForyInt32FixedSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Int32; + public override ForyInt32Fixed DefaultValue => new(0); + public override void WriteData(ref WriteContext context, in ForyInt32Fixed value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteInt32(value.RawValue); + } + + public override ForyInt32Fixed ReadData(ref ReadContext context) + { + return new ForyInt32Fixed(context.Reader.ReadInt32()); + } +} + +public sealed class ForyInt64FixedSerializer : Serializer +{ + public static ForyInt64FixedSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Int64; + public override ForyInt64Fixed DefaultValue => new(0); + public override void WriteData(ref WriteContext context, in ForyInt64Fixed value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteInt64(value.RawValue); + } + + public override ForyInt64Fixed ReadData(ref ReadContext context) + { + return new ForyInt64Fixed(context.Reader.ReadInt64()); + } +} + +public sealed class ForyInt64TaggedSerializer : Serializer +{ + public static ForyInt64TaggedSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.TaggedInt64; + public override ForyInt64Tagged DefaultValue => new(0); + public override void WriteData(ref WriteContext context, in ForyInt64Tagged value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteTaggedInt64(value.RawValue); + } + + public override ForyInt64Tagged ReadData(ref ReadContext context) + { + return new ForyInt64Tagged(context.Reader.ReadTaggedInt64()); + } +} + +public sealed class ForyUInt32FixedSerializer : Serializer +{ + public static ForyUInt32FixedSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.UInt32; + public override ForyUInt32Fixed DefaultValue => new(0); + public override void WriteData(ref WriteContext context, in ForyUInt32Fixed value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteUInt32(value.RawValue); + } + + public override ForyUInt32Fixed ReadData(ref ReadContext context) + { + return new ForyUInt32Fixed(context.Reader.ReadUInt32()); + } +} + +public sealed class ForyUInt64FixedSerializer : Serializer +{ + public static ForyUInt64FixedSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.UInt64; + public override ForyUInt64Fixed DefaultValue => new(0); + public override void WriteData(ref WriteContext context, in ForyUInt64Fixed value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteUInt64(value.RawValue); + } + + public override ForyUInt64Fixed ReadData(ref ReadContext context) + { + return new ForyUInt64Fixed(context.Reader.ReadUInt64()); + } +} + +public sealed class ForyUInt64TaggedSerializer : Serializer +{ + public static ForyUInt64TaggedSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.TaggedUInt64; + public override ForyUInt64Tagged DefaultValue => new(0); + public override void WriteData(ref WriteContext context, in ForyUInt64Tagged value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteTaggedUInt64(value.RawValue); + } + + public override ForyUInt64Tagged ReadData(ref ReadContext context) + { + return new ForyUInt64Tagged(context.Reader.ReadTaggedUInt64()); + } +} + +public sealed class DateOnlySerializer : Serializer +{ + private static readonly DateOnly Epoch = new(1970, 1, 1); + public static DateOnlySerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Date; + public override DateOnly DefaultValue => Epoch; + + public override void WriteData(ref WriteContext context, in DateOnly value, bool hasGenerics) + { + _ = hasGenerics; + int days = value.DayNumber - Epoch.DayNumber; + context.Writer.WriteInt32(days); + } + + public override DateOnly ReadData(ref ReadContext context) + { + int days = context.Reader.ReadInt32(); + return DateOnly.FromDayNumber(Epoch.DayNumber + days); + } +} + +public sealed class DateTimeOffsetSerializer : Serializer +{ + public static DateTimeOffsetSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Timestamp; + public override DateTimeOffset DefaultValue => DateTimeOffset.UnixEpoch; + + public override void WriteData(ref WriteContext context, in DateTimeOffset value, bool hasGenerics) + { + _ = hasGenerics; + ForyTimestamp ts = ForyTimestamp.FromDateTimeOffset(value); + context.Writer.WriteInt64(ts.Seconds); + context.Writer.WriteUInt32(ts.Nanos); + } + + public override DateTimeOffset ReadData(ref ReadContext context) + { + long seconds = context.Reader.ReadInt64(); + uint nanos = context.Reader.ReadUInt32(); + return new ForyTimestamp(seconds, nanos).ToDateTimeOffset(); + } +} + +public sealed class DateTimeSerializer : Serializer +{ + public static DateTimeSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Timestamp; + public override DateTime DefaultValue => DateTime.UnixEpoch; + + public override void WriteData(ref WriteContext context, in DateTime value, bool hasGenerics) + { + _ = hasGenerics; + DateTimeOffset dto = value.Kind switch + { + DateTimeKind.Utc => new DateTimeOffset(value, TimeSpan.Zero), + DateTimeKind.Local => value, + _ => new DateTimeOffset(DateTime.SpecifyKind(value, DateTimeKind.Utc)), + }; + ForyTimestamp ts = ForyTimestamp.FromDateTimeOffset(dto); + context.Writer.WriteInt64(ts.Seconds); + context.Writer.WriteUInt32(ts.Nanos); + } + + public override DateTime ReadData(ref ReadContext context) + { + long seconds = context.Reader.ReadInt64(); + uint nanos = context.Reader.ReadUInt32(); + return new ForyTimestamp(seconds, nanos).ToDateTimeOffset().UtcDateTime; + } +} + +public sealed class TimeSpanSerializer : Serializer +{ + public static TimeSpanSerializer Instance { get; } = new(); + public override ForyTypeId StaticTypeId => ForyTypeId.Duration; + public override TimeSpan DefaultValue => TimeSpan.Zero; + + public override void WriteData(ref WriteContext context, in TimeSpan value, bool hasGenerics) + { + _ = hasGenerics; + long seconds = value.Ticks / TimeSpan.TicksPerSecond; + int nanos = checked((int)((value.Ticks % TimeSpan.TicksPerSecond) * 100)); + context.Writer.WriteInt64(seconds); + context.Writer.WriteInt32(nanos); + } + + public override TimeSpan ReadData(ref ReadContext context) + { + long seconds = context.Reader.ReadInt64(); + int nanos = context.Reader.ReadInt32(); + return TimeSpan.FromSeconds(seconds) + TimeSpan.FromTicks(nanos / 100); + } +} diff --git a/csharp/src/Fory/RefResolver.cs b/csharp/src/Fory/RefResolver.cs new file mode 100644 index 0000000000..183bbfce86 --- /dev/null +++ b/csharp/src/Fory/RefResolver.cs @@ -0,0 +1,103 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public sealed class RefWriter +{ + private readonly Dictionary _refs = new(ReferenceEqualityComparer.Instance); + private uint _nextRefId; + + public bool TryWriteReference(ByteWriter writer, object obj) + { + if (_refs.TryGetValue(obj, out uint refId)) + { + writer.WriteInt8((sbyte)RefFlag.Ref); + writer.WriteVarUInt32(refId); + return true; + } + + _refs[obj] = _nextRefId; + _nextRefId += 1; + writer.WriteInt8((sbyte)RefFlag.RefValue); + return false; + } + + public uint ReserveRefId() + { + uint id = _nextRefId; + _nextRefId += 1; + return id; + } + + public void Reset() + { + _refs.Clear(); + _nextRefId = 0; + } +} + +public sealed class RefReader +{ + private readonly List _refs = []; + + public uint ReserveRefId() + { + uint id = (uint)_refs.Count; + _refs.Add(null); + return id; + } + + public void StoreRef(object? value, uint refId) + { + int index = checked((int)refId); + _refs[index] = value; + } + + public T ReadRef(uint refId) + { + int index = checked((int)refId); + if (index < 0 || index >= _refs.Count) + { + throw new ForyRefException($"ref_id out of range: {refId}"); + } + + if (_refs[index] is T typed) + { + return typed; + } + + throw new ForyRefException($"ref_id {refId} has unexpected runtime type"); + } + + public object? ReadRefValue(uint refId) + { + int index = checked((int)refId); + if (index < 0 || index >= _refs.Count) + { + throw new ForyRefException($"ref_id out of range: {refId}"); + } + + return _refs[index]; + } + + public void Reset() + { + _refs.Clear(); + } +} + diff --git a/csharp/src/Fory/SchemaHash.cs b/csharp/src/Fory/SchemaHash.cs new file mode 100644 index 0000000000..039e99024c --- /dev/null +++ b/csharp/src/Fory/SchemaHash.cs @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Text; + +namespace Apache.Fory; + +public static class SchemaHash +{ + public static uint StructHash32(string fingerprint) + { + byte[] bytes = Encoding.UTF8.GetBytes(fingerprint); + (ulong h1, _) = MurmurHash3.X64_128(bytes, 47); + return unchecked((uint)h1); + } +} + diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs new file mode 100644 index 0000000000..1e4dca8474 --- /dev/null +++ b/csharp/src/Fory/Serializer.cs @@ -0,0 +1,667 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public interface ISerializer +{ + Type Type { get; } + + ForyTypeId StaticTypeId { get; } + + bool IsNullableType { get; } + + bool IsReferenceTrackableType { get; } + + object? DefaultObject { get; } + + bool IsNoneObject(object? value); + + void WriteData(ref WriteContext context, object? value, bool hasGenerics); + + object? ReadData(ref ReadContext context); + + void Write(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics); + + object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo); + + void WriteTypeInfo(ref WriteContext context); + + void ReadTypeInfo(ref ReadContext context); + + IReadOnlyList CompatibleTypeMetaFields(bool trackRef); +} + +public abstract class Serializer : ISerializer +{ + public Type Type => typeof(T); + + public abstract ForyTypeId StaticTypeId { get; } + + public virtual bool IsNullableType => false; + + public virtual bool IsReferenceTrackableType => false; + + public virtual T DefaultValue => default!; + + public virtual bool IsNone(T value) => false; + + public object? DefaultObject => DefaultValue; + + public bool IsNoneObject(object? value) + { + if (value is null) + { + return IsNullableType; + } + + return value is T typed && IsNone(typed); + } + + public abstract void WriteData(ref WriteContext context, in T value, bool hasGenerics); + + public abstract T ReadData(ref ReadContext context); + + public virtual IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + { + return []; + } + + public virtual void WriteTypeInfo(ref WriteContext context) + { + SerializerTypeInfo.WriteTypeInfo(this, ref context); + } + + public virtual void ReadTypeInfo(ref ReadContext context) + { + SerializerTypeInfo.ReadTypeInfo(this, ref context); + } + + public virtual void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + if (refMode != RefMode.None) + { + bool wroteTrackingRefFlag = false; + if (refMode == RefMode.Tracking && + IsReferenceTrackableType && + value is object obj) + { + if (context.RefWriter.TryWriteReference(context.Writer, obj)) + { + return; + } + + wroteTrackingRefFlag = true; + } + + if (!wroteTrackingRefFlag) + { + if (IsNullableType && IsNone(value)) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + return; + } + + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); + } + } + + if (writeTypeInfo) + { + WriteTypeInfo(ref context); + } + + WriteData(ref context, value, hasGenerics); + } + + public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + if (refMode != RefMode.None) + { + sbyte rawFlag = context.Reader.ReadInt8(); + RefFlag flag = (RefFlag)rawFlag; + switch (flag) + { + case RefFlag.Null: + return DefaultValue; + case RefFlag.Ref: + uint refId = context.Reader.ReadVarUInt32(); + return context.RefReader.ReadRef(refId); + case RefFlag.RefValue: + { + uint reservedRefId = context.RefReader.ReserveRefId(); + context.PushPendingReference(reservedRefId); + if (readTypeInfo) + { + ReadTypeInfo(ref context); + } + + T value = ReadData(ref context); + context.FinishPendingReferenceIfNeeded(value); + context.PopPendingReference(); + return value; + } + case RefFlag.NotNullValue: + break; + default: + throw new ForyRefException($"invalid ref flag {rawFlag}"); + } + } + + if (readTypeInfo) + { + ReadTypeInfo(ref context); + } + + return ReadData(ref context); + } + + object? ISerializer.DefaultObject => DefaultValue; + + bool ISerializer.IsNoneObject(object? value) + { + if (value is null) + { + return IsNullableType; + } + + return value is T typed && IsNone(typed); + } + + void ISerializer.WriteData(ref WriteContext context, object? value, bool hasGenerics) + { + if (value is T typed) + { + WriteData(ref context, typed, hasGenerics); + return; + } + + if (value is null && IsNullableType) + { + WriteData(ref context, DefaultValue, hasGenerics); + return; + } + + throw new ForyInvalidDataException( + $"serializer {GetType().Name} expected value of type {typeof(T)}, got {value?.GetType()}"); + } + + object? ISerializer.ReadData(ref ReadContext context) + { + return ReadData(ref context); + } + + void ISerializer.Write(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + if (value is T typed) + { + Write(ref context, typed, refMode, writeTypeInfo, hasGenerics); + return; + } + + if (value is null && IsNullableType) + { + Write(ref context, DefaultValue, refMode, writeTypeInfo, hasGenerics); + return; + } + + throw new ForyInvalidDataException( + $"serializer {GetType().Name} expected value of type {typeof(T)}, got {value?.GetType()}"); + } + + object? ISerializer.Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + return Read(ref context, refMode, readTypeInfo); + } +} + +internal static class SerializerTypeInfo +{ + public static void WriteTypeInfo(ISerializer serializer, ref WriteContext context) + { + if (!serializer.StaticTypeId.IsUserTypeKind()) + { + context.Writer.WriteUInt8((byte)serializer.StaticTypeId); + return; + } + + RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(serializer.Type); + ForyTypeId wireTypeId = ResolveWireTypeId(info.Kind, info.RegisterByName, context.Compatible); + context.Writer.WriteUInt8((byte)wireTypeId); + switch (wireTypeId) + { + case ForyTypeId.CompatibleStruct: + case ForyTypeId.NamedCompatibleStruct: + { + TypeMeta typeMeta = BuildCompatibleTypeMeta(serializer, info, wireTypeId, context.TrackRef); + context.WriteCompatibleTypeMeta(serializer.Type, typeMeta); + return; + } + case ForyTypeId.NamedEnum: + case ForyTypeId.NamedStruct: + case ForyTypeId.NamedExt: + case ForyTypeId.NamedUnion: + { + if (context.Compatible) + { + TypeMeta typeMeta = BuildCompatibleTypeMeta(serializer, info, wireTypeId, context.TrackRef); + context.WriteCompatibleTypeMeta(serializer.Type, typeMeta); + } + else + { + if (info.NamespaceName is null) + { + throw new ForyInvalidDataException("missing namespace metadata for name-registered type"); + } + + WriteMetaString( + ref context, + info.NamespaceName.Value, + TypeMetaEncodings.NamespaceMetaStringEncodings, + MetaStringEncoder.Namespace); + WriteMetaString( + ref context, + info.TypeName, + TypeMetaEncodings.TypeNameMetaStringEncodings, + MetaStringEncoder.TypeName); + } + + return; + } + default: + if (!info.RegisterByName && WireTypeNeedsUserTypeId(wireTypeId)) + { + if (!info.UserTypeId.HasValue) + { + throw new ForyInvalidDataException("missing user type id for id-registered type"); + } + + context.Writer.WriteVarUInt32(info.UserTypeId.Value); + } + + return; + } + } + + public static void ReadTypeInfo(ISerializer serializer, ref ReadContext context) + { + uint rawTypeId = context.Reader.ReadVarUInt32(); + if (!Enum.IsDefined(typeof(ForyTypeId), rawTypeId)) + { + throw new ForyInvalidDataException($"unknown type id {rawTypeId}"); + } + + ForyTypeId typeId = (ForyTypeId)rawTypeId; + if (!serializer.StaticTypeId.IsUserTypeKind()) + { + if (typeId != serializer.StaticTypeId) + { + throw new ForyTypeMismatchException((uint)serializer.StaticTypeId, rawTypeId); + } + + return; + } + + RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(serializer.Type); + HashSet allowed = AllowedWireTypeIds(info.Kind, info.RegisterByName, context.Compatible); + if (!allowed.Contains(typeId)) + { + uint expected = allowed.Count > 0 ? (uint)allowed.First() : 0; + throw new ForyTypeMismatchException(expected, rawTypeId); + } + + switch (typeId) + { + case ForyTypeId.CompatibleStruct: + case ForyTypeId.NamedCompatibleStruct: + { + TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); + ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); + context.PushCompatibleTypeMeta(serializer.Type, remoteTypeMeta); + return; + } + case ForyTypeId.NamedEnum: + case ForyTypeId.NamedStruct: + case ForyTypeId.NamedExt: + case ForyTypeId.NamedUnion: + { + if (context.Compatible) + { + TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); + ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); + if (typeId == ForyTypeId.NamedStruct) + { + context.PushCompatibleTypeMeta(serializer.Type, remoteTypeMeta); + } + } + else + { + MetaString namespaceName = ReadMetaString( + ref context, + MetaStringDecoder.Namespace, + TypeMetaEncodings.NamespaceMetaStringEncodings); + MetaString typeName = ReadMetaString( + ref context, + MetaStringDecoder.TypeName, + TypeMetaEncodings.TypeNameMetaStringEncodings); + if (!info.RegisterByName || info.NamespaceName is null) + { + throw new ForyInvalidDataException("received name-registered type info for id-registered local type"); + } + + if (namespaceName.Value != info.NamespaceName.Value.Value || typeName.Value != info.TypeName.Value) + { + throw new ForyInvalidDataException( + $"type name mismatch: expected {info.NamespaceName.Value.Value}::{info.TypeName.Value}, got {namespaceName.Value}::{typeName.Value}"); + } + } + + return; + } + default: + if (!info.RegisterByName && WireTypeNeedsUserTypeId(typeId)) + { + if (!info.UserTypeId.HasValue) + { + throw new ForyInvalidDataException("missing user type id for id-registered local type"); + } + + uint remoteUserTypeId = context.Reader.ReadVarUInt32(); + if (remoteUserTypeId != info.UserTypeId.Value) + { + throw new ForyTypeMismatchException(info.UserTypeId.Value, remoteUserTypeId); + } + } + + return; + } + } + + public static ForyTypeId ResolveWireTypeId(ForyTypeId declaredKind, bool registerByName, bool compatible) + { + ForyTypeId baseKind = NormalizeBaseKind(declaredKind); + if (registerByName) + { + return NamedKind(baseKind, compatible); + } + + return IdKind(baseKind, compatible); + } + + public static HashSet AllowedWireTypeIds(ForyTypeId declaredKind, bool registerByName, bool compatible) + { + ForyTypeId baseKind = NormalizeBaseKind(declaredKind); + ForyTypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible); + HashSet allowed = [expected]; + if (baseKind == ForyTypeId.Struct && compatible) + { + allowed.Add(ForyTypeId.CompatibleStruct); + allowed.Add(ForyTypeId.NamedCompatibleStruct); + allowed.Add(ForyTypeId.Struct); + allowed.Add(ForyTypeId.NamedStruct); + } + + return allowed; + } + + private static ForyTypeId NormalizeBaseKind(ForyTypeId kind) + { + return kind switch + { + ForyTypeId.NamedEnum => ForyTypeId.Enum, + ForyTypeId.CompatibleStruct or ForyTypeId.NamedCompatibleStruct or ForyTypeId.NamedStruct => ForyTypeId.Struct, + ForyTypeId.NamedExt => ForyTypeId.Ext, + ForyTypeId.NamedUnion => ForyTypeId.TypedUnion, + _ => kind, + }; + } + + private static ForyTypeId NamedKind(ForyTypeId baseKind, bool compatible) + { + return baseKind switch + { + ForyTypeId.Struct => compatible ? ForyTypeId.NamedCompatibleStruct : ForyTypeId.NamedStruct, + ForyTypeId.Enum => ForyTypeId.NamedEnum, + ForyTypeId.Ext => ForyTypeId.NamedExt, + ForyTypeId.TypedUnion => ForyTypeId.NamedUnion, + _ => baseKind, + }; + } + + private static ForyTypeId IdKind(ForyTypeId baseKind, bool compatible) + { + return baseKind switch + { + ForyTypeId.Struct => compatible ? ForyTypeId.CompatibleStruct : ForyTypeId.Struct, + _ => baseKind, + }; + } + + private static bool WireTypeNeedsUserTypeId(ForyTypeId typeId) + { + return typeId is ForyTypeId.Enum or ForyTypeId.Struct or ForyTypeId.Ext or ForyTypeId.TypedUnion; + } + + private static TypeMeta BuildCompatibleTypeMeta( + ISerializer serializer, + RegisteredTypeInfo info, + ForyTypeId wireTypeId, + bool trackRef) + { + IReadOnlyList fields = serializer.CompatibleTypeMetaFields(trackRef); + bool hasFieldsMeta = fields.Count > 0; + if (info.RegisterByName) + { + if (info.NamespaceName is null) + { + throw new ForyInvalidDataException("missing namespace metadata for name-registered type"); + } + + return new TypeMeta( + (uint)wireTypeId, + null, + info.NamespaceName.Value, + info.TypeName, + true, + fields, + hasFieldsMeta); + } + + if (!info.UserTypeId.HasValue) + { + throw new ForyInvalidDataException("missing user type id metadata for id-registered type"); + } + + return new TypeMeta( + (uint)wireTypeId, + info.UserTypeId.Value, + MetaString.Empty('.', '_'), + MetaString.Empty('$', '_'), + false, + fields, + hasFieldsMeta); + } + + private static void ValidateCompatibleTypeMeta( + TypeMeta remoteTypeMeta, + RegisteredTypeInfo localInfo, + HashSet expectedWireTypes, + ForyTypeId actualWireTypeId) + { + if (remoteTypeMeta.RegisterByName) + { + if (!localInfo.RegisterByName || localInfo.NamespaceName is null) + { + throw new ForyInvalidDataException( + "received name-registered compatible metadata for id-registered local type"); + } + + if (remoteTypeMeta.NamespaceName.Value != localInfo.NamespaceName.Value.Value) + { + throw new ForyInvalidDataException( + $"namespace mismatch: expected {localInfo.NamespaceName.Value.Value}, got {remoteTypeMeta.NamespaceName.Value}"); + } + + if (remoteTypeMeta.TypeName.Value != localInfo.TypeName.Value) + { + throw new ForyInvalidDataException( + $"type name mismatch: expected {localInfo.TypeName.Value}, got {remoteTypeMeta.TypeName.Value}"); + } + } + else + { + if (localInfo.RegisterByName) + { + throw new ForyInvalidDataException( + "received id-registered compatible metadata for name-registered local type"); + } + + if (!remoteTypeMeta.UserTypeId.HasValue) + { + throw new ForyInvalidDataException("missing user type id in compatible type metadata"); + } + + if (!localInfo.UserTypeId.HasValue) + { + throw new ForyInvalidDataException("missing local user type id metadata for id-registered type"); + } + + if (remoteTypeMeta.UserTypeId.Value != localInfo.UserTypeId.Value) + { + throw new ForyTypeMismatchException(localInfo.UserTypeId.Value, remoteTypeMeta.UserTypeId.Value); + } + } + + if (remoteTypeMeta.TypeId.HasValue && + Enum.IsDefined(typeof(ForyTypeId), remoteTypeMeta.TypeId.Value)) + { + ForyTypeId remoteWireTypeId = (ForyTypeId)remoteTypeMeta.TypeId.Value; + if (!expectedWireTypes.Contains(remoteWireTypeId)) + { + throw new ForyTypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value); + } + } + } + + private static void WriteMetaString( + ref WriteContext context, + MetaString value, + IReadOnlyList encodings, + MetaStringEncoder encoder) + { + MetaString normalized = encodings.Contains(value.Encoding) + ? value + : encoder.Encode(value.Value, encodings); + if (!encodings.Contains(normalized.Encoding)) + { + throw new ForyEncodingException("failed to normalize meta string encoding"); + } + + byte[] bytes = normalized.Bytes; + (uint index, bool isNew) = context.MetaStringWriteState.AssignIndexIfAbsent(normalized); + if (isNew) + { + context.Writer.WriteVarUInt32((uint)(bytes.Length << 1)); + if (bytes.Length > 16) + { + context.Writer.WriteInt64(unchecked((long)JavaMetaStringHash(normalized))); + } + else if (bytes.Length > 0) + { + context.Writer.WriteUInt8((byte)normalized.Encoding); + } + + context.Writer.WriteBytes(bytes); + } + else + { + context.Writer.WriteVarUInt32(((index + 1) << 1) | 1); + } + } + + private static MetaString ReadMetaString( + ref ReadContext context, + MetaStringDecoder decoder, + IReadOnlyList encodings) + { + uint header = context.Reader.ReadVarUInt32(); + int length = checked((int)(header >> 1)); + bool isRef = (header & 1) == 1; + if (isRef) + { + int index = length - 1; + MetaString? cached = context.MetaStringReadState.ValueAt(index); + if (cached is null) + { + throw new ForyInvalidDataException($"unknown meta string ref index {index}"); + } + + return cached.Value; + } + + MetaString value; + if (length == 0) + { + value = MetaString.Empty(decoder.SpecialChar1, decoder.SpecialChar2); + } + else + { + MetaStringEncoding encoding; + if (length > 16) + { + long hash = context.Reader.ReadInt64(); + byte rawEncoding = unchecked((byte)(hash & 0xFF)); + encoding = (MetaStringEncoding)rawEncoding; + } + else + { + encoding = (MetaStringEncoding)context.Reader.ReadUInt8(); + } + + if (!encodings.Contains(encoding)) + { + throw new ForyInvalidDataException($"meta string encoding {encoding} not allowed in this context"); + } + + byte[] bytes = context.Reader.ReadBytes(length); + value = decoder.Decode(bytes, encoding); + } + + context.MetaStringReadState.Append(value); + return value; + } + + private static ulong JavaMetaStringHash(MetaString metaString) + { + (ulong h1, _) = MurmurHash3.X64_128(metaString.Bytes, 47); + long hash = unchecked((long)h1); + if (hash != long.MinValue) + { + hash = Math.Abs(hash); + } + + ulong result = unchecked((ulong)hash); + if (result == 0) + { + result += 256; + } + + result &= 0xffffffffffffff00; + result |= (byte)metaString.Encoding; + return result; + } +} diff --git a/csharp/src/Fory/SerializerRegistry.cs b/csharp/src/Fory/SerializerRegistry.cs new file mode 100644 index 0000000000..b0b0769428 --- /dev/null +++ b/csharp/src/Fory/SerializerRegistry.cs @@ -0,0 +1,288 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Collections.Concurrent; + +namespace Apache.Fory; + +public static class SerializerRegistry +{ + private static readonly ConcurrentDictionary Cache = new(); + private static readonly ConcurrentDictionary> GeneratedFactories = new(); + + static SerializerRegistry() + { + RegisterBuiltins(); + GeneratedSerializerRegistry.Register(GeneratedFactories); + } + + public static void RegisterGenerated(Func> factory) + { + GeneratedFactories[typeof(T)] = () => factory(); + Cache.TryRemove(typeof(T), out _); + } + + public static void RegisterCustom(Type type, ISerializer serializer) + { + Cache[type] = serializer; + } + + public static Serializer Get() + { + ISerializer serializer = Get(typeof(T)); + if (serializer is Serializer typed) + { + return typed; + } + + throw new ForyInvalidDataException($"serializer type mismatch for {typeof(T)}"); + } + + public static ISerializer Get(Type type) + { + return Cache.GetOrAdd(type, Create); + } + + private static ISerializer Create(Type type) + { + if (GeneratedFactories.TryGetValue(type, out Func? generatedFactory)) + { + return generatedFactory(); + } + + if (type == typeof(bool)) + { + return BoolSerializer.Instance; + } + + if (type == typeof(sbyte)) + { + return Int8Serializer.Instance; + } + + if (type == typeof(short)) + { + return Int16Serializer.Instance; + } + + if (type == typeof(int)) + { + return Int32Serializer.Instance; + } + + if (type == typeof(long)) + { + return Int64Serializer.Instance; + } + + if (type == typeof(byte)) + { + return UInt8Serializer.Instance; + } + + if (type == typeof(ushort)) + { + return UInt16Serializer.Instance; + } + + if (type == typeof(uint)) + { + return UInt32Serializer.Instance; + } + + if (type == typeof(ulong)) + { + return UInt64Serializer.Instance; + } + + if (type == typeof(float)) + { + return Float32Serializer.Instance; + } + + if (type == typeof(double)) + { + return Float64Serializer.Instance; + } + + if (type == typeof(string)) + { + return StringSerializer.Instance; + } + + if (type == typeof(byte[])) + { + return BinarySerializer.Instance; + } + + if (type == typeof(DateOnly)) + { + return DateOnlySerializer.Instance; + } + + if (type == typeof(DateTimeOffset)) + { + return DateTimeOffsetSerializer.Instance; + } + + if (type == typeof(DateTime)) + { + return DateTimeSerializer.Instance; + } + + if (type == typeof(TimeSpan)) + { + return TimeSpanSerializer.Instance; + } + + if (type == typeof(ForyInt32Fixed)) + { + return ForyInt32FixedSerializer.Instance; + } + + if (type == typeof(ForyInt64Fixed)) + { + return ForyInt64FixedSerializer.Instance; + } + + if (type == typeof(ForyInt64Tagged)) + { + return ForyInt64TaggedSerializer.Instance; + } + + if (type == typeof(ForyUInt32Fixed)) + { + return ForyUInt32FixedSerializer.Instance; + } + + if (type == typeof(ForyUInt64Fixed)) + { + return ForyUInt64FixedSerializer.Instance; + } + + if (type == typeof(ForyUInt64Tagged)) + { + return ForyUInt64TaggedSerializer.Instance; + } + + if (type == typeof(ForyAnyNullValue)) + { + return ForyAnyNullValueSerializer.Instance; + } + + if (type == typeof(object)) + { + return DynamicAnyObjectSerializer.Instance; + } + + if (typeof(Union).IsAssignableFrom(type)) + { + Type serializerType = typeof(UnionSerializer<>).MakeGenericType(type); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + + if (type.IsEnum) + { + Type serializerType = typeof(EnumSerializer<>).MakeGenericType(type); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + + if (type.IsArray) + { + Type elementType = type.GetElementType()!; + Type serializerType = typeof(ArraySerializer<>).MakeGenericType(elementType); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + Type[] genericArgs = type.GetGenericArguments(); + if (genericType == typeof(Nullable<>)) + { + Type serializerType = typeof(NullableSerializer<>).MakeGenericType(genericArgs[0]); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + + if (genericType == typeof(List<>)) + { + Type serializerType = typeof(ListSerializer<>).MakeGenericType(genericArgs[0]); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + + if (genericType == typeof(HashSet<>)) + { + Type serializerType = typeof(SetSerializer<>).MakeGenericType(genericArgs[0]); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + + if (genericType == typeof(Dictionary<,>)) + { + Type serializerType = typeof(MapSerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + + if (genericType == typeof(ForyMap<,>)) + { + Type serializerType = typeof(ForyMapSerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); + return (ISerializer)Activator.CreateInstance(serializerType)!; + } + } + + if (GeneratedSerializerRegistry.TryCreate(type, out ISerializer? generatedSerializer)) + { + return generatedSerializer!; + } + + throw new ForyTypeNotRegisteredException($"No serializer available for {type}"); + } + + private static void RegisterBuiltins() + { + Cache[typeof(bool)] = BoolSerializer.Instance; + Cache[typeof(sbyte)] = Int8Serializer.Instance; + Cache[typeof(short)] = Int16Serializer.Instance; + Cache[typeof(int)] = Int32Serializer.Instance; + Cache[typeof(long)] = Int64Serializer.Instance; + Cache[typeof(byte)] = UInt8Serializer.Instance; + Cache[typeof(ushort)] = UInt16Serializer.Instance; + Cache[typeof(uint)] = UInt32Serializer.Instance; + Cache[typeof(ulong)] = UInt64Serializer.Instance; + Cache[typeof(float)] = Float32Serializer.Instance; + Cache[typeof(double)] = Float64Serializer.Instance; + Cache[typeof(string)] = StringSerializer.Instance; + Cache[typeof(byte[])] = BinarySerializer.Instance; + Cache[typeof(object)] = DynamicAnyObjectSerializer.Instance; + Cache[typeof(ForyAnyNullValue)] = ForyAnyNullValueSerializer.Instance; + Cache[typeof(Union)] = new UnionSerializer(); + } +} + +internal static partial class GeneratedSerializerRegistry +{ + public static void Register(ConcurrentDictionary> factories) + { + _ = factories; + } + + public static bool TryCreate(Type type, out ISerializer? serializer) + { + _ = type; + serializer = null; + return false; + } +} diff --git a/csharp/src/Fory/TypeMeta.cs b/csharp/src/Fory/TypeMeta.cs new file mode 100644 index 0000000000..0e56db3747 --- /dev/null +++ b/csharp/src/Fory/TypeMeta.cs @@ -0,0 +1,690 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +internal static class TypeMetaConstants +{ + public const int SmallNumFieldsThreshold = 0b1_1111; + public const byte RegisterByNameFlag = 0b10_0000; + public const int FieldNameSizeThreshold = 0b1111; + public const int BigNameThreshold = 0b11_1111; + public const ulong TypeMetaHasFieldsMetaFlag = 1UL << 8; + public const ulong TypeMetaCompressedFlag = 1UL << 9; + public const ulong TypeMetaSizeMask = 0xFF; + public const ulong TypeMetaNumHashBits = 50; + public const ulong TypeMetaHashSeed = 47; + public const uint NoUserTypeId = uint.MaxValue; +} + +public static class TypeMetaEncodings +{ + public static readonly MetaStringEncoding[] NamespaceMetaStringEncodings = + [ + MetaStringEncoding.Utf8, + MetaStringEncoding.AllToLowerSpecial, + MetaStringEncoding.LowerUpperDigitSpecial, + ]; + + public static readonly MetaStringEncoding[] TypeNameMetaStringEncodings = + [ + MetaStringEncoding.Utf8, + MetaStringEncoding.AllToLowerSpecial, + MetaStringEncoding.LowerUpperDigitSpecial, + MetaStringEncoding.FirstToLowerSpecial, + ]; + + public static readonly MetaStringEncoding[] FieldNameMetaStringEncodings = + [ + MetaStringEncoding.Utf8, + MetaStringEncoding.AllToLowerSpecial, + MetaStringEncoding.LowerUpperDigitSpecial, + ]; +} + +internal static class TypeMetaUtils +{ + public static int EncodingIndexOf(IReadOnlyList encodings, MetaStringEncoding encoding) + { + for (int i = 0; i < encodings.Count; i++) + { + if (encodings[i] == encoding) + { + return i; + } + } + + return -1; + } + + public static string LowerCamelToLowerUnderscore(string name) + { + if (name.Length == 0) + { + return name; + } + + Span chars = name.ToCharArray(); + var sb = new System.Text.StringBuilder(name.Length + 4); + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + if (char.IsUpper(c)) + { + if (i > 0) + { + bool prevUpper = char.IsUpper(chars[i - 1]); + bool nextUpperOrEnd = i + 1 >= chars.Length || char.IsUpper(chars[i + 1]); + if (!prevUpper || !nextUpperOrEnd) + { + sb.Append('_'); + } + } + + sb.Append(char.ToLowerInvariant(c)); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } +} + +public sealed class TypeMetaFieldType : IEquatable +{ + public TypeMetaFieldType( + uint typeId, + bool nullable, + bool trackRef = false, + IReadOnlyList? generics = null) + { + TypeId = typeId; + Nullable = nullable; + TrackRef = trackRef; + Generics = generics ?? []; + } + + public uint TypeId { get; } + + public bool Nullable { get; } + + public bool TrackRef { get; } + + public IReadOnlyList Generics { get; } + + internal void Write(ByteWriter writer, bool writeFlags, bool? nullableOverride = null) + { + if (writeFlags) + { + uint header = TypeId << 2; + if (nullableOverride ?? Nullable) + { + header |= 0b10; + } + + if (TrackRef) + { + header |= 0b1; + } + + writer.WriteVarUInt32(header); + } + else + { + writer.WriteUInt8(unchecked((byte)TypeId)); + } + + if (TypeId is (uint)ForyTypeId.List or (uint)ForyTypeId.Set) + { + TypeMetaFieldType element = Generics.Count > 0 + ? Generics[0] + : new TypeMetaFieldType((uint)ForyTypeId.Unknown, true); + element.Write(writer, true, element.Nullable); + } + else if (TypeId == (uint)ForyTypeId.Map) + { + TypeMetaFieldType key = Generics.Count > 0 + ? Generics[0] + : new TypeMetaFieldType((uint)ForyTypeId.Unknown, true); + TypeMetaFieldType value = Generics.Count > 1 + ? Generics[1] + : new TypeMetaFieldType((uint)ForyTypeId.Unknown, true); + key.Write(writer, true, key.Nullable); + value.Write(writer, true, value.Nullable); + } + } + + internal static TypeMetaFieldType Read( + ByteReader reader, + bool readFlags, + bool? nullable = null, + bool? trackRef = null) + { + uint header = readFlags ? reader.ReadVarUInt32() : reader.ReadUInt8(); + + uint typeId; + bool resolvedNullable; + bool resolvedTrackRef; + if (readFlags) + { + typeId = header >> 2; + resolvedNullable = (header & 0b10) != 0; + resolvedTrackRef = (header & 0b1) != 0; + } + else + { + typeId = header; + resolvedNullable = nullable ?? false; + resolvedTrackRef = trackRef ?? false; + } + + if (typeId is (uint)ForyTypeId.List or (uint)ForyTypeId.Set) + { + TypeMetaFieldType element = Read(reader, true); + return new TypeMetaFieldType(typeId, resolvedNullable, resolvedTrackRef, [element]); + } + + if (typeId == (uint)ForyTypeId.Map) + { + TypeMetaFieldType key = Read(reader, true); + TypeMetaFieldType value = Read(reader, true); + return new TypeMetaFieldType(typeId, resolvedNullable, resolvedTrackRef, [key, value]); + } + + return new TypeMetaFieldType(typeId, resolvedNullable, resolvedTrackRef); + } + + public bool Equals(TypeMetaFieldType? other) + { + if (other is null) + { + return false; + } + + return TypeId == other.TypeId && + Nullable == other.Nullable && + TrackRef == other.TrackRef && + Generics.SequenceEqual(other.Generics); + } + + public override bool Equals(object? obj) + { + return obj is TypeMetaFieldType other && Equals(other); + } + + public override int GetHashCode() + { + HashCode hc = new(); + hc.Add(TypeId); + hc.Add(Nullable); + hc.Add(TrackRef); + foreach (TypeMetaFieldType t in Generics) + { + hc.Add(t); + } + + return hc.ToHashCode(); + } +} + +public sealed class TypeMetaFieldInfo : IEquatable +{ + public TypeMetaFieldInfo(short? fieldId, string fieldName, TypeMetaFieldType fieldType) + { + FieldId = fieldId; + FieldName = fieldName; + FieldType = fieldType; + } + + public short? FieldId { get; } + + public string FieldName { get; } + + public TypeMetaFieldType FieldType { get; } + + internal void Write(ByteWriter writer) + { + byte header = 0; + if (FieldType.TrackRef) + { + header |= 0b1; + } + + if (FieldType.Nullable) + { + header |= 0b10; + } + + if (FieldId.HasValue) + { + short fieldId = FieldId.Value; + if (fieldId < 0) + { + throw new ForyEncodingException("negative field id is invalid"); + } + + int size = fieldId; + header |= 0b11 << 6; + if (size >= TypeMetaConstants.FieldNameSizeThreshold) + { + header |= 0b0011_1100; + writer.WriteUInt8(header); + writer.WriteVarUInt32((uint)(size - TypeMetaConstants.FieldNameSizeThreshold)); + } + else + { + header |= (byte)(size << 2); + writer.WriteUInt8(header); + } + + FieldType.Write(writer, false); + return; + } + + string snakeName = TypeMetaUtils.LowerCamelToLowerUnderscore(FieldName); + MetaString encoded = MetaStringEncoder.FieldName.Encode(snakeName, TypeMetaEncodings.FieldNameMetaStringEncodings); + int encodingIndex = Array.IndexOf(TypeMetaEncodings.FieldNameMetaStringEncodings, encoded.Encoding); + if (encodingIndex < 0) + { + throw new ForyEncodingException("unsupported field name encoding"); + } + + int encodedSize = encoded.Bytes.Length - 1; + header |= (byte)(encodingIndex << 6); + if (encodedSize >= TypeMetaConstants.FieldNameSizeThreshold) + { + header |= 0b0011_1100; + writer.WriteUInt8(header); + writer.WriteVarUInt32((uint)(encodedSize - TypeMetaConstants.FieldNameSizeThreshold)); + } + else + { + header |= (byte)(encodedSize << 2); + writer.WriteUInt8(header); + } + + FieldType.Write(writer, false); + writer.WriteBytes(encoded.Bytes); + } + + internal static TypeMetaFieldInfo Read(ByteReader reader) + { + byte header = reader.ReadUInt8(); + int encodingFlags = (header >> 6) & 0b11; + int size = (header >> 2) & 0b1111; + if (size == TypeMetaConstants.FieldNameSizeThreshold) + { + size += (int)reader.ReadVarUInt32(); + } + + size += 1; + + bool nullable = (header & 0b10) != 0; + bool trackRef = (header & 0b1) != 0; + TypeMetaFieldType fieldType = TypeMetaFieldType.Read(reader, false, nullable, trackRef); + + if (encodingFlags == 3) + { + short fieldId = unchecked((short)(size - 1)); + return new TypeMetaFieldInfo(fieldId, $"$tag{fieldId}", fieldType); + } + + if (encodingFlags >= TypeMetaEncodings.FieldNameMetaStringEncodings.Length) + { + throw new ForyInvalidDataException("invalid field name encoding id"); + } + + byte[] nameBytes = reader.ReadBytes(size); + string name = MetaStringDecoder.FieldName.Decode( + nameBytes, + TypeMetaEncodings.FieldNameMetaStringEncodings[encodingFlags]).Value; + return new TypeMetaFieldInfo(null, name, fieldType); + } + + public bool Equals(TypeMetaFieldInfo? other) + { + if (other is null) + { + return false; + } + + return FieldId == other.FieldId && + FieldName == other.FieldName && + FieldType.Equals(other.FieldType); + } + + public override bool Equals(object? obj) + { + return obj is TypeMetaFieldInfo other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(FieldId, FieldName, FieldType); + } +} + +public sealed class TypeMeta : IEquatable +{ + public TypeMeta( + uint? typeId, + uint? userTypeId, + MetaString namespaceName, + MetaString typeName, + bool registerByName, + IReadOnlyList fields, + bool hasFieldsMeta = true, + bool compressed = false, + ulong headerHash = 0) + { + if (registerByName) + { + if (typeName.Value.Length == 0) + { + throw new ForyEncodingException("type name is required in register-by-name mode"); + } + } + else + { + if (!typeId.HasValue) + { + throw new ForyEncodingException("type id is required in register-by-id mode"); + } + + if (!userTypeId.HasValue || userTypeId.Value == TypeMetaConstants.NoUserTypeId) + { + throw new ForyEncodingException("user type id is required in register-by-id mode"); + } + } + + TypeId = typeId; + UserTypeId = userTypeId; + NamespaceName = namespaceName; + TypeName = typeName; + RegisterByName = registerByName; + Fields = fields; + HasFieldsMeta = hasFieldsMeta; + Compressed = compressed; + HeaderHash = headerHash; + } + + public uint? TypeId { get; } + + public uint? UserTypeId { get; } + + public MetaString NamespaceName { get; } + + public MetaString TypeName { get; } + + public bool RegisterByName { get; } + + public IReadOnlyList Fields { get; } + + public bool HasFieldsMeta { get; } + + public bool Compressed { get; } + + public ulong HeaderHash { get; } + + public byte[] Encode() + { + if (Compressed) + { + throw new ForyEncodingException("compressed TypeMeta is not supported yet"); + } + + byte[] body = EncodeBody(); + (ulong bodyHash, _) = MurmurHash3.X64_128(body, TypeMetaConstants.TypeMetaHashSeed); + ulong shifted = bodyHash << (int)(64 - TypeMetaConstants.TypeMetaNumHashBits); + long signed = unchecked((long)shifted); + long absSigned = signed == long.MinValue ? signed : Math.Abs(signed); + + ulong header = unchecked((ulong)absSigned); + if (HasFieldsMeta) + { + header |= TypeMetaConstants.TypeMetaHasFieldsMetaFlag; + } + + if (Compressed) + { + header |= TypeMetaConstants.TypeMetaCompressedFlag; + } + + header |= (ulong)Math.Min(body.Length, (int)TypeMetaConstants.TypeMetaSizeMask); + ByteWriter writer = new(body.Length + 16); + writer.WriteUInt64(header); + if (body.Length >= (int)TypeMetaConstants.TypeMetaSizeMask) + { + writer.WriteVarUInt32((uint)(body.Length - (int)TypeMetaConstants.TypeMetaSizeMask)); + } + + writer.WriteBytes(body); + return writer.ToArray(); + } + + public static TypeMeta Decode(byte[] bytes) + { + return Decode(new ByteReader(bytes)); + } + + public static TypeMeta Decode(ByteReader reader) + { + ulong header = reader.ReadUInt64(); + bool compressed = (header & TypeMetaConstants.TypeMetaCompressedFlag) != 0; + bool hasFieldsMeta = (header & TypeMetaConstants.TypeMetaHasFieldsMetaFlag) != 0; + int metaSize = (int)(header & TypeMetaConstants.TypeMetaSizeMask); + if (metaSize == (int)TypeMetaConstants.TypeMetaSizeMask) + { + metaSize += (int)reader.ReadVarUInt32(); + } + + byte[] encodedBody = reader.ReadBytes(metaSize); + if (compressed) + { + throw new ForyEncodingException("compressed TypeMeta is not supported yet"); + } + + ByteReader bodyReader = new(encodedBody); + byte metaHeader = bodyReader.ReadUInt8(); + int numFields = metaHeader & TypeMetaConstants.SmallNumFieldsThreshold; + if (numFields == TypeMetaConstants.SmallNumFieldsThreshold) + { + numFields += (int)bodyReader.ReadVarUInt32(); + } + + bool registerByName = (metaHeader & TypeMetaConstants.RegisterByNameFlag) != 0; + uint? typeId; + uint? userTypeId; + MetaString namespaceName; + MetaString typeName; + if (registerByName) + { + namespaceName = ReadName(bodyReader, MetaStringDecoder.Namespace, TypeMetaEncodings.NamespaceMetaStringEncodings); + typeName = ReadName(bodyReader, MetaStringDecoder.TypeName, TypeMetaEncodings.TypeNameMetaStringEncodings); + typeId = null; + userTypeId = null; + } + else + { + typeId = bodyReader.ReadUInt8(); + userTypeId = bodyReader.ReadVarUInt32(); + namespaceName = MetaString.Empty('.', '_'); + typeName = MetaString.Empty('$', '_'); + } + + List fields = new(numFields); + for (int i = 0; i < numFields; i++) + { + fields.Add(TypeMetaFieldInfo.Read(bodyReader)); + } + + if (bodyReader.Remaining != 0) + { + throw new ForyInvalidDataException("unexpected trailing bytes in TypeMeta body"); + } + + return new TypeMeta( + typeId, + userTypeId, + namespaceName, + typeName, + registerByName, + fields, + hasFieldsMeta, + compressed, + header >> (int)(64 - TypeMetaConstants.TypeMetaNumHashBits)); + } + + private byte[] EncodeBody() + { + ByteWriter writer = new(128); + byte metaHeader = (byte)Math.Min(Fields.Count, TypeMetaConstants.SmallNumFieldsThreshold); + if (RegisterByName) + { + metaHeader |= TypeMetaConstants.RegisterByNameFlag; + } + + writer.WriteUInt8(metaHeader); + if (Fields.Count >= TypeMetaConstants.SmallNumFieldsThreshold) + { + writer.WriteVarUInt32((uint)(Fields.Count - TypeMetaConstants.SmallNumFieldsThreshold)); + } + + if (RegisterByName) + { + WriteName(writer, NamespaceName, TypeMetaEncodings.NamespaceMetaStringEncodings); + WriteName(writer, TypeName, TypeMetaEncodings.TypeNameMetaStringEncodings); + } + else + { + if (!TypeId.HasValue) + { + throw new ForyEncodingException("type id is required in register-by-id mode"); + } + + if (!UserTypeId.HasValue || UserTypeId == TypeMetaConstants.NoUserTypeId) + { + throw new ForyEncodingException("user type id is required in register-by-id mode"); + } + + writer.WriteUInt8(unchecked((byte)TypeId.Value)); + writer.WriteVarUInt32(UserTypeId.Value); + } + + foreach (TypeMetaFieldInfo field in Fields) + { + field.Write(writer); + } + + return writer.ToArray(); + } + + private static void WriteName(ByteWriter writer, MetaString name, IReadOnlyList encodings) + { + MetaString normalized = encodings.Contains(name.Encoding) + ? name + : (encodings.SequenceEqual(TypeMetaEncodings.NamespaceMetaStringEncodings) + ? MetaStringEncoder.Namespace.Encode(name.Value, encodings) + : encodings.SequenceEqual(TypeMetaEncodings.TypeNameMetaStringEncodings) + ? MetaStringEncoder.TypeName.Encode(name.Value, encodings) + : MetaStringEncoder.FieldName.Encode(name.Value, encodings)); + + int encodingIndex = TypeMetaUtils.EncodingIndexOf(encodings, normalized.Encoding); + if (encodingIndex < 0) + { + throw new ForyEncodingException("failed to normalize meta string encoding"); + } + + byte[] bytes = normalized.Bytes; + if (bytes.Length >= TypeMetaConstants.BigNameThreshold) + { + writer.WriteUInt8((byte)((TypeMetaConstants.BigNameThreshold << 2) | encodingIndex)); + writer.WriteVarUInt32((uint)(bytes.Length - TypeMetaConstants.BigNameThreshold)); + } + else + { + writer.WriteUInt8((byte)((bytes.Length << 2) | encodingIndex)); + } + + writer.WriteBytes(bytes); + } + + private static MetaString ReadName(ByteReader reader, MetaStringDecoder decoder, IReadOnlyList encodings) + { + byte header = reader.ReadUInt8(); + int encodingIndex = header & 0b11; + if (encodingIndex >= encodings.Count) + { + throw new ForyInvalidDataException("invalid meta string encoding index"); + } + + int length = header >> 2; + if (length >= TypeMetaConstants.BigNameThreshold) + { + length = TypeMetaConstants.BigNameThreshold + (int)reader.ReadVarUInt32(); + } + + byte[] bytes = reader.ReadBytes(length); + return decoder.Decode(bytes, encodings[encodingIndex]); + } + + public bool Equals(TypeMeta? other) + { + if (other is null) + { + return false; + } + + return TypeId == other.TypeId && + UserTypeId == other.UserTypeId && + NamespaceName.Equals(other.NamespaceName) && + TypeName.Equals(other.TypeName) && + RegisterByName == other.RegisterByName && + Fields.SequenceEqual(other.Fields) && + HasFieldsMeta == other.HasFieldsMeta && + Compressed == other.Compressed && + HeaderHash == other.HeaderHash; + } + + public override bool Equals(object? obj) + { + return obj is TypeMeta other && Equals(other); + } + + public override int GetHashCode() + { + HashCode hc = new(); + hc.Add(TypeId); + hc.Add(UserTypeId); + hc.Add(NamespaceName); + hc.Add(TypeName); + hc.Add(RegisterByName); + hc.Add(HasFieldsMeta); + hc.Add(Compressed); + hc.Add(HeaderHash); + foreach (TypeMetaFieldInfo f in Fields) + { + hc.Add(f); + } + + return hc.ToHashCode(); + } + +} diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs new file mode 100644 index 0000000000..8e3a33f58e --- /dev/null +++ b/csharp/src/Fory/TypeResolver.cs @@ -0,0 +1,369 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public sealed record RegisteredTypeInfo( + uint? UserTypeId, + ForyTypeId Kind, + bool RegisterByName, + MetaString? NamespaceName, + MetaString TypeName, + ISerializer Serializer); + +internal enum DynamicRegistrationMode +{ + IdOnly, + NameOnly, + Mixed, +} + +internal readonly record struct TypeNameKey(string NamespaceName, string TypeName); + +internal sealed class TypeReader +{ + public required ForyTypeId Kind { get; init; } + public required Func Reader { get; init; } + public required Func CompatibleReader { get; init; } +} + +public sealed class TypeResolver +{ + private readonly Dictionary _byType = []; + private readonly Dictionary _byUserTypeId = []; + private readonly Dictionary _byTypeName = []; + private readonly Dictionary _registrationModeByKind = []; + + public void Register(Type type, uint id, ISerializer? explicitSerializer = null) + { + ISerializer serializer = explicitSerializer ?? SerializerRegistry.Get(type); + RegisteredTypeInfo info = new( + id, + serializer.StaticTypeId, + false, + null, + MetaString.Empty('$', '_'), + serializer); + _byType[type] = info; + MarkRegistrationMode(info.Kind, false); + _byUserTypeId[id] = new TypeReader + { + Kind = serializer.StaticTypeId, + Reader = context => serializer.Read(ref context, RefMode.None, false), + CompatibleReader = (context, typeMeta) => + { + context.PushCompatibleTypeMeta(type, typeMeta); + return serializer.Read(ref context, RefMode.None, false); + }, + }; + } + + public void Register(Type type, string namespaceName, string typeName, ISerializer? explicitSerializer = null) + { + ISerializer serializer = explicitSerializer ?? SerializerRegistry.Get(type); + MetaString namespaceMeta = MetaStringEncoder.Namespace.Encode(namespaceName, TypeMetaEncodings.NamespaceMetaStringEncodings); + MetaString typeNameMeta = MetaStringEncoder.TypeName.Encode(typeName, TypeMetaEncodings.TypeNameMetaStringEncodings); + RegisteredTypeInfo info = new( + null, + serializer.StaticTypeId, + true, + namespaceMeta, + typeNameMeta, + serializer); + _byType[type] = info; + MarkRegistrationMode(info.Kind, true); + _byTypeName[new TypeNameKey(namespaceName, typeName)] = new TypeReader + { + Kind = serializer.StaticTypeId, + Reader = context => serializer.Read(ref context, RefMode.None, false), + CompatibleReader = (context, typeMeta) => + { + context.PushCompatibleTypeMeta(type, typeMeta); + return serializer.Read(ref context, RefMode.None, false); + }, + }; + } + + public RegisteredTypeInfo? RegisteredTypeInfo(Type type) + { + return _byType.TryGetValue(type, out RegisteredTypeInfo? info) ? info : null; + } + + public RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) + { + if (_byType.TryGetValue(type, out RegisteredTypeInfo? info)) + { + return info; + } + + throw new ForyTypeNotRegisteredException($"{type} is not registered"); + } + + public object? ReadByUserTypeId(uint userTypeId, ref ReadContext context, TypeMeta? compatibleTypeMeta = null) + { + if (!_byUserTypeId.TryGetValue(userTypeId, out TypeReader? entry)) + { + throw new ForyTypeNotRegisteredException($"user_type_id={userTypeId}"); + } + + return compatibleTypeMeta is null + ? entry.Reader(context) + : entry.CompatibleReader(context, compatibleTypeMeta); + } + + public object? ReadByTypeName(string namespaceName, string typeName, ref ReadContext context, TypeMeta? compatibleTypeMeta = null) + { + if (!_byTypeName.TryGetValue(new TypeNameKey(namespaceName, typeName), out TypeReader? entry)) + { + throw new ForyTypeNotRegisteredException($"namespace={namespaceName}, type={typeName}"); + } + + return compatibleTypeMeta is null + ? entry.Reader(context) + : entry.CompatibleReader(context, compatibleTypeMeta); + } + + public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) + { + uint rawTypeId = context.Reader.ReadVarUInt32(); + if (!Enum.IsDefined(typeof(ForyTypeId), rawTypeId)) + { + throw new ForyInvalidDataException($"unknown dynamic type id {rawTypeId}"); + } + + ForyTypeId wireTypeId = (ForyTypeId)rawTypeId; + switch (wireTypeId) + { + case ForyTypeId.CompatibleStruct: + case ForyTypeId.NamedCompatibleStruct: + { + TypeMeta typeMeta = context.ReadCompatibleTypeMeta(); + if (typeMeta.RegisterByName) + { + return new DynamicTypeInfo(wireTypeId, null, typeMeta.NamespaceName, typeMeta.TypeName, typeMeta); + } + + return new DynamicTypeInfo(wireTypeId, typeMeta.UserTypeId, null, null, typeMeta); + } + case ForyTypeId.NamedStruct: + case ForyTypeId.NamedEnum: + case ForyTypeId.NamedExt: + case ForyTypeId.NamedUnion: + { + MetaString namespaceName = ReadMetaString(context.Reader, MetaStringDecoder.Namespace, TypeMetaEncodings.NamespaceMetaStringEncodings); + MetaString typeName = ReadMetaString(context.Reader, MetaStringDecoder.TypeName, TypeMetaEncodings.TypeNameMetaStringEncodings); + return new DynamicTypeInfo(wireTypeId, null, namespaceName, typeName, null); + } + case ForyTypeId.Struct: + case ForyTypeId.Enum: + case ForyTypeId.Ext: + case ForyTypeId.TypedUnion: + { + DynamicRegistrationMode mode = DynamicRegistrationModeFor(wireTypeId); + if (mode == DynamicRegistrationMode.IdOnly) + { + return new DynamicTypeInfo(wireTypeId, context.Reader.ReadVarUInt32(), null, null, null); + } + + if (mode == DynamicRegistrationMode.NameOnly) + { + MetaString namespaceName = ReadMetaString(context.Reader, MetaStringDecoder.Namespace, TypeMetaEncodings.NamespaceMetaStringEncodings); + MetaString typeName = ReadMetaString(context.Reader, MetaStringDecoder.TypeName, TypeMetaEncodings.TypeNameMetaStringEncodings); + return new DynamicTypeInfo(wireTypeId, null, namespaceName, typeName, null); + } + + throw new ForyInvalidDataException($"ambiguous dynamic type registration mode for {wireTypeId}"); + } + default: + return new DynamicTypeInfo(wireTypeId, null, null, null, null); + } + } + + public object? ReadDynamicValue(DynamicTypeInfo typeInfo, ref ReadContext context) + { + switch (typeInfo.WireTypeId) + { + case ForyTypeId.Bool: + return BoolSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.Int8: + return Int8Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.Int16: + return Int16Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.Int32: + return ForyInt32FixedSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.VarInt32: + return Int32Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.Int64: + return ForyInt64FixedSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.VarInt64: + return Int64Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.TaggedInt64: + return ForyInt64TaggedSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.UInt8: + return UInt8Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.UInt16: + return UInt16Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.UInt32: + return ForyUInt32FixedSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.VarUInt32: + return UInt32Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.UInt64: + return ForyUInt64FixedSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.VarUInt64: + return UInt64Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.TaggedUInt64: + return ForyUInt64TaggedSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.Float32: + return Float32Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.Float64: + return Float64Serializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.String: + return StringSerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.Binary: + case ForyTypeId.UInt8Array: + return BinarySerializer.Instance.Read(ref context, RefMode.None, false); + case ForyTypeId.BoolArray: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.Int8Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.Int16Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.Int32Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.Int64Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.UInt16Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.UInt32Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.UInt64Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.Float32Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.Float64Array: + return new ArraySerializer().Read(ref context, RefMode.None, false); + case ForyTypeId.List: + return DynamicAnyCodec.ReadAnyList(ref context, RefMode.None, false) ?? []; + case ForyTypeId.Map: + return DynamicAnyCodec.ReadDynamicAnyMapValue(ref context); + case ForyTypeId.Union: + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + case ForyTypeId.Struct: + case ForyTypeId.Enum: + case ForyTypeId.Ext: + case ForyTypeId.TypedUnion: + { + if (typeInfo.UserTypeId.HasValue) + { + return ReadByUserTypeId(typeInfo.UserTypeId.Value, ref context); + } + + if (typeInfo.NamespaceName.HasValue && typeInfo.TypeName.HasValue) + { + return ReadByTypeName(typeInfo.NamespaceName.Value.Value, typeInfo.TypeName.Value.Value, ref context); + } + + throw new ForyInvalidDataException($"missing dynamic registration info for {typeInfo.WireTypeId}"); + } + case ForyTypeId.NamedStruct: + case ForyTypeId.NamedEnum: + case ForyTypeId.NamedExt: + case ForyTypeId.NamedUnion: + { + if (!typeInfo.NamespaceName.HasValue || !typeInfo.TypeName.HasValue) + { + throw new ForyInvalidDataException($"missing dynamic type name for {typeInfo.WireTypeId}"); + } + + return ReadByTypeName(typeInfo.NamespaceName.Value.Value, typeInfo.TypeName.Value.Value, ref context); + } + case ForyTypeId.CompatibleStruct: + case ForyTypeId.NamedCompatibleStruct: + { + if (typeInfo.CompatibleTypeMeta is null) + { + throw new ForyInvalidDataException($"missing compatible type meta for {typeInfo.WireTypeId}"); + } + + TypeMeta compatibleTypeMeta = typeInfo.CompatibleTypeMeta; + if (compatibleTypeMeta.RegisterByName) + { + return ReadByTypeName( + compatibleTypeMeta.NamespaceName.Value, + compatibleTypeMeta.TypeName.Value, + ref context, + compatibleTypeMeta); + } + + if (!compatibleTypeMeta.UserTypeId.HasValue) + { + throw new ForyInvalidDataException("missing user type id in compatible dynamic type meta"); + } + + return ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value, ref context, compatibleTypeMeta); + } + case ForyTypeId.None: + return new ForyAnyNullValue(); + default: + throw new ForyInvalidDataException($"unsupported dynamic type id {typeInfo.WireTypeId}"); + } + } + + private void MarkRegistrationMode(ForyTypeId kind, bool registerByName) + { + DynamicRegistrationMode mode = registerByName ? DynamicRegistrationMode.NameOnly : DynamicRegistrationMode.IdOnly; + if (!_registrationModeByKind.TryGetValue(kind, out DynamicRegistrationMode existing)) + { + _registrationModeByKind[kind] = mode; + return; + } + + if (existing != mode) + { + _registrationModeByKind[kind] = DynamicRegistrationMode.Mixed; + } + } + + private DynamicRegistrationMode DynamicRegistrationModeFor(ForyTypeId kind) + { + if (_registrationModeByKind.TryGetValue(kind, out DynamicRegistrationMode mode)) + { + return mode; + } + + throw new ForyTypeNotRegisteredException($"no dynamic registration mode for kind {kind}"); + } + + private static MetaString ReadMetaString(ByteReader reader, MetaStringDecoder decoder, IReadOnlyList encodings) + { + byte header = reader.ReadUInt8(); + int encodingIndex = header & 0b11; + if (encodingIndex >= encodings.Count) + { + throw new ForyInvalidDataException("invalid meta string encoding index"); + } + + int length = header >> 2; + if (length >= 0b11_1111) + { + length = 0b11_1111 + (int)reader.ReadVarUInt32(); + } + + byte[] bytes = reader.ReadBytes(length); + return decoder.Decode(bytes, encodings[encodingIndex]); + } +} diff --git a/csharp/src/Fory/Union.cs b/csharp/src/Fory/Union.cs new file mode 100644 index 0000000000..1ab18c38b6 --- /dev/null +++ b/csharp/src/Fory/Union.cs @@ -0,0 +1,133 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public class Union : IEquatable +{ + public Union(int index, object? value) + : this(index, value, (int)ForyTypeId.Unknown) + { + } + + public Union(int index, object? value, int valueTypeId) + { + Index = index; + Value = value; + ValueTypeId = valueTypeId; + } + + public int Index { get; } + + public object? Value { get; } + + public int ValueTypeId { get; } + + public bool HasValue => Value is not null; + + public T GetValue() + { + if (Value is T typed) + { + return typed; + } + + throw new InvalidOperationException( + $"union value type mismatch: expected {typeof(T)}, got {Value?.GetType()}"); + } + + public bool Equals(Union? other) + { + return other is not null && Index == other.Index && Equals(Value, other.Value); + } + + public override bool Equals(object? obj) + { + return obj is Union other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Index, Value); + } + + public override string ToString() + { + return $"Union{{index={Index}, value={Value}}}"; + } +} + +public sealed class Union2 : Union +{ + private Union2(int index, object? value) + : this(index, value, (int)ForyTypeId.Unknown) + { + } + + private Union2(int index, object? value, int valueTypeId) + : base(index, value, valueTypeId) + { + if (index is < 0 or > 1) + { + throw new ArgumentOutOfRangeException(nameof(index), $"Union2 index must be 0 or 1, got {index}"); + } + } + + public static Union2 OfT1(T1 value) + { + return new Union2(0, value); + } + + public static Union2 OfT2(T2 value) + { + return new Union2(1, value); + } + + public static Union2 Of(int index, object? value) + { + return new Union2(index, value); + } + + public bool IsT1 => Index == 0; + + public bool IsT2 => Index == 1; + + public T1 GetT1() + { + if (!IsT1) + { + throw new InvalidOperationException($"Union2 currently holds case {Index}, not case 0"); + } + + return GetValue(); + } + + public T2 GetT2() + { + if (!IsT2) + { + throw new InvalidOperationException($"Union2 currently holds case {Index}, not case 1"); + } + + return GetValue(); + } + + public override string ToString() + { + return $"Union2{{index={Index}, value={Value}}}"; + } +} diff --git a/csharp/src/Fory/UnionSerializer.cs b/csharp/src/Fory/UnionSerializer.cs new file mode 100644 index 0000000000..debbb8569f --- /dev/null +++ b/csharp/src/Fory/UnionSerializer.cs @@ -0,0 +1,99 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Linq.Expressions; +using System.Reflection; + +namespace Apache.Fory; + +public sealed class UnionSerializer : Serializer + where TUnion : Union +{ + private static readonly Func Factory = BuildFactory(); + + public override ForyTypeId StaticTypeId => ForyTypeId.TypedUnion; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override TUnion DefaultValue => null!; + + public override bool IsNone(TUnion value) + { + return value is null; + } + + public override void WriteData(ref WriteContext context, in TUnion value, bool hasGenerics) + { + _ = hasGenerics; + if (value is null) + { + throw new ForyInvalidDataException("union value is null"); + } + + context.Writer.WriteVarUInt32((uint)value.Index); + DynamicAnyCodec.WriteAny(ref context, value.Value, RefMode.Tracking, true, false); + } + + public override TUnion ReadData(ref ReadContext context) + { + uint rawCaseId = context.Reader.ReadVarUInt32(); + if (rawCaseId > int.MaxValue) + { + throw new ForyInvalidDataException($"union case id out of range: {rawCaseId}"); + } + + object? caseValue = DynamicAnyCodec.ReadAny(ref context, RefMode.Tracking, true); + return Factory((int)rawCaseId, caseValue); + } + + private static Func BuildFactory() + { + if (typeof(TUnion) == typeof(Union)) + { + return (index, value) => (TUnion)(object)new Union(index, value); + } + + ConstructorInfo? ctor = typeof(TUnion).GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + [typeof(int), typeof(object)], + modifiers: null); + if (ctor is not null) + { + ParameterExpression indexParam = Expression.Parameter(typeof(int), "index"); + ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); + NewExpression created = Expression.New(ctor, indexParam, valueParam); + return Expression.Lambda>(created, indexParam, valueParam).Compile(); + } + + MethodInfo? ofFactory = typeof(TUnion).GetMethod( + "Of", + BindingFlags.Public | BindingFlags.Static, + binder: null, + [typeof(int), typeof(object)], + modifiers: null); + if (ofFactory is not null && typeof(TUnion).IsAssignableFrom(ofFactory.ReturnType)) + { + return (index, value) => (TUnion)ofFactory.Invoke(null, [index, value])!; + } + + throw new ForyInvalidDataException( + $"union type {typeof(TUnion)} must define (int, object) constructor or static Of(int, object)"); + } +} diff --git a/csharp/tests/Fory.Tests/Fory.Tests.csproj b/csharp/tests/Fory.Tests/Fory.Tests.csproj new file mode 100644 index 0000000000..e9f9edbab4 --- /dev/null +++ b/csharp/tests/Fory.Tests/Fory.Tests.csproj @@ -0,0 +1,28 @@ + + + net8.0 + 12.0 + enable + enable + false + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs new file mode 100644 index 0000000000..7bf435d835 --- /dev/null +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -0,0 +1,402 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Buffers; +using Apache.Fory; +using ForyRuntime = Apache.Fory.Fory; + +namespace Apache.Fory.Tests; + +[ForyObject] +public enum TestColor +{ + Green, + Red, + Blue, + White, +} + +[ForyObject] +public sealed class Address +{ + public string Street { get; set; } = string.Empty; + public int Zip { get; set; } +} + +[ForyObject] +public sealed class Person +{ + public long Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Nickname { get; set; } + public List Scores { get; set; } = []; + public HashSet Tags { get; set; } = []; + public List
Addresses { get; set; } = []; + public Dictionary Metadata { get; set; } = []; +} + +[ForyObject] +public sealed class Node +{ + public int Value { get; set; } + public Node? Next { get; set; } +} + +[ForyObject] +public sealed class FieldOrder +{ + public string Z { get; set; } = string.Empty; + public long A { get; set; } + public short B { get; set; } + public int C { get; set; } +} + +[ForyObject] +public sealed class EncodedNumbers +{ + [ForyField(ForyFieldEncoding.Fixed)] + public uint U32Fixed { get; set; } + + [ForyField(ForyFieldEncoding.Tagged)] + public ulong U64Tagged { get; set; } +} + +[ForyObject] +public sealed class OneStringField +{ + public string? F1 { get; set; } +} + +[ForyObject] +public sealed class TwoStringField +{ + public string F1 { get; set; } = string.Empty; + public string F2 { get; set; } = string.Empty; +} + +[ForyObject] +public sealed class StructWithEnum +{ + public string Name { get; set; } = string.Empty; + public TestColor Color { get; set; } + public int Value { get; set; } +} + +[ForyObject] +public sealed class StructWithNullableMap +{ + public ForyMap Data { get; set; } = new(); +} + +[ForyObject] +public sealed class StructWithUnion2 +{ + public Union2 Union { get; set; } = Union2.OfT1(string.Empty); +} + +public sealed class ForyRuntimeTests +{ + [Fact] + public void PrimitiveRoundTrip() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + + Assert.True(fory.Deserialize(fory.Serialize(true))); + Assert.Equal(-123_456, fory.Deserialize(fory.Serialize(-123_456))); + Assert.Equal(9_223_372_036_854_775_000L, fory.Deserialize(fory.Serialize(9_223_372_036_854_775_000L))); + Assert.Equal(123_456u, fory.Deserialize(fory.Serialize(123_456u))); + Assert.Equal(9_223_372_036_854_775_000UL, fory.Deserialize(fory.Serialize(9_223_372_036_854_775_000UL))); + Assert.Equal(3.25f, fory.Deserialize(fory.Serialize(3.25f))); + Assert.Equal(3.1415926, fory.Deserialize(fory.Serialize(3.1415926))); + Assert.Equal("hello_fory", fory.Deserialize(fory.Serialize("hello_fory"))); + + byte[] binary = [0x01, 0x02, 0x03, 0xFF]; + Assert.Equal(binary, fory.Deserialize(fory.Serialize(binary))); + } + + [Fact] + public void OptionalRoundTrip() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + + string? present = "present"; + string? absent = null; + Assert.Equal("present", fory.Deserialize(fory.Serialize(present))); + Assert.Null(fory.Deserialize(fory.Serialize(absent))); + } + + [Fact] + public void CollectionsRoundTrip() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + + List list = ["a", null, "b"]; + Assert.Equal(list, fory.Deserialize>(fory.Serialize(list))); + + int[] intArray = [1, 2, 3, 4]; + Assert.Equal(intArray, fory.Deserialize(fory.Serialize(intArray))); + + byte[] bytes = [1, 2, 3, 250]; + Assert.Equal(bytes, fory.Deserialize(fory.Serialize(bytes))); + + HashSet set = [1, 5, 8]; + Assert.Equal(set, fory.Deserialize>(fory.Serialize(set))); + + Dictionary map = new() { [1] = 100, [2] = null, [3] = -7 }; + Dictionary decoded = fory.Deserialize>(fory.Serialize(map)); + Assert.Equal(map.Count, decoded.Count); + foreach ((sbyte key, int? value) in map) + { + Assert.Equal(value, decoded[key]); + } + } + + [Fact] + public void StreamDeserializeConsumesSingleFrame() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + + byte[] p1 = fory.Serialize(11); + byte[] p2 = fory.Serialize(22); + byte[] joined = new byte[p1.Length + p2.Length]; + Buffer.BlockCopy(p1, 0, joined, 0, p1.Length); + Buffer.BlockCopy(p2, 0, joined, p1.Length, p2.Length); + + ReadOnlySequence sequence = new(joined); + int first = fory.Deserialize(ref sequence); + int second = fory.Deserialize(ref sequence); + + Assert.Equal(11, first); + Assert.Equal(22, second); + Assert.Equal(0, sequence.Length); + } + + [Fact] + public void StreamDeserializeObjectConsumesSingleFrame() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + + byte[] p1 = fory.SerializeObject("first"); + byte[] p2 = fory.SerializeObject(99); + byte[] joined = new byte[p1.Length + p2.Length]; + Buffer.BlockCopy(p1, 0, joined, 0, p1.Length); + Buffer.BlockCopy(p2, 0, joined, p1.Length, p2.Length); + + ReadOnlySequence sequence = new(joined); + object? first = fory.DeserializeObject(ref sequence); + object? second = fory.DeserializeObject(ref sequence); + + Assert.Equal("first", first); + Assert.Equal(99, second); + Assert.Equal(0, sequence.Length); + } + + [Fact] + public void MacroStructRoundTrip() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + fory.Register
(100); + fory.Register(101); + + Person person = new() + { + Id = 42, + Name = "Alice", + Nickname = null, + Scores = [10, 20, 30], + Tags = ["swift", "xlang"], + Addresses = [new Address { Street = "Main", Zip = 94107 }], + Metadata = new Dictionary { [1] = 100, [2] = null }, + }; + + Person decoded = fory.Deserialize(fory.Serialize(person)); + Assert.Equal(person.Id, decoded.Id); + Assert.Equal(person.Name, decoded.Name); + Assert.Equal(person.Nickname, decoded.Nickname); + Assert.Equal(person.Scores, decoded.Scores); + Assert.Equal(person.Tags, decoded.Tags); + Assert.Single(decoded.Addresses); + Assert.Equal(person.Addresses[0].Street, decoded.Addresses[0].Street); + Assert.Equal(person.Addresses[0].Zip, decoded.Addresses[0].Zip); + Assert.Equal(person.Metadata.Count, decoded.Metadata.Count); + foreach ((sbyte key, int? value) in person.Metadata) + { + Assert.Equal(value, decoded.Metadata[key]); + } + } + + [Fact] + public void MacroClassReferenceTracking() + { + ForyRuntime fory = ForyRuntime.Builder().TrackRef(true).Build(); + fory.Register(200); + + Node node = new() { Value = 7 }; + node.Next = node; + + Node decoded = fory.Deserialize(fory.Serialize(node)); + Assert.Equal(7, decoded.Value); + Assert.Same(decoded, decoded.Next); + } + + [Fact] + public void ForyMapSupportsNullKeyRoundTrip() + { + ForyRuntime fory = ForyRuntime.Builder().Compatible(true).Build(); + + ForyMap map = new(); + map.Add("k1", "v1"); + map.Add(null, "v2"); + map.Add("k3", null); + map.Add("k4", "v4"); + + ForyMap decoded = fory.Deserialize>(fory.Serialize(map)); + Assert.True(decoded.HasNullKey); + Assert.Equal("v2", decoded.NullKeyValue); + Assert.True(decoded.TryGetValue("k1", out string? v1)); + Assert.Equal("v1", v1); + Assert.True(decoded.TryGetValue("k3", out string? v3)); + Assert.Null(v3); + } + + [Fact] + public void StructWithNullableMapRoundTrip() + { + ForyRuntime fory = ForyRuntime.Builder().Compatible(true).Build(); + fory.Register(202); + + StructWithNullableMap value = new(); + value.Data.Add("key1", "value1"); + value.Data.Add(null, "value2"); + value.Data.Add("key3", null); + + StructWithNullableMap decoded = fory.Deserialize(fory.Serialize(value)); + Assert.True(decoded.Data.HasNullKey); + Assert.Equal("value2", decoded.Data.NullKeyValue); + Assert.True(decoded.Data.TryGetValue("key1", out string? key1)); + Assert.Equal("value1", key1); + Assert.True(decoded.Data.TryGetValue("key3", out string? key3)); + Assert.Null(key3); + } + + [Fact] + public void MacroFieldOrderFollowsForyRules() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + fory.Register(300); + + FieldOrder value = new() { Z = "tail", A = 123_456_789, B = 17, C = 99 }; + byte[] data = fory.Serialize(value); + + ByteReader reader = new(data); + _ = fory.ReadHead(reader); + _ = reader.ReadInt8(); + _ = reader.ReadVarUInt32(); + _ = reader.ReadVarUInt32(); + _ = reader.ReadInt32(); + + short first = reader.ReadInt16(); + long second = reader.ReadVarInt64(); + int third = reader.ReadVarInt32(); + ReadContext tailContext = new(reader, new TypeResolver(), false, false); + string fourth = StringSerializer.Instance.ReadData(ref tailContext); + + Assert.Equal(value.B, first); + Assert.Equal(value.A, second); + Assert.Equal(value.C, third); + Assert.Equal(value.Z, fourth); + } + + [Fact] + public void MacroFieldEncodingOverridesForUnsignedTypes() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + fory.Register(301); + + EncodedNumbers value = new() + { + U32Fixed = 0x11223344u, + U64Tagged = (ulong)int.MaxValue + 99UL, + }; + + EncodedNumbers decoded = fory.Deserialize(fory.Serialize(value)); + Assert.Equal(value.U32Fixed, decoded.U32Fixed); + Assert.Equal(value.U64Tagged, decoded.U64Tagged); + } + + [Fact] + public void CompatibleSchemaEvolutionRoundTrip() + { + ForyRuntime writer = ForyRuntime.Builder().Compatible(true).Build(); + writer.Register(200); + + ForyRuntime reader = ForyRuntime.Builder().Compatible(true).Build(); + reader.Register(200); + + OneStringField source = new() { F1 = "hello" }; + byte[] payload = writer.Serialize(source); + TwoStringField evolved = reader.Deserialize(payload); + + Assert.Equal("hello", evolved.F1); + Assert.Equal(string.Empty, evolved.F2); + } + + [Fact] + public void SchemaVersionMismatchThrows() + { + ForyRuntime writer = ForyRuntime.Builder().Compatible(false).Build(); + writer.Register(200); + + ForyRuntime reader = ForyRuntime.Builder().Compatible(false).Build(); + reader.Register(200); + + byte[] payload = writer.Serialize(new OneStringField { F1 = "hello" }); + Assert.Throws(() => { _ = reader.Deserialize(payload); }); + } + + [Fact] + public void UnionFieldRoundTripCompatible() + { + ForyRuntime fory = ForyRuntime.Builder().Compatible(true).Build(); + fory.Register(301); + + StructWithUnion2 first = new() { Union = Union2.OfT1("hello") }; + StructWithUnion2 second = new() { Union = Union2.OfT2(42L) }; + + StructWithUnion2 firstDecoded = fory.Deserialize(fory.Serialize(first)); + StructWithUnion2 secondDecoded = fory.Deserialize(fory.Serialize(second)); + + Assert.Equal(0, firstDecoded.Union.Index); + Assert.Equal("hello", firstDecoded.Union.GetT1()); + Assert.Equal(1, secondDecoded.Union.Index); + Assert.Equal(42L, secondDecoded.Union.GetT2()); + } + + [Fact] + public void EnumRoundTrip() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + fory.Register(100); + fory.Register(101); + + StructWithEnum value = new() { Name = "enum", Color = TestColor.Blue, Value = 42 }; + StructWithEnum decoded = fory.Deserialize(fory.Serialize(value)); + Assert.Equal(value.Name, decoded.Name); + Assert.Equal(value.Color, decoded.Color); + Assert.Equal(value.Value, decoded.Value); + } +} diff --git a/csharp/tests/Fory.Tests/GlobalUsings.cs b/csharp/tests/Fory.Tests/GlobalUsings.cs new file mode 100644 index 0000000000..8c927eb747 --- /dev/null +++ b/csharp/tests/Fory.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/csharp/tests/Fory.XlangPeer/Fory.XlangPeer.csproj b/csharp/tests/Fory.XlangPeer/Fory.XlangPeer.csproj new file mode 100644 index 0000000000..8c92630101 --- /dev/null +++ b/csharp/tests/Fory.XlangPeer/Fory.XlangPeer.csproj @@ -0,0 +1,14 @@ + + + Exe + net8.0 + 12.0 + enable + enable + + + + + + + diff --git a/csharp/tests/Fory.XlangPeer/Program.cs b/csharp/tests/Fory.XlangPeer/Program.cs new file mode 100644 index 0000000000..18668847cc --- /dev/null +++ b/csharp/tests/Fory.XlangPeer/Program.cs @@ -0,0 +1,1449 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Buffers; +using System.Buffers.Binary; +using System.Text; +using Apache.Fory; +using ForyRuntime = Apache.Fory.Fory; + +namespace Apache.Fory.XlangPeer; + +internal static class Program +{ + private const string DataFileEnv = "DATA_FILE"; + + private static readonly string[] StringSamples = + [ + "ab", + "Rust123", + "Çüéâäàåçêëèïî", + "こんにちは", + "Привет", + "𝄞🎵🎶", + "Hello, 世界", + ]; + + private static readonly int[] VarInt32Values = + [ + int.MinValue, + int.MinValue + 1, + -1_000_000, + -1000, + -128, + -1, + 0, + 1, + 127, + 128, + 16_383, + 16_384, + 2_097_151, + 2_097_152, + 268_435_455, + 268_435_456, + int.MaxValue - 1, + int.MaxValue, + ]; + + private static readonly uint[] VarUInt32Values = + [ + 0, + 1, + 127, + 128, + 16_383, + 16_384, + 2_097_151, + 2_097_152, + 268_435_455, + 268_435_456, + 2_147_483_646, + 2_147_483_647, + ]; + + private static readonly ulong[] VarUInt64Values = + [ + 0UL, + 1UL, + 127UL, + 128UL, + 16_383UL, + 16_384UL, + 2_097_151UL, + 2_097_152UL, + 268_435_455UL, + 268_435_456UL, + 34_359_738_367UL, + 34_359_738_368UL, + 4_398_046_511_103UL, + 4_398_046_511_104UL, + 562_949_953_421_311UL, + 562_949_953_421_312UL, + 72_057_594_037_927_935UL, + 72_057_594_037_927_936UL, + long.MaxValue, + ]; + + private static readonly long[] VarInt64Values = + [ + long.MinValue, + long.MinValue + 1, + -1_000_000_000_000L, + -1_000_000L, + -1000L, + -128L, + -1L, + 0L, + 1L, + 127L, + 1000L, + 1_000_000L, + 1_000_000_000_000L, + long.MaxValue - 1, + long.MaxValue, + ]; + + private static int Main(string[] args) + { + try + { + string caseName = ParseCaseName(args); + string dataFile = RequireDataFile(); + byte[] input = File.ReadAllBytes(dataFile); + byte[] output = ExecuteCase(caseName, input); + File.WriteAllBytes(dataFile, output); + Console.WriteLine($"case {caseName} passed"); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"xlang peer failed: {ex}"); + return 1; + } + } + + private static string ParseCaseName(string[] args) + { + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "--case" && i + 1 < args.Length) + { + return args[i + 1]; + } + } + + if (args.Length == 1) + { + return args[0]; + } + + throw new InvalidOperationException("Usage: Fory.XlangPeer --case "); + } + + private static string RequireDataFile() + { + string? dataFile = Environment.GetEnvironmentVariable(DataFileEnv); + if (string.IsNullOrWhiteSpace(dataFile)) + { + throw new InvalidOperationException($"{DataFileEnv} environment variable is required"); + } + + return dataFile; + } + + private static byte[] ExecuteCase(string caseName, byte[] input) + { + return caseName switch + { + "test_buffer" => CaseBuffer(input), + "test_buffer_var" => CaseBufferVar(input), + "test_murmurhash3" => CaseMurmurHash3(input), + "test_string_serializer" => CaseStringSerializer(input), + "test_cross_language_serializer" => CaseCrossLanguageSerializer(input), + "test_simple_struct" => CaseSimpleStruct(input), + "test_named_simple_struct" => CaseNamedSimpleStruct(input), + "test_list" => CaseList(input), + "test_map" => CaseMap(input), + "test_integer" => CaseInteger(input), + "test_item" => CaseItem(input), + "test_color" => CaseColor(input), + "test_union_xlang" => CaseUnionXlang(input), + "test_struct_with_list" => CaseStructWithList(input), + "test_struct_with_map" => CaseStructWithMap(input), + "test_skip_id_custom" => CaseSkipIdCustom(input), + "test_skip_name_custom" => CaseSkipNameCustom(input), + "test_consistent_named" => CaseConsistentNamed(input), + "test_struct_version_check" => CaseStructVersionCheck(input), + "test_polymorphic_list" => CasePolymorphicList(input), + "test_polymorphic_map" => CasePolymorphicMap(input), + "test_one_field_struct_compatible" => CaseOneFieldStructCompatible(input), + "test_one_field_struct_schema" => CaseOneFieldStructSchema(input), + "test_one_string_field_schema" => CaseOneStringFieldSchema(input), + "test_one_string_field_compatible" => CaseOneStringFieldCompatible(input), + "test_two_string_field_compatible" => CaseTwoStringFieldCompatible(input), + "test_schema_evolution_compatible" => CaseSchemaEvolutionCompatible(input), + "test_schema_evolution_compatible_reverse" => CaseSchemaEvolutionCompatibleReverse(input), + "test_one_enum_field_schema" => CaseOneEnumFieldSchema(input), + "test_one_enum_field_compatible" => CaseOneEnumFieldCompatible(input), + "test_two_enum_field_compatible" => CaseTwoEnumFieldCompatible(input), + "test_enum_schema_evolution_compatible" => CaseEnumSchemaEvolutionCompatible(input), + "test_enum_schema_evolution_compatible_reverse" => CaseEnumSchemaEvolutionCompatibleReverse(input), + "test_nullable_field_schema_consistent_not_null" => CaseNullableFieldSchemaConsistentNotNull(input), + "test_nullable_field_schema_consistent_null" => CaseNullableFieldSchemaConsistentNull(input), + "test_nullable_field_compatible_not_null" => CaseNullableFieldCompatibleNotNull(input), + "test_nullable_field_compatible_null" => CaseNullableFieldCompatibleNull(input), + "test_ref_schema_consistent" => CaseRefSchemaConsistent(input), + "test_ref_compatible" => CaseRefCompatible(input), + "test_collection_element_ref_override" => CaseCollectionElementRefOverride(input), + "test_circular_ref_schema_consistent" => CaseCircularRefSchemaConsistent(input), + "test_circular_ref_compatible" => CaseCircularRefCompatible(input), + "test_unsigned_schema_consistent_simple" => CaseUnsignedSchemaConsistentSimple(input), + "test_unsigned_schema_consistent" => CaseUnsignedSchemaConsistent(input), + "test_unsigned_schema_compatible" => CaseUnsignedSchemaCompatible(input), + _ => throw new InvalidOperationException($"unknown test case {caseName}"), + }; + } + + private static byte[] CaseBuffer(byte[] input) + { + ByteReader reader = new(input); + Ensure(reader.ReadUInt8() == 1, "bool mismatch"); + Ensure(reader.ReadInt8() == sbyte.MaxValue, "byte mismatch"); + Ensure(reader.ReadInt16() == short.MaxValue, "int16 mismatch"); + Ensure(reader.ReadInt32() == int.MaxValue, "int32 mismatch"); + Ensure(reader.ReadInt64() == long.MaxValue, "int64 mismatch"); + Ensure(Math.Abs(reader.ReadFloat32() - (-1.1f)) < 0.0001f, "float32 mismatch"); + Ensure(Math.Abs(reader.ReadFloat64() - (-1.1d)) < 0.000001d, "float64 mismatch"); + Ensure(reader.ReadVarUInt32() == 100, "varuint32 mismatch"); + int size = reader.ReadInt32(); + byte[] payload = reader.ReadBytes(size); + Ensure(payload.SequenceEqual("ab"u8.ToArray()), "binary mismatch"); + Ensure(reader.Remaining == 0, "buffer should be fully consumed"); + + ByteWriter writer = new(); + writer.WriteUInt8(1); + writer.WriteInt8(sbyte.MaxValue); + writer.WriteInt16(short.MaxValue); + writer.WriteInt32(int.MaxValue); + writer.WriteInt64(long.MaxValue); + writer.WriteFloat32(-1.1f); + writer.WriteFloat64(-1.1d); + writer.WriteVarUInt32(100); + writer.WriteInt32(2); + writer.WriteBytes("ab"u8); + return writer.ToArray(); + } + + private static byte[] CaseBufferVar(byte[] input) + { + ByteReader reader = new(input); + foreach (int expected in VarInt32Values) + { + Ensure(reader.ReadVarInt32() == expected, $"varint32 mismatch {expected}"); + } + + foreach (uint expected in VarUInt32Values) + { + Ensure(reader.ReadVarUInt32() == expected, $"varuint32 mismatch {expected}"); + } + + foreach (ulong expected in VarUInt64Values) + { + Ensure(reader.ReadVarUInt64() == expected, $"varuint64 mismatch {expected}"); + } + + foreach (long expected in VarInt64Values) + { + Ensure(reader.ReadVarInt64() == expected, $"varint64 mismatch {expected}"); + } + + Ensure(reader.Remaining == 0, "buffer var should be fully consumed"); + + ByteWriter writer = new(); + foreach (int value in VarInt32Values) + { + writer.WriteVarInt32(value); + } + + foreach (uint value in VarUInt32Values) + { + writer.WriteVarUInt32(value); + } + + foreach (ulong value in VarUInt64Values) + { + writer.WriteVarUInt64(value); + } + + foreach (long value in VarInt64Values) + { + writer.WriteVarInt64(value); + } + + return writer.ToArray(); + } + + private static byte[] CaseMurmurHash3(byte[] input) + { + if (input.Length == 32) + { + (ulong h1a, ulong h1b) = PeerMurmurHash3.X64_128([1, 2, 8], 47); + (ulong h2a, ulong h2b) = PeerMurmurHash3.X64_128(Encoding.UTF8.GetBytes("01234567890123456789"), 47); + ByteWriter writer = new(); + writer.WriteInt64(unchecked((long)h1a)); + writer.WriteInt64(unchecked((long)h1b)); + writer.WriteInt64(unchecked((long)h2a)); + writer.WriteInt64(unchecked((long)h2b)); + return writer.ToArray(); + } + + if (input.Length == 16) + { + ByteReader reader = new(input); + long h1 = reader.ReadInt64(); + long h2 = reader.ReadInt64(); + (ulong expected1, ulong expected2) = PeerMurmurHash3.X64_128([1, 2, 8], 47); + Ensure(h1 == unchecked((long)expected1), "murmur hash h1 mismatch"); + Ensure(h2 == unchecked((long)expected2), "murmur hash h2 mismatch"); + return []; + } + + throw new InvalidOperationException($"unexpected murmur hash input length {input.Length}"); + } + + private static byte[] CaseStringSerializer(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + ReadOnlySequence sequence = new(input); + foreach (string expected in StringSamples) + { + string value = fory.Deserialize(ref sequence); + Ensure(value == expected, "string value mismatch"); + } + + EnsureConsumed(sequence, nameof(CaseStringSerializer)); + List output = []; + foreach (string sample in StringSamples) + { + Append(output, fory.SerializeObject(sample)); + } + + return output.ToArray(); + } + + private static byte[] CaseCrossLanguageSerializer(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(101); + ReadOnlySequence sequence = new(input); + + bool b1 = fory.Deserialize(ref sequence); + bool b2 = fory.Deserialize(ref sequence); + int i32 = fory.Deserialize(ref sequence); + sbyte i8a = fory.Deserialize(ref sequence); + sbyte i8b = fory.Deserialize(ref sequence); + short i16a = fory.Deserialize(ref sequence); + short i16b = fory.Deserialize(ref sequence); + int i32a = fory.Deserialize(ref sequence); + int i32b = fory.Deserialize(ref sequence); + long i64a = fory.Deserialize(ref sequence); + long i64b = fory.Deserialize(ref sequence); + float f32 = fory.Deserialize(ref sequence); + double f64 = fory.Deserialize(ref sequence); + string str = fory.Deserialize(ref sequence); + DateOnly day = fory.Deserialize(ref sequence); + DateTimeOffset timestamp = fory.Deserialize(ref sequence); + bool[] bools = fory.Deserialize(ref sequence); + byte[] bytes = fory.Deserialize(ref sequence); + short[] int16s = fory.Deserialize(ref sequence); + int[] int32s = fory.Deserialize(ref sequence); + long[] int64s = fory.Deserialize(ref sequence); + float[] floats = fory.Deserialize(ref sequence); + double[] doubles = fory.Deserialize(ref sequence); + List list = fory.Deserialize>(ref sequence); + HashSet set = fory.Deserialize>(ref sequence); + Dictionary map = fory.Deserialize>(ref sequence); + Color color = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseCrossLanguageSerializer)); + + Ensure(b1, "bool1 mismatch"); + Ensure(!b2, "bool2 mismatch"); + Ensure(i32 == -1, "int mismatch"); + Ensure(str == "str", "string mismatch"); + Ensure(day == new DateOnly(2021, 11, 23), "date mismatch"); + Ensure(timestamp.ToUnixTimeSeconds() == 100, "timestamp mismatch"); + Ensure(color == Color.White, "color mismatch"); + + List output = []; + Append(output, fory.SerializeObject(b1)); + Append(output, fory.SerializeObject(b2)); + Append(output, fory.SerializeObject(i32)); + Append(output, fory.SerializeObject(i8a)); + Append(output, fory.SerializeObject(i8b)); + Append(output, fory.SerializeObject(i16a)); + Append(output, fory.SerializeObject(i16b)); + Append(output, fory.SerializeObject(i32a)); + Append(output, fory.SerializeObject(i32b)); + Append(output, fory.SerializeObject(i64a)); + Append(output, fory.SerializeObject(i64b)); + Append(output, fory.SerializeObject(f32)); + Append(output, fory.SerializeObject(f64)); + Append(output, fory.SerializeObject(str)); + Append(output, fory.SerializeObject(day)); + Append(output, fory.SerializeObject(timestamp)); + Append(output, fory.SerializeObject(bools)); + Append(output, fory.SerializeObject(bytes)); + Append(output, fory.SerializeObject(int16s)); + Append(output, fory.SerializeObject(int32s)); + Append(output, fory.SerializeObject(int64s)); + Append(output, fory.SerializeObject(floats)); + Append(output, fory.SerializeObject(doubles)); + Append(output, fory.SerializeObject(list)); + Append(output, fory.SerializeObject(set)); + Append(output, fory.SerializeObject(map)); + Append(output, fory.SerializeObject(color)); + return output.ToArray(); + } + + private static byte[] CaseSimpleStruct(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + RegisterSimpleById(fory); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseNamedSimpleStruct(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + RegisterSimpleByName(fory); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseList(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(102); + ReadOnlySequence sequence = new(input); + List strList = fory.Deserialize>(ref sequence); + List strList2 = fory.Deserialize>(ref sequence); + List itemList = fory.Deserialize>(ref sequence); + List itemList2 = fory.Deserialize>(ref sequence); + EnsureConsumed(sequence, nameof(CaseList)); + + List output = []; + Append(output, fory.SerializeObject(strList)); + Append(output, fory.SerializeObject(strList2)); + Append(output, fory.SerializeObject(itemList)); + Append(output, fory.SerializeObject(itemList2)); + return output.ToArray(); + } + + private static byte[] CaseMap(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(102); + ReadOnlySequence sequence = new(input); + ForyMap strMap = fory.Deserialize>(ref sequence); + ForyMap itemMap = fory.Deserialize>(ref sequence); + EnsureConsumed(sequence, nameof(CaseMap)); + + List output = []; + Append(output, fory.SerializeObject(strMap)); + Append(output, fory.SerializeObject(itemMap)); + return output.ToArray(); + } + + private static byte[] CaseInteger(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(101); + ReadOnlySequence sequence = new(input); + Item1 obj = fory.Deserialize(ref sequence); + int f1 = fory.Deserialize(ref sequence); + int f2 = fory.Deserialize(ref sequence); + int f3 = fory.Deserialize(ref sequence); + int f4 = fory.Deserialize(ref sequence); + int f5 = fory.Deserialize(ref sequence); + int f6 = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseInteger)); + + Ensure(obj.F1 == 1 && obj.F2 == 2, "item1 primitive fields mismatch"); + Ensure(obj.F3 == 3 && obj.F4 == 4 && obj.F5 == 0 && obj.F6 == 0, "item1 boxed fields mismatch"); + + List output = []; + Append(output, fory.SerializeObject(obj)); + Append(output, fory.SerializeObject(f1)); + Append(output, fory.SerializeObject(f2)); + Append(output, fory.SerializeObject(f3)); + Append(output, fory.SerializeObject(f4)); + Append(output, fory.SerializeObject(f5)); + Append(output, fory.SerializeObject(f6)); + return output.ToArray(); + } + + private static byte[] CaseItem(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(102); + ReadOnlySequence sequence = new(input); + Item i1 = fory.Deserialize(ref sequence); + Item i2 = fory.Deserialize(ref sequence); + Item i3 = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseItem)); + + List output = []; + Append(output, fory.SerializeObject(i1)); + Append(output, fory.SerializeObject(i2)); + Append(output, fory.SerializeObject(i3)); + return output.ToArray(); + } + + private static byte[] CaseColor(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(101); + ReadOnlySequence sequence = new(input); + Color c1 = fory.Deserialize(ref sequence); + Color c2 = fory.Deserialize(ref sequence); + Color c3 = fory.Deserialize(ref sequence); + Color c4 = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseColor)); + + List output = []; + Append(output, fory.SerializeObject(c1)); + Append(output, fory.SerializeObject(c2)); + Append(output, fory.SerializeObject(c3)); + Append(output, fory.SerializeObject(c4)); + return output.ToArray(); + } + + private static byte[] CaseStructWithList(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(201); + ReadOnlySequence sequence = new(input); + StructWithList s1 = fory.Deserialize(ref sequence); + StructWithList s2 = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseStructWithList)); + + List output = []; + Append(output, fory.SerializeObject(s1)); + Append(output, fory.SerializeObject(s2)); + return output.ToArray(); + } + + private static byte[] CaseStructWithMap(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(202); + ReadOnlySequence sequence = new(input); + StructWithMap s1 = fory.Deserialize(ref sequence); + StructWithMap s2 = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseStructWithMap)); + + List output = []; + Append(output, fory.SerializeObject(s1)); + Append(output, fory.SerializeObject(s2)); + return output.ToArray(); + } + + private static byte[] CaseUnionXlang(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(301); + + ReadOnlySequence sequence = new(input); + StructWithUnion2 first = fory.Deserialize(ref sequence); + StructWithUnion2 second = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseUnionXlang)); + + Ensure(first.Union.Index == 0, "union case index mismatch for first value"); + Ensure(first.Union.Value is string firstValue && firstValue == "hello", "union case value mismatch for first value"); + Ensure(second.Union.Index == 1, "union case index mismatch for second value"); + Ensure(second.Union.Value is long secondValue && secondValue == 42L, "union case value mismatch for second value"); + + List output = []; + Append(output, fory.SerializeObject(first)); + Append(output, fory.SerializeObject(second)); + return output.ToArray(); + } + + private static byte[] CaseSkipIdCustom(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(101); + fory.Register(102); + fory.Register(103); + fory.Register(104); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseSkipNameCustom(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register("color"); + fory.Register("my_struct"); + fory.Register(string.Empty, "my_ext"); + fory.Register("my_wrapper"); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseConsistentNamed(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false, checkStructVersion: true); + fory.Register("color"); + fory.Register("my_struct"); + fory.Register(string.Empty, "my_ext"); + + ReadOnlySequence sequence = new(input); + List output = []; + for (int i = 0; i < 3; i++) + { + Color color = fory.Deserialize(ref sequence); + Append(output, fory.SerializeObject(color)); + } + + for (int i = 0; i < 3; i++) + { + MyStruct myStruct = fory.Deserialize(ref sequence); + Append(output, fory.SerializeObject(myStruct)); + } + + for (int i = 0; i < 3; i++) + { + MyExt myExt = fory.Deserialize(ref sequence); + Append(output, fory.SerializeObject(myExt)); + } + + EnsureConsumed(sequence, nameof(CaseConsistentNamed)); + return output.ToArray(); + } + + private static byte[] CaseStructVersionCheck(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false, checkStructVersion: true); + fory.Register(201); + return RoundTripSingle(input, fory); + } + + private static byte[] CasePolymorphicList(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(302); + fory.Register(303); + fory.Register(304); + + ReadOnlySequence sequence = new(input); + List animals = fory.Deserialize>(ref sequence); + AnimalListHolder holder = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CasePolymorphicList)); + + List output = []; + Append(output, fory.SerializeObject(animals)); + Append(output, fory.SerializeObject(holder)); + return output.ToArray(); + } + + private static byte[] CasePolymorphicMap(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(302); + fory.Register(303); + fory.Register(305); + + ReadOnlySequence sequence = new(input); + Dictionary map = fory.Deserialize>(ref sequence); + AnimalMapHolder holder = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CasePolymorphicMap)); + + List output = []; + Append(output, fory.SerializeObject(map)); + Append(output, fory.SerializeObject(holder)); + return output.ToArray(); + } + + private static byte[] CaseOneFieldStructCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(200); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseOneFieldStructSchema(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false); + fory.Register(200); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseOneStringFieldSchema(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false); + fory.Register(200); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseOneStringFieldCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(200); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseTwoStringFieldCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(201); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseSchemaEvolutionCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(200); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseSchemaEvolutionCompatibleReverse(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(200); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseOneEnumFieldSchema(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false); + fory.Register(210); + fory.Register(211); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseOneEnumFieldCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(210); + fory.Register(211); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseTwoEnumFieldCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(210); + fory.Register(212); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseEnumSchemaEvolutionCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(210); + fory.Register(211); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseEnumSchemaEvolutionCompatibleReverse(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(210); + fory.Register(211); + + ReadOnlySequence sequence = new(input); + TwoEnumFieldStruct value = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseEnumSchemaEvolutionCompatibleReverse)); + Ensure(value.F1 == TestEnum.ValueC, "enum schema evolution reverse F1 mismatch"); + Ensure(value.F2 == TestEnum.ValueA, "enum schema evolution reverse F2 default mismatch"); + return fory.SerializeObject(value); + } + + private static byte[] CaseNullableFieldSchemaConsistentNotNull(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false); + fory.Register(401); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseNullableFieldSchemaConsistentNull(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false); + fory.Register(401); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseNullableFieldCompatibleNotNull(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(402); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseNullableFieldCompatibleNull(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(402); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseRefSchemaConsistent(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false, trackRef: true); + fory.Register(501); + fory.Register(502); + + ReadOnlySequence sequence = new(input); + RefOuterSchemaConsistent outer = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseRefSchemaConsistent)); + Ensure(ReferenceEquals(outer.Inner1, outer.Inner2), "reference tracking mismatch"); + return fory.SerializeObject(outer); + } + + private static byte[] CaseRefCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true, trackRef: true); + fory.Register(503); + fory.Register(504); + + ReadOnlySequence sequence = new(input); + RefOuterCompatible outer = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseRefCompatible)); + Ensure(ReferenceEquals(outer.Inner1, outer.Inner2), "reference tracking mismatch"); + return fory.SerializeObject(outer); + } + + private static byte[] CaseCollectionElementRefOverride(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false, trackRef: true); + fory.Register(701); + fory.Register(702); + + ReadOnlySequence sequence = new(input); + RefOverrideContainer container = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseCollectionElementRefOverride)); + + if (container.ListField.Count > 0) + { + RefOverrideElement? shared = container.ListField[0]; + if (shared is not null) + { + if (container.ListField.Count > 1) + { + container.ListField[1] = shared; + } + + if (container.MapField.ContainsKey("k1")) + { + container.MapField["k1"] = shared; + } + + if (container.MapField.ContainsKey("k2")) + { + container.MapField["k2"] = shared; + } + } + } + + return fory.SerializeObject(container); + } + + private static byte[] CaseCircularRefSchemaConsistent(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false, trackRef: true); + fory.Register(601); + + ReadOnlySequence sequence = new(input); + CircularRefStruct value = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseCircularRefSchemaConsistent)); + Ensure(ReferenceEquals(value, value.SelfRef), "circular ref mismatch"); + return fory.SerializeObject(value); + } + + private static byte[] CaseCircularRefCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true, trackRef: true); + fory.Register(602); + + ReadOnlySequence sequence = new(input); + CircularRefStruct value = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, nameof(CaseCircularRefCompatible)); + Ensure(ReferenceEquals(value, value.SelfRef), "circular ref mismatch"); + return fory.SerializeObject(value); + } + + private static byte[] CaseUnsignedSchemaConsistentSimple(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false); + fory.Register(1); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseUnsignedSchemaConsistent(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: false); + fory.Register(501); + return RoundTripSingle(input, fory); + } + + private static byte[] CaseUnsignedSchemaCompatible(byte[] input) + { + ForyRuntime fory = BuildFory(compatible: true); + fory.Register(502); + return RoundTripSingle(input, fory); + } + + private static byte[] RoundTripSingle(byte[] input, ForyRuntime fory) + { + ReadOnlySequence sequence = new(input); + T value = fory.Deserialize(ref sequence); + EnsureConsumed(sequence, typeof(T).Name); + return fory.SerializeObject(value); + } + + private static void RegisterSimpleById(ForyRuntime fory) + { + fory.Register(101); + fory.Register(102); + fory.Register(103); + } + + private static void RegisterSimpleByName(ForyRuntime fory) + { + fory.Register("demo", "color"); + fory.Register("demo", "item"); + fory.Register("demo", "simple_struct"); + } + + private static ForyRuntime BuildFory(bool compatible, bool trackRef = false, bool checkStructVersion = false) + { + return ForyRuntime.Builder() + .Compatible(compatible) + .TrackRef(trackRef) + .CheckStructVersion(checkStructVersion) + .Build(); + } + + private static void Append(List target, byte[] payload) + { + target.AddRange(payload); + } + + private static void EnsureConsumed(ReadOnlySequence sequence, string caseName) + { + Ensure(sequence.Length == 0, $"case {caseName} did not consume full payload"); + } + + private static void Ensure(bool condition, string message) + { + if (!condition) + { + throw new InvalidOperationException(message); + } + } +} + +[ForyObject] +public enum Color +{ + Green, + Red, + Blue, + White, +} + +[ForyObject] +public sealed class Item +{ + public string Name { get; set; } = string.Empty; +} + +[ForyObject] +public sealed class SimpleStruct +{ + public Dictionary F1 { get; set; } = []; + public int F2 { get; set; } + public Item F3 { get; set; } = new(); + public string F4 { get; set; } = string.Empty; + public Color F5 { get; set; } + public List F6 { get; set; } = []; + public int F7 { get; set; } + public int F8 { get; set; } + public int Last { get; set; } +} + +[ForyObject] +public sealed class Item1 +{ + public int F1 { get; set; } + public int F2 { get; set; } + public int F3 { get; set; } + public int F4 { get; set; } + public int F5 { get; set; } + public int F6 { get; set; } +} + +[ForyObject] +public sealed class StructWithList +{ + public List Items { get; set; } = []; +} + +[ForyObject] +public sealed class StructWithMap +{ + public ForyMap Data { get; set; } = new(); +} + +[ForyObject] +public sealed class StructWithUnion2 +{ + public Union2 Union { get; set; } = Union2.OfT1(string.Empty); +} + +[ForyObject] +public sealed class MyStruct +{ + public int Id { get; set; } +} + +[ForyObject] +public sealed class MyExt +{ + public int Id { get; set; } +} + +public sealed class MyExtSerializer : Serializer +{ + public override ForyTypeId StaticTypeId => ForyTypeId.Ext; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override MyExt DefaultValue => null!; + public override bool IsNone(MyExt value) => value is null; + + public override void WriteData(ref WriteContext context, in MyExt value, bool hasGenerics) + { + _ = hasGenerics; + context.Writer.WriteVarInt32((value ?? new MyExt()).Id); + } + + public override MyExt ReadData(ref ReadContext context) + { + return new MyExt { Id = context.Reader.ReadVarInt32() }; + } +} + +[ForyObject] +public sealed class MyWrapper +{ + public Color Color { get; set; } + public MyExt MyExt { get; set; } = new(); + public MyStruct MyStruct { get; set; } = new(); +} + +[ForyObject] +public sealed class EmptyWrapper +{ +} + +[ForyObject] +public sealed class VersionCheckStruct +{ + public int F1 { get; set; } + public string? F2 { get; set; } + public double F3 { get; set; } +} + +[ForyObject] +public sealed class Dog +{ + public int Age { get; set; } + public string? Name { get; set; } +} + +[ForyObject] +public sealed class Cat +{ + public int Age { get; set; } + public int Lives { get; set; } +} + +[ForyObject] +public sealed class AnimalListHolder +{ + public List Animals { get; set; } = []; +} + +[ForyObject] +public sealed class AnimalMapHolder +{ + public Dictionary AnimalMap { get; set; } = []; +} + +[ForyObject] +public sealed class OneFieldStruct +{ + public int Value { get; set; } +} + +[ForyObject] +public sealed class OneStringFieldStruct +{ + public string? F1 { get; set; } +} + +[ForyObject] +public sealed class TwoStringFieldStruct +{ + public string F1 { get; set; } = string.Empty; + public string F2 { get; set; } = string.Empty; +} + +[ForyObject] +public enum TestEnum +{ + ValueA, + ValueB, + ValueC, +} + +[ForyObject] +public sealed class OneEnumFieldStruct +{ + public TestEnum F1 { get; set; } +} + +[ForyObject] +public sealed class TwoEnumFieldStruct +{ + public TestEnum F1 { get; set; } + public TestEnum F2 { get; set; } +} + +[ForyObject] +public sealed class NullableComprehensiveSchemaConsistent +{ + public sbyte ByteField { get; set; } + public short ShortField { get; set; } + public int IntField { get; set; } + public long LongField { get; set; } + public float FloatField { get; set; } + public double DoubleField { get; set; } + public bool BoolField { get; set; } + + public string StringField { get; set; } = string.Empty; + public List ListField { get; set; } = []; + public HashSet SetField { get; set; } = []; + public ForyMap MapField { get; set; } = new(); + + public int? NullableInt { get; set; } + public long? NullableLong { get; set; } + public float? NullableFloat { get; set; } + public double? NullableDouble { get; set; } + public bool? NullableBool { get; set; } + public string? NullableString { get; set; } + public List? NullableList { get; set; } + public HashSet? NullableSet { get; set; } + public ForyMap? NullableMap { get; set; } +} + +[ForyObject] +public sealed class NullableComprehensiveCompatible +{ + public sbyte ByteField { get; set; } + public short ShortField { get; set; } + public int IntField { get; set; } + public long LongField { get; set; } + public float FloatField { get; set; } + public double DoubleField { get; set; } + public bool BoolField { get; set; } + + public int BoxedInt { get; set; } + public long BoxedLong { get; set; } + public float BoxedFloat { get; set; } + public double BoxedDouble { get; set; } + public bool BoxedBool { get; set; } + + public string StringField { get; set; } = string.Empty; + public List ListField { get; set; } = []; + public HashSet SetField { get; set; } = []; + public ForyMap MapField { get; set; } = new(); + + public int NullableInt1 { get; set; } + public long NullableLong1 { get; set; } + public float NullableFloat1 { get; set; } + public double NullableDouble1 { get; set; } + public bool NullableBool1 { get; set; } + + public string NullableString2 { get; set; } = string.Empty; + public List NullableList2 { get; set; } = []; + public HashSet NullableSet2 { get; set; } = []; + public ForyMap NullableMap2 { get; set; } = new(); +} + +[ForyObject] +public sealed class RefInnerSchemaConsistent +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +[ForyObject] +public sealed class RefOuterSchemaConsistent +{ + public RefInnerSchemaConsistent? Inner1 { get; set; } + public RefInnerSchemaConsistent? Inner2 { get; set; } +} + +[ForyObject] +public sealed class RefInnerCompatible +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +[ForyObject] +public sealed class RefOuterCompatible +{ + public RefInnerCompatible? Inner1 { get; set; } + public RefInnerCompatible? Inner2 { get; set; } +} + +[ForyObject] +public sealed class RefOverrideElement +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +[ForyObject] +public sealed class RefOverrideContainer +{ + public List ListField { get; set; } = []; + public Dictionary MapField { get; set; } = []; +} + +[ForyObject] +public sealed class CircularRefStruct +{ + public string Name { get; set; } = string.Empty; + public CircularRefStruct? SelfRef { get; set; } +} + +[ForyObject] +public sealed class UnsignedSchemaConsistentSimple +{ + [ForyField(ForyFieldEncoding.Tagged)] + public ulong U64Tagged { get; set; } + + [ForyField(ForyFieldEncoding.Tagged)] + public ulong? U64TaggedNullable { get; set; } +} + +[ForyObject] +public sealed class UnsignedSchemaConsistent +{ + public byte U8Field { get; set; } + public ushort U16Field { get; set; } + public uint U32VarField { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public uint U32FixedField { get; set; } + + public ulong U64VarField { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public ulong U64FixedField { get; set; } + + [ForyField(ForyFieldEncoding.Tagged)] + public ulong U64TaggedField { get; set; } + + public byte? U8NullableField { get; set; } + public ushort? U16NullableField { get; set; } + public uint? U32VarNullableField { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public uint? U32FixedNullableField { get; set; } + + public ulong? U64VarNullableField { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public ulong? U64FixedNullableField { get; set; } + + [ForyField(ForyFieldEncoding.Tagged)] + public ulong? U64TaggedNullableField { get; set; } +} + +[ForyObject] +public sealed class UnsignedSchemaCompatible +{ + public byte? U8Field1 { get; set; } + public ushort? U16Field1 { get; set; } + public uint? U32VarField1 { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public uint? U32FixedField1 { get; set; } + + public ulong? U64VarField1 { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public ulong? U64FixedField1 { get; set; } + + [ForyField(ForyFieldEncoding.Tagged)] + public ulong? U64TaggedField1 { get; set; } + + public byte U8Field2 { get; set; } + public ushort U16Field2 { get; set; } + public uint U32VarField2 { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public uint U32FixedField2 { get; set; } + + public ulong U64VarField2 { get; set; } + + [ForyField(ForyFieldEncoding.Fixed)] + public ulong U64FixedField2 { get; set; } + + [ForyField(ForyFieldEncoding.Tagged)] + public ulong U64TaggedField2 { get; set; } +} + +internal static class PeerMurmurHash3 +{ + public static (ulong H1, ulong H2) X64_128(ReadOnlySpan bytes, ulong seed) + { + const ulong c1 = 0x87c37b91114253d5; + const ulong c2 = 0x4cf5ad432745937f; + + ulong h1 = seed; + ulong h2 = seed; + + int nblocks = bytes.Length / 16; + for (int i = 0; i < nblocks; i++) + { + int offset = i * 16; + ulong k1 = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(offset, 8)); + ulong k2 = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(offset + 8, 8)); + + k1 *= c1; + k1 = RotateLeft(k1, 31); + k1 *= c2; + h1 ^= k1; + + h1 = RotateLeft(h1, 27); + h1 += h2; + h1 = h1 * 5 + 0x52dce729; + + k2 *= c2; + k2 = RotateLeft(k2, 33); + k2 *= c1; + h2 ^= k2; + + h2 = RotateLeft(h2, 31); + h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + ulong tk1 = 0; + ulong tk2 = 0; + int tailStart = nblocks * 16; + ReadOnlySpan tail = bytes.Slice(tailStart); + switch (bytes.Length & 15) + { + case 15: + tk2 ^= (ulong)tail[14] << 48; + goto case 14; + case 14: + tk2 ^= (ulong)tail[13] << 40; + goto case 13; + case 13: + tk2 ^= (ulong)tail[12] << 32; + goto case 12; + case 12: + tk2 ^= (ulong)tail[11] << 24; + goto case 11; + case 11: + tk2 ^= (ulong)tail[10] << 16; + goto case 10; + case 10: + tk2 ^= (ulong)tail[9] << 8; + goto case 9; + case 9: + tk2 ^= tail[8]; + tk2 *= c2; + tk2 = RotateLeft(tk2, 33); + tk2 *= c1; + h2 ^= tk2; + goto case 8; + case 8: + tk1 ^= (ulong)tail[7] << 56; + goto case 7; + case 7: + tk1 ^= (ulong)tail[6] << 48; + goto case 6; + case 6: + tk1 ^= (ulong)tail[5] << 40; + goto case 5; + case 5: + tk1 ^= (ulong)tail[4] << 32; + goto case 4; + case 4: + tk1 ^= (ulong)tail[3] << 24; + goto case 3; + case 3: + tk1 ^= (ulong)tail[2] << 16; + goto case 2; + case 2: + tk1 ^= (ulong)tail[1] << 8; + goto case 1; + case 1: + tk1 ^= tail[0]; + tk1 *= c1; + tk1 = RotateLeft(tk1, 31); + tk1 *= c2; + h1 ^= tk1; + break; + } + + h1 ^= (ulong)bytes.Length; + h2 ^= (ulong)bytes.Length; + h1 += h2; + h2 += h1; + + h1 = Fmix64(h1); + h2 = Fmix64(h2); + + h1 += h2; + h2 += h1; + + return (h1, h2); + } + + private static ulong RotateLeft(ulong value, int count) + { + return (value << count) | (value >> (64 - count)); + } + + private static ulong Fmix64(ulong x) + { + x ^= x >> 33; + x *= 0xff51afd7ed558ccd; + x ^= x >> 33; + x *= 0xc4ceb9fe1a85ec53; + x ^= x >> 33; + return x; + } +} diff --git a/java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java b/java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java new file mode 100644 index 0000000000..08aecfba03 --- /dev/null +++ b/java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.xlang; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.fory.test.TestUtils; +import org.testng.SkipException; +import org.testng.annotations.Test; + +/** Executes cross-language tests against the C# implementation. */ +@Test +public class CSharpXlangTest extends XlangTestBase { + private static final String CSHARP_DLL = "Fory.XlangPeer.dll"; + private static final File CSHARP_DIR = new File("../../csharp"); + private static final File CSHARP_BINARY_DIR = new File(CSHARP_DIR, "tests/Fory.XlangPeer/bin/Debug/net8.0"); + private volatile boolean peerBuilt; + + @Override + protected void ensurePeerReady() { + String enabled = System.getenv("FORY_CSHARP_JAVA_CI"); + if (!"1".equals(enabled)) { + throw new SkipException("Skipping CSharpXlangTest: FORY_CSHARP_JAVA_CI not set to 1"); + } + + if (!isDotnetAvailable()) { + throw new SkipException("Skipping CSharpXlangTest: dotnet is not available"); + } + + try { + ensurePeerBuilt(); + } catch (IOException e) { + throw new RuntimeException("Failed to build C# peer", e); + } + } + + @Override + protected CommandContext buildCommandContext(String caseName, Path dataFile) throws IOException { + ensurePeerBuilt(); + + List command = new ArrayList<>(); + command.add("dotnet"); + command.add(new File(CSHARP_BINARY_DIR, CSHARP_DLL).getAbsolutePath()); + command.add("--case"); + command.add(caseName); + + Map env = envBuilder(dataFile); + return new CommandContext(command, env, CSHARP_BINARY_DIR); + } + + @Override + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testUnionXlang(boolean enableCodegen) throws java.io.IOException { + super.testUnionXlang(enableCodegen); + } + + private boolean isDotnetAvailable() { + try { + Process process = new ProcessBuilder("dotnet", "--version").start(); + if (!process.waitFor(30, TimeUnit.SECONDS)) { + process.destroyForcibly(); + return false; + } + return process.exitValue() == 0; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + return false; + } + } + + private void ensurePeerBuilt() throws IOException { + if (peerBuilt) { + return; + } + + synchronized (this) { + if (peerBuilt) { + return; + } + + List buildCommand = + List.of("dotnet", "build", "tests/Fory.XlangPeer/Fory.XlangPeer.csproj", "-c", "Debug"); + boolean built = TestUtils.executeCommand(buildCommand, 180, Collections.emptyMap(), CSHARP_DIR); + if (!built) { + throw new IOException("dotnet build failed for csharp/tests/Fory.XlangPeer"); + } + + File dll = new File(CSHARP_BINARY_DIR, CSHARP_DLL); + if (!dll.exists()) { + throw new IOException("C# peer assembly not found: " + dll.getAbsolutePath()); + } + + peerBuilt = true; + } + } +} From 50ffa34c749afa7a3aa518c99b8bd5e6b6e9eccd Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 08:54:09 +0800 Subject: [PATCH 02/16] perf(csharp): use static typed dispatch for typed serialization --- .gitignore | 11 +- csharp/README.md | 2 +- .../src/Fory.Generator/ForyObjectGenerator.cs | 24 +- csharp/src/Fory/AnySerializer.cs | 83 ++-- csharp/src/Fory/CollectionSerializers.cs | 199 ++++---- csharp/src/Fory/EnumSerializer.cs | 11 +- csharp/src/Fory/FieldSkipper.cs | 22 +- csharp/src/Fory/Fory.cs | 26 +- csharp/src/Fory/ForyMap.cs | 86 ++-- csharp/src/Fory/OptionalSerializer.cs | 56 +-- csharp/src/Fory/PrimitiveSerializers.cs | 261 +++++----- csharp/src/Fory/Serializer.cs | 228 +++++---- csharp/src/Fory/SerializerRegistry.cs | 144 +++--- csharp/src/Fory/TypeResolver.cs | 74 +-- csharp/src/Fory/TypedSerializerBinding.cs | 446 ++++++++++++++++++ csharp/src/Fory/UnionSerializer.cs | 16 +- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 2 +- csharp/tests/Fory.Tests/GlobalUsings.cs | 19 +- csharp/tests/Fory.XlangPeer/Program.cs | 16 +- .../apache/fory/xlang/CSharpXlangTest.java | 234 ++++++++- licenserc.toml | 2 +- 21 files changed, 1320 insertions(+), 642 deletions(-) create mode 100644 csharp/src/Fory/TypedSerializerBinding.cs diff --git a/.gitignore b/.gitignore index db9be7edff..2c0f134e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,13 @@ benchmarks/**/report/ ignored/** ci-logs/** **/*.log -swift/.build \ No newline at end of file +swift/.build + +csharp/src/Fory.Generator/bin/ +csharp/src/Fory.Generator/obj/ +csharp/src/Fory/bin/ +csharp/src/Fory/obj/ +csharp/tests/Fory.Tests/bin/ +csharp/tests/Fory.Tests/obj/ +csharp/tests/Fory.XlangPeer/bin/ +csharp/tests/Fory.XlangPeer/obj/ \ No newline at end of file diff --git a/csharp/README.md b/csharp/README.md index ad4f19d919..a3300bee6c 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -1 +1 @@ -# Apache Fory™ C# +# Apache Fory™ C\# diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index d0625b8735..c0cb978c8a 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -126,12 +126,12 @@ private static void Emit(SourceProductionContext context, ImmutableArray(() => new global::Apache.Fory.EnumSerializer<{model.TypeName}>());"); + $" global::Apache.Fory.SerializerRegistry.RegisterGenerated<{model.TypeName}, global::Apache.Fory.EnumSerializer<{model.TypeName}>>();"); } else { sb.AppendLine( - $" global::Apache.Fory.SerializerRegistry.RegisterGenerated<{model.TypeName}>(() => new {model.SerializerName}());"); + $" global::Apache.Fory.SerializerRegistry.RegisterGenerated<{model.TypeName}, {model.SerializerName}>();"); } } @@ -143,7 +143,7 @@ private static void Emit(SourceProductionContext context, ImmutableArray"); + sb.AppendLine($"file readonly struct {model.SerializerName} : global::Apache.Fory.IStaticSerializer<{model.SerializerName}, {model.TypeName}>"); sb.AppendLine("{"); sb.AppendLine(" private static global::Apache.Fory.RefMode __ForyRefMode(bool nullable, bool trackRef)"); sb.AppendLine(" {"); @@ -177,21 +177,21 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(");"); sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" public override global::Apache.Fory.ForyTypeId StaticTypeId => global::Apache.Fory.ForyTypeId.Struct;"); + sb.AppendLine(" public static global::Apache.Fory.ForyTypeId StaticTypeId => global::Apache.Fory.ForyTypeId.Struct;"); if (model.Kind == DeclKind.Class) { - sb.AppendLine(" public override bool IsNullableType => true;"); - sb.AppendLine(" public override bool IsReferenceTrackableType => true;"); - sb.AppendLine($" public override {model.TypeName} DefaultValue => null!;"); - sb.AppendLine($" public override bool IsNone({model.TypeName} value) => value is null;"); + sb.AppendLine(" public static bool IsNullableType => true;"); + sb.AppendLine(" public static bool IsReferenceTrackableType => true;"); + sb.AppendLine($" public static {model.TypeName} DefaultValue => null!;"); + sb.AppendLine($" public static bool IsNone(in {model.TypeName} value) => value is null;"); } else { - sb.AppendLine($" public override {model.TypeName} DefaultValue => new {model.TypeName}();"); + sb.AppendLine($" public static {model.TypeName} DefaultValue => new {model.TypeName}();"); } sb.AppendLine(); - sb.AppendLine(" public override global::System.Collections.Generic.IReadOnlyList CompatibleTypeMetaFields(bool trackRef)"); + sb.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList CompatibleTypeMetaFields(bool trackRef)"); sb.AppendLine(" {"); if (model.SortedMembers.Length == 0) { @@ -213,7 +213,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine( - $" public override void WriteData(ref global::Apache.Fory.WriteContext context, in {model.TypeName} value, bool hasGenerics)"); + $" public static void WriteData(ref global::Apache.Fory.WriteContext context, in {model.TypeName} value, bool hasGenerics)"); sb.AppendLine(" {"); sb.AppendLine(" _ = hasGenerics;"); sb.AppendLine(" if (context.Compatible)"); @@ -242,7 +242,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine($" public override {model.TypeName} ReadData(ref global::Apache.Fory.ReadContext context)"); + sb.AppendLine($" public static {model.TypeName} ReadData(ref global::Apache.Fory.ReadContext context)"); sb.AppendLine(" {"); sb.AppendLine(" if (context.Compatible)"); sb.AppendLine(" {"); diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index c4b34010c6..12907c318b 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -28,38 +28,35 @@ public readonly struct ForyAnyNullValue { } -public sealed class ForyAnyNullValueSerializer : Serializer +public readonly struct ForyAnyNullValueSerializer : IStaticSerializer { - public static ForyAnyNullValueSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.None; - public override bool IsNullableType => true; - public override ForyAnyNullValue DefaultValue => new(); - public override bool IsNone(ForyAnyNullValue value) => true; - public override void WriteData(ref WriteContext context, in ForyAnyNullValue value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.None; + public static bool IsNullableType => true; + public static ForyAnyNullValue DefaultValue => new(); + public static bool IsNone(in ForyAnyNullValue value) => true; + public static void WriteData(ref WriteContext context, in ForyAnyNullValue value, bool hasGenerics) { _ = context; _ = value; _ = hasGenerics; } - public override ForyAnyNullValue ReadData(ref ReadContext context) + public static ForyAnyNullValue ReadData(ref ReadContext context) { _ = context; return new ForyAnyNullValue(); } } -public sealed class DynamicAnyObjectSerializer : Serializer +public readonly struct DynamicAnyObjectSerializer : IStaticSerializer { - public static DynamicAnyObjectSerializer Instance { get; } = new(); + public static ForyTypeId StaticTypeId => ForyTypeId.Unknown; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static object? DefaultValue => new ForyAnyNullValue(); + public static bool IsNone(in object? value) => value is null || value is ForyAnyNullValue; - public override ForyTypeId StaticTypeId => ForyTypeId.Unknown; - public override bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; - public override object? DefaultValue => new ForyAnyNullValue(); - public override bool IsNone(object? value) => value is null || value is ForyAnyNullValue; - - public override void WriteData(ref WriteContext context, in object? value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in object? value, bool hasGenerics) { if (IsNone(value)) { @@ -69,7 +66,7 @@ public override void WriteData(ref WriteContext context, in object? value, bool DynamicAnyCodec.WriteAnyPayload(value!, ref context, hasGenerics); } - public override object? ReadData(ref ReadContext context) + public static object? ReadData(ref ReadContext context) { DynamicTypeInfo? dynamicTypeInfo = context.DynamicTypeInfo(typeof(object)); if (dynamicTypeInfo is null) @@ -86,18 +83,18 @@ public override void WriteData(ref WriteContext context, in object? value, bool return context.TypeResolver.ReadDynamicValue(dynamicTypeInfo, ref context); } - public override void WriteTypeInfo(ref WriteContext context) + public static void WriteTypeInfo(ref WriteContext context) { throw new ForyInvalidDataException("dynamic Any value type info is runtime-only"); } - public override void ReadTypeInfo(ref ReadContext context) + public static void ReadTypeInfo(ref ReadContext context) { DynamicTypeInfo typeInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); context.SetDynamicTypeInfo(typeof(object), typeInfo); } - public override void Write(ref WriteContext context, in object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public static void Write(ref WriteContext context, in object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { if (refMode != RefMode.None) { @@ -132,7 +129,7 @@ public override void Write(ref WriteContext context, in object? value, RefMode r WriteData(ref context, value, hasGenerics); } - public override object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + public static object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { if (refMode != RefMode.None) { @@ -179,7 +176,7 @@ public override void Write(ref WriteContext context, in object? value, RefMode r private static bool AnyValueIsReferenceTrackable(object value) { - ISerializer serializer = SerializerRegistry.Get(value.GetType()); + SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); return serializer.IsReferenceTrackableType; } @@ -207,7 +204,7 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) return; } - ISerializer serializer = SerializerRegistry.Get(value.GetType()); + SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); serializer.WriteTypeInfo(ref context); } @@ -241,25 +238,25 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) public static void WriteAny(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo = true, bool hasGenerics = false) { - DynamicAnyObjectSerializer.Instance.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + SerializerRegistry.Get().Write(ref context, value, refMode, writeTypeInfo, hasGenerics); } public static object? ReadAny(ref ReadContext context, RefMode refMode, bool readTypeInfo = true) { - object? value = DynamicAnyObjectSerializer.Instance.Read(ref context, refMode, readTypeInfo); + object? value = SerializerRegistry.Get().Read(ref context, refMode, readTypeInfo); return value is ForyAnyNullValue ? null : value; } public static void WriteAnyList(ref WriteContext context, IList? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) { List? wrapped = value is null ? null : [.. value]; - Serializer> serializer = new ListSerializer(); - serializer.Write(ref context, wrapped, refMode, writeTypeInfo, hasGenerics); + Serializer> serializer = SerializerRegistry.Get>(); + serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); } public static List? ReadAnyList(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) { - Serializer> serializer = new ListSerializer(); + Serializer> serializer = SerializerRegistry.Get>(); List? wrapped = serializer.Read(ref context, refMode, readTypeInfo); if (wrapped is null) { @@ -280,13 +277,13 @@ public static void WriteAnyList(ref WriteContext context, IList? value, public static void WriteStringAnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) { Dictionary? wrapped = value is null ? null : new Dictionary(value); - Serializer> serializer = new MapSerializer(); - serializer.Write(ref context, wrapped, refMode, writeTypeInfo, hasGenerics); + Serializer> serializer = SerializerRegistry.Get>(); + serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); } public static Dictionary? ReadStringAnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) { - Serializer> serializer = new MapSerializer(); + Serializer> serializer = SerializerRegistry.Get>(); Dictionary? wrapped = serializer.Read(ref context, refMode, readTypeInfo); if (wrapped is null) { @@ -305,13 +302,13 @@ public static void WriteStringAnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) { Dictionary? wrapped = value is null ? null : new Dictionary(value); - Serializer> serializer = new MapSerializer(); - serializer.Write(ref context, wrapped, refMode, writeTypeInfo, hasGenerics); + Serializer> serializer = SerializerRegistry.Get>(); + serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); } public static Dictionary? ReadInt32AnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) { - Serializer> serializer = new MapSerializer(); + Serializer> serializer = SerializerRegistry.Get>(); Dictionary? wrapped = serializer.Read(ref context, refMode, readTypeInfo); if (wrapped is null) { @@ -394,7 +391,7 @@ public static void WriteAnyPayload(object value, ref WriteContext context, bool return; } - ISerializer serializer = SerializerRegistry.Get(value.GetType()); + SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); serializer.WriteData(ref context, value, hasGenerics); } @@ -505,7 +502,7 @@ public static List ReadCollectionData(Serializer elementSerializer, ref { for (int i = 0; i < length; i++) { - values.Add((T)elementSerializer.Read(ref context, RefMode.Tracking, true)!); + values.Add(elementSerializer.Read(ref context, RefMode.Tracking, true)); } return values; @@ -548,10 +545,10 @@ public static List ReadCollectionData(Serializer elementSerializer, ref if (trackRef) { - for (int i = 0; i < length; i++) - { - values.Add((T)elementSerializer.Read(ref context, RefMode.Tracking, false)!); - } + for (int i = 0; i < length; i++) + { + values.Add(elementSerializer.Read(ref context, RefMode.Tracking, false)); + } if (!declared) { @@ -600,11 +597,11 @@ private static T ReadCollectionElementWithCanonicalization( { if (!canonicalize) { - return (T)elementSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + return elementSerializer.Read(ref context, RefMode.None, readTypeInfo); } int start = context.Reader.Cursor; - T value = (T)elementSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + T value = elementSerializer.Read(ref context, RefMode.None, readTypeInfo); int end = context.Reader.Cursor; return context.CanonicalizeNonTrackingReference(value, start, end); } diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index a3601fdaf5..e9b7d9b4bf 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -380,21 +380,21 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) } } -public sealed class ArraySerializer : Serializer +public readonly struct ArraySerializer : IStaticSerializer, T[]> { - private readonly Serializer _elementSerializer = SerializerRegistry.Get(); - private readonly ForyTypeId? _primitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); + private static Serializer ElementSerializer => SerializerRegistry.Get(); + private static readonly ForyTypeId? PrimitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); - public override ForyTypeId StaticTypeId => _primitiveArrayTypeId ?? ForyTypeId.List; - public override bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; - public override T[] DefaultValue => null!; - public override bool IsNone(T[] value) => value is null; + public static ForyTypeId StaticTypeId => PrimitiveArrayTypeId ?? ForyTypeId.List; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static T[] DefaultValue => null!; + public static bool IsNone(in T[] value) => value is null; - public override void WriteData(ref WriteContext context, in T[] value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in T[] value, bool hasGenerics) { T[] safe = value ?? []; - if (_primitiveArrayTypeId is not null) + if (PrimitiveArrayTypeId is not null) { PrimitiveArrayCodec.WritePrimitiveArray(safe, ref context); return; @@ -402,81 +402,83 @@ public override void WriteData(ref WriteContext context, in T[] value, bool hasG DynamicAnyCodec.WriteCollectionData( safe, - _elementSerializer, + ElementSerializer, ref context, hasGenerics); } - public override T[] ReadData(ref ReadContext context) + public static T[] ReadData(ref ReadContext context) { - if (_primitiveArrayTypeId is not null) + if (PrimitiveArrayTypeId is not null) { return PrimitiveArrayCodec.ReadPrimitiveArray(ref context); } - List values = DynamicAnyCodec.ReadCollectionData(_elementSerializer, ref context); + List values = DynamicAnyCodec.ReadCollectionData(ElementSerializer, ref context); return values.ToArray(); } } -public sealed class ListSerializer : Serializer> +public readonly struct ListSerializer : IStaticSerializer, List> { - private readonly Serializer _elementSerializer = SerializerRegistry.Get(); + private static Serializer ElementSerializer => SerializerRegistry.Get(); - public override ForyTypeId StaticTypeId => ForyTypeId.List; - public override bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; - public override List DefaultValue => null!; - public override bool IsNone(List value) => value is null; + public static ForyTypeId StaticTypeId => ForyTypeId.List; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static List DefaultValue => null!; + public static bool IsNone(in List value) => value is null; - public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in List value, bool hasGenerics) { List safe = value ?? []; - DynamicAnyCodec.WriteCollectionData(safe, _elementSerializer, ref context, hasGenerics); + DynamicAnyCodec.WriteCollectionData(safe, ElementSerializer, ref context, hasGenerics); } - public override List ReadData(ref ReadContext context) + public static List ReadData(ref ReadContext context) { - return DynamicAnyCodec.ReadCollectionData(_elementSerializer, ref context); + return DynamicAnyCodec.ReadCollectionData(ElementSerializer, ref context); } } -public sealed class SetSerializer : Serializer> where T : notnull +public readonly struct SetSerializer : IStaticSerializer, HashSet> where T : notnull { - private readonly ListSerializer _listSerializer = new(); + private static Serializer> ListSerializer => SerializerRegistry.Get>(); - public override ForyTypeId StaticTypeId => ForyTypeId.Set; - public override bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; - public override HashSet DefaultValue => null!; - public override bool IsNone(HashSet value) => value is null; + public static ForyTypeId StaticTypeId => ForyTypeId.Set; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static HashSet DefaultValue => null!; + public static bool IsNone(in HashSet value) => value is null; - public override void WriteData(ref WriteContext context, in HashSet value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in HashSet value, bool hasGenerics) { List list = value is null ? [] : [.. value]; - _listSerializer.WriteData(ref context, list, hasGenerics); + ListSerializer.WriteData(ref context, list, hasGenerics); } - public override HashSet ReadData(ref ReadContext context) + public static HashSet ReadData(ref ReadContext context) { - return [.. _listSerializer.ReadData(ref context)]; + return [.. ListSerializer.ReadData(ref context)]; } } -public sealed class MapSerializer : Serializer> +public readonly struct MapSerializer : IStaticSerializer, Dictionary> where TKey : notnull { - private readonly Serializer _keySerializer = SerializerRegistry.Get(); - private readonly Serializer _valueSerializer = SerializerRegistry.Get(); + private static Serializer KeySerializer => SerializerRegistry.Get(); + private static Serializer ValueSerializer => SerializerRegistry.Get(); - public override ForyTypeId StaticTypeId => ForyTypeId.Map; - public override bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; - public override Dictionary DefaultValue => null!; - public override bool IsNone(Dictionary value) => value is null; + public static ForyTypeId StaticTypeId => ForyTypeId.Map; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static Dictionary DefaultValue => null!; + public static bool IsNone(in Dictionary value) => value is null; - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) { + Serializer keySerializer = KeySerializer; + Serializer valueSerializer = ValueSerializer; Dictionary map = value ?? []; context.Writer.WriteVarUInt32((uint)map.Count); if (map.Count == 0) @@ -484,17 +486,28 @@ public override void WriteData(ref WriteContext context, in Dictionary[] pairs = [.. map]; if (keyDynamicType || valueDynamicType) { - WriteDynamicMapPairs(pairs, ref context, hasGenerics, trackKeyRef, trackValueRef, keyDeclared, valueDeclared, keyDynamicType, valueDynamicType); + WriteDynamicMapPairs( + pairs, + ref context, + hasGenerics, + trackKeyRef, + trackValueRef, + keyDeclared, + valueDeclared, + keyDynamicType, + valueDynamicType, + keySerializer, + valueSerializer); return; } @@ -502,8 +515,8 @@ public override void WriteData(ref WriteContext context, in Dictionary pair = pairs[index]; - bool keyIsNull = _keySerializer.IsNoneObject(pair.Key); - bool valueIsNull = _valueSerializer.IsNoneObject(pair.Value); + bool keyIsNull = keySerializer.IsNoneObject(pair.Key); + bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); if (keyIsNull || valueIsNull) { byte header = 0; @@ -542,20 +555,20 @@ public override void WriteData(ref WriteContext context, in Dictionary current = pairs[index]; - if (_keySerializer.IsNoneObject(current.Key) || _valueSerializer.IsNoneObject(current.Value)) + if (keySerializer.IsNoneObject(current.Key) || valueSerializer.IsNoneObject(current.Value)) { break; } - _keySerializer.Write(ref context, current.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - _valueSerializer.Write(ref context, current.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + keySerializer.Write(ref context, current.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + valueSerializer.Write(ref context, current.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); chunkSize += 1; index += 1; } @@ -615,8 +628,10 @@ public override void WriteData(ref WriteContext context, in Dictionary ReadData(ref ReadContext context) + public static Dictionary ReadData(ref ReadContext context) { + Serializer keySerializer = KeySerializer; + Serializer valueSerializer = ValueSerializer; int totalLength = checked((int)context.Reader.ReadVarUInt32()); if (totalLength == 0) { @@ -624,9 +639,9 @@ public override Dictionary ReadData(ref ReadContext context) } Dictionary map = new(totalLength); - bool keyDynamicType = _keySerializer.StaticTypeId == ForyTypeId.Unknown; - bool valueDynamicType = _valueSerializer.StaticTypeId == ForyTypeId.Unknown; - bool canonicalizeValues = context.TrackRef && _valueSerializer.IsReferenceTrackableType; + bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool canonicalizeValues = context.TrackRef && valueSerializer.IsReferenceTrackableType; int readCount = 0; while (readCount < totalLength) @@ -641,7 +656,7 @@ public override Dictionary ReadData(ref ReadContext context) if (keyNull && valueNull) { - map[(TKey)_keySerializer.DefaultObject!] = (TValue)_valueSerializer.DefaultObject!; + map[(TKey)keySerializer.DefaultObject!] = (TValue)valueSerializer.DefaultObject!; readCount += 1; continue; } @@ -653,16 +668,16 @@ public override Dictionary ReadData(ref ReadContext context) trackValueRef, valueDynamicType || !valueDeclared, canonicalizeValues, - _valueSerializer); - map[(TKey)_keySerializer.DefaultObject!] = value; + valueSerializer); + map[(TKey)keySerializer.DefaultObject!] = value; readCount += 1; continue; } if (valueNull) { - TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared)!; - map[key] = (TValue)_valueSerializer.DefaultObject!; + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared); + map[key] = (TValue)valueSerializer.DefaultObject!; readCount += 1; continue; } @@ -670,18 +685,18 @@ public override Dictionary ReadData(ref ReadContext context) int chunkSize = context.Reader.ReadUInt8(); if (!keyDeclared) { - _keySerializer.ReadTypeInfo(ref context); + keySerializer.ReadTypeInfo(ref context); } if (!valueDeclared) { - _valueSerializer.ReadTypeInfo(ref context); + valueSerializer.ReadTypeInfo(ref context); } for (int i = 0; i < chunkSize; i++) { - TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false)!; - TValue value = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, _valueSerializer); + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + TValue value = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, valueSerializer); map[key] = value; } @@ -701,7 +716,7 @@ public override Dictionary ReadData(ref ReadContext context) return map; } - private void WriteDynamicMapPairs( + private static void WriteDynamicMapPairs( KeyValuePair[] pairs, ref WriteContext context, bool hasGenerics, @@ -710,12 +725,14 @@ private void WriteDynamicMapPairs( bool keyDeclared, bool valueDeclared, bool keyDynamicType, - bool valueDynamicType) + bool valueDynamicType, + Serializer keySerializer, + Serializer valueSerializer) { foreach (KeyValuePair pair in pairs) { - bool keyIsNull = _keySerializer.IsNoneObject(pair.Key); - bool valueIsNull = _valueSerializer.IsNoneObject(pair.Value); + bool keyIsNull = keySerializer.IsNoneObject(pair.Key); + bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); byte header = 0; if (trackKeyRef) { @@ -761,11 +778,11 @@ private void WriteDynamicMapPairs( } else { - _valueSerializer.WriteTypeInfo(ref context); + valueSerializer.WriteTypeInfo(ref context); } } - _valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); continue; } @@ -779,11 +796,11 @@ private void WriteDynamicMapPairs( } else { - _keySerializer.WriteTypeInfo(ref context); + keySerializer.WriteTypeInfo(ref context); } } - _keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); continue; } @@ -796,7 +813,7 @@ private void WriteDynamicMapPairs( } else { - _keySerializer.WriteTypeInfo(ref context); + keySerializer.WriteTypeInfo(ref context); } } @@ -808,12 +825,12 @@ private void WriteDynamicMapPairs( } else { - _valueSerializer.WriteTypeInfo(ref context); + valueSerializer.WriteTypeInfo(ref context); } } - _keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - _valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); } } @@ -822,15 +839,15 @@ private static TValue ReadValueElement( bool trackValueRef, bool readTypeInfo, bool canonicalizeValues, - ISerializer valueSerializer) + Serializer valueSerializer) { if (trackValueRef || !canonicalizeValues) { - return (TValue)valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo)!; + return valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo); } int start = context.Reader.Cursor; - TValue value = (TValue)valueSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + TValue value = valueSerializer.Read(ref context, RefMode.None, readTypeInfo); int end = context.Reader.Cursor; return context.CanonicalizeNonTrackingReference(value, start, end); } diff --git a/csharp/src/Fory/EnumSerializer.cs b/csharp/src/Fory/EnumSerializer.cs index 1d73180b50..7efd3c31e5 100644 --- a/csharp/src/Fory/EnumSerializer.cs +++ b/csharp/src/Fory/EnumSerializer.cs @@ -17,19 +17,19 @@ namespace Apache.Fory; -public sealed class EnumSerializer : Serializer where TEnum : struct, Enum +public readonly struct EnumSerializer : IStaticSerializer, TEnum> where TEnum : struct, Enum { - public override ForyTypeId StaticTypeId => ForyTypeId.Enum; - public override TEnum DefaultValue => default; + public static ForyTypeId StaticTypeId => ForyTypeId.Enum; + public static TEnum DefaultValue => default; - public override void WriteData(ref WriteContext context, in TEnum value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in TEnum value, bool hasGenerics) { _ = hasGenerics; uint ordinal = Convert.ToUInt32(value); context.Writer.WriteVarUInt32(ordinal); } - public override TEnum ReadData(ref ReadContext context) + public static TEnum ReadData(ref ReadContext context) { uint ordinal = context.Reader.ReadVarUInt32(); TEnum value = (TEnum)Enum.ToObject(typeof(TEnum), ordinal); @@ -41,4 +41,3 @@ public override TEnum ReadData(ref ReadContext context) return value; } } - diff --git a/csharp/src/Fory/FieldSkipper.cs b/csharp/src/Fory/FieldSkipper.cs index bb5773d877..16c06f2292 100644 --- a/csharp/src/Fory/FieldSkipper.cs +++ b/csharp/src/Fory/FieldSkipper.cs @@ -57,21 +57,21 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie switch (fieldType.TypeId) { case (uint)ForyTypeId.Bool: - return BoolSerializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.Int8: - return Int8Serializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.Int16: - return Int16Serializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.VarInt32: - return Int32Serializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.VarInt64: - return Int64Serializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.Float32: - return Float32Serializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.Float64: - return Float64Serializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.String: - return StringSerializer.Instance.Read(ref context, refMode, false); + return SerializerRegistry.Get().Read(ref context, refMode, false); case (uint)ForyTypeId.List: { if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)ForyTypeId.String) @@ -79,7 +79,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie throw new ForyInvalidDataException("unsupported compatible list element type"); } - return new ListSerializer().Read(ref context, refMode, false); + return SerializerRegistry.Get>().Read(ref context, refMode, false); } case (uint)ForyTypeId.Set: { @@ -88,7 +88,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie throw new ForyInvalidDataException("unsupported compatible set element type"); } - return new SetSerializer().Read(ref context, refMode, false); + return SerializerRegistry.Get>().Read(ref context, refMode, false); } case (uint)ForyTypeId.Map: { @@ -99,7 +99,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie throw new ForyInvalidDataException("unsupported compatible map key/value type"); } - return new MapSerializer().Read(ref context, refMode, false); + return SerializerRegistry.Get>().Read(ref context, refMode, false); } case (uint)ForyTypeId.Enum: return ReadEnumOrdinal(ref context, refMode); diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs index 2f0a881dbd..d2f107ec0d 100644 --- a/csharp/src/Fory/Fory.cs +++ b/csharp/src/Fory/Fory.cs @@ -55,28 +55,26 @@ public Fory Register(string typeNamespace, string typeName) } public Fory Register(uint typeId) - where TSerializer : Serializer, new() + where TSerializer : IStaticSerializer { - TSerializer serializer = new(); - SerializerRegistry.RegisterCustom(typeof(T), serializer); - _typeResolver.Register(typeof(T), typeId, serializer); + SerializerRegistry.RegisterCustom(); + _typeResolver.Register(typeof(T), typeId); return this; } public Fory Register(string typeNamespace, string typeName) - where TSerializer : Serializer, new() + where TSerializer : IStaticSerializer { - TSerializer serializer = new(); - SerializerRegistry.RegisterCustom(typeof(T), serializer); - _typeResolver.Register(typeof(T), typeNamespace, typeName, serializer); + SerializerRegistry.RegisterCustom(); + _typeResolver.Register(typeof(T), typeNamespace, typeName); return this; } public byte[] Serialize(in T value) { ByteWriter writer = new(); - Serializer serializer = SerializerRegistry.Get(); - bool isNone = serializer.IsNoneObject(value); + Serializer binding = TypedSerializerBindingCache.Get(); + bool isNone = binding.IsNone(value); WriteHead(writer, isNone); if (!isNone) { @@ -88,7 +86,7 @@ public byte[] Serialize(in T value) new CompatibleTypeDefWriteState(), new MetaStringWriteState()); RefMode refMode = Config.TrackRef ? RefMode.Tracking : RefMode.NullOnly; - serializer.Write(ref context, value, refMode, true, false); + binding.Write(ref context, value, refMode, true, false); context.ResetObjectState(); } @@ -202,10 +200,10 @@ public bool ReadHead(ByteReader reader) private T DeserializeFromReader(ByteReader reader) { bool isNone = ReadHead(reader); - Serializer serializer = SerializerRegistry.Get(); + Serializer binding = TypedSerializerBindingCache.Get(); if (isNone) { - return serializer.DefaultValue; + return binding.DefaultValue; } ReadContext context = new( @@ -216,7 +214,7 @@ private T DeserializeFromReader(ByteReader reader) new CompatibleTypeDefReadState(), new MetaStringReadState()); RefMode refMode = Config.TrackRef ? RefMode.Tracking : RefMode.NullOnly; - T value = serializer.Read(ref context, refMode, true); + T value = binding.Read(ref context, refMode, true); context.ResetObjectState(); return value; } diff --git a/csharp/src/Fory/ForyMap.cs b/csharp/src/Fory/ForyMap.cs index 82b7e51aa8..85a7967e06 100644 --- a/csharp/src/Fory/ForyMap.cs +++ b/csharp/src/Fory/ForyMap.cs @@ -101,19 +101,21 @@ IEnumerator IEnumerable.GetEnumerator() } } -public sealed class ForyMapSerializer : Serializer> +public readonly struct ForyMapSerializer : IStaticSerializer, ForyMap> { - private readonly Serializer _keySerializer = SerializerRegistry.Get(); - private readonly Serializer _valueSerializer = SerializerRegistry.Get(); + private static Serializer KeySerializer => SerializerRegistry.Get(); + private static Serializer ValueSerializer => SerializerRegistry.Get(); - public override ForyTypeId StaticTypeId => ForyTypeId.Map; - public override bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; - public override ForyMap DefaultValue => null!; - public override bool IsNone(ForyMap value) => value is null; + public static ForyTypeId StaticTypeId => ForyTypeId.Map; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static ForyMap DefaultValue => null!; + public static bool IsNone(in ForyMap value) => value is null; - public override void WriteData(ref WriteContext context, in ForyMap value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in ForyMap value, bool hasGenerics) { + Serializer keySerializer = KeySerializer; + Serializer valueSerializer = ValueSerializer; ForyMap map = value ?? new ForyMap(); context.Writer.WriteVarUInt32((uint)map.Count); if (map.Count == 0) @@ -121,15 +123,15 @@ public override void WriteData(ref WriteContext context, in ForyMap entry in map) { - bool keyIsNull = entry.Key is null || _keySerializer.IsNoneObject(entry.Key); - bool valueIsNull = _valueSerializer.IsNoneObject(entry.Value); + bool keyIsNull = entry.Key is null || keySerializer.IsNoneObject(entry.Key); + bool valueIsNull = valueSerializer.IsNoneObject(entry.Value); byte header = 0; if (trackKeyRef) { @@ -169,10 +171,10 @@ public override void WriteData(ref WriteContext context, in ForyMap ReadData(ref ReadContext context) + public static ForyMap ReadData(ref ReadContext context) { + Serializer keySerializer = KeySerializer; + Serializer valueSerializer = ValueSerializer; int totalLength = checked((int)context.Reader.ReadVarUInt32()); if (totalLength == 0) { @@ -232,9 +236,9 @@ public override ForyMap ReadData(ref ReadContext context) } ForyMap map = new(); - bool keyDynamicType = _keySerializer.StaticTypeId == ForyTypeId.Unknown; - bool valueDynamicType = _valueSerializer.StaticTypeId == ForyTypeId.Unknown; - bool canonicalizeValues = context.TrackRef && _valueSerializer.IsReferenceTrackableType; + bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool canonicalizeValues = context.TrackRef && valueSerializer.IsReferenceTrackableType; int readCount = 0; while (readCount < totalLength) @@ -249,28 +253,28 @@ public override ForyMap ReadData(ref ReadContext context) if (keyNull && valueNull) { - map.Add(default, (TValue)_valueSerializer.DefaultObject!); + map.Add(default, (TValue)valueSerializer.DefaultObject!); readCount += 1; continue; } if (keyNull) { - TValue value = ReadValueElement( + TValue valueRead = ReadValueElement( ref context, trackValueRef, valueDynamicType || !valueDeclared, canonicalizeValues, - _valueSerializer); - map.Add(default, value); + valueSerializer); + map.Add(default, valueRead); readCount += 1; continue; } if (valueNull) { - TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared)!; - map.Add(key, (TValue)_valueSerializer.DefaultObject!); + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared); + map.Add(key, (TValue)valueSerializer.DefaultObject!); readCount += 1; continue; } @@ -278,19 +282,19 @@ public override ForyMap ReadData(ref ReadContext context) int chunkSize = context.Reader.ReadUInt8(); if (!keyDeclared) { - _keySerializer.ReadTypeInfo(ref context); + keySerializer.ReadTypeInfo(ref context); } if (!valueDeclared) { - _valueSerializer.ReadTypeInfo(ref context); + valueSerializer.ReadTypeInfo(ref context); } for (int i = 0; i < chunkSize; i++) { - TKey key = (TKey)_keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false)!; - TValue value = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, _valueSerializer); - map.Add(key, value); + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + TValue valueRead = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, valueSerializer); + map.Add(key, valueRead); } if (!keyDeclared) @@ -314,15 +318,15 @@ private static TValue ReadValueElement( bool trackValueRef, bool readTypeInfo, bool canonicalizeValues, - ISerializer valueSerializer) + Serializer valueSerializer) { if (trackValueRef || !canonicalizeValues) { - return (TValue)valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo)!; + return valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo); } int start = context.Reader.Cursor; - TValue value = (TValue)valueSerializer.Read(ref context, RefMode.None, readTypeInfo)!; + TValue value = valueSerializer.Read(ref context, RefMode.None, readTypeInfo); int end = context.Reader.Cursor; return context.CanonicalizeNonTrackingReference(value, start, end); } diff --git a/csharp/src/Fory/OptionalSerializer.cs b/csharp/src/Fory/OptionalSerializer.cs index 114815c253..dcccc1a128 100644 --- a/csharp/src/Fory/OptionalSerializer.cs +++ b/csharp/src/Fory/OptionalSerializer.cs @@ -17,29 +17,24 @@ namespace Apache.Fory; -public sealed class NullableSerializer : Serializer where T : struct +public readonly struct NullableSerializer : IStaticSerializer, T?> where T : struct { - private readonly Serializer _wrappedSerializer; + private static Serializer WrappedSerializer => SerializerRegistry.Get(); - public NullableSerializer() - { - _wrappedSerializer = SerializerRegistry.Get(); - } + public static ForyTypeId StaticTypeId => WrappedSerializer.StaticTypeId; - public override ForyTypeId StaticTypeId => _wrappedSerializer.StaticTypeId; + public static bool IsNullableType => true; - public override bool IsNullableType => true; + public static bool IsReferenceTrackableType => WrappedSerializer.IsReferenceTrackableType; - public override bool IsReferenceTrackableType => _wrappedSerializer.IsReferenceTrackableType; + public static T? DefaultValue => null; - public override T? DefaultValue => null; - - public override bool IsNone(T? value) + public static bool IsNone(in T? value) { return !value.HasValue; } - public override void WriteData(ref WriteContext context, in T? value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in T? value, bool hasGenerics) { if (!value.HasValue) { @@ -47,30 +42,30 @@ public override void WriteData(ref WriteContext context, in T? value, bool hasGe } T wrapped = value.Value; - _wrappedSerializer.WriteData(ref context, wrapped, hasGenerics); + WrappedSerializer.WriteData(ref context, wrapped, hasGenerics); } - public override T? ReadData(ref ReadContext context) + public static T? ReadData(ref ReadContext context) { - return _wrappedSerializer.ReadData(ref context); + return WrappedSerializer.ReadData(ref context); } - public override void WriteTypeInfo(ref WriteContext context) + public static void WriteTypeInfo(ref WriteContext context) { - _wrappedSerializer.WriteTypeInfo(ref context); + WrappedSerializer.WriteTypeInfo(ref context); } - public override void ReadTypeInfo(ref ReadContext context) + public static void ReadTypeInfo(ref ReadContext context) { - _wrappedSerializer.ReadTypeInfo(ref context); + WrappedSerializer.ReadTypeInfo(ref context); } - public override IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + public static IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { - return _wrappedSerializer.CompatibleTypeMetaFields(trackRef); + return WrappedSerializer.CompatibleTypeMetaFields(trackRef); } - public override void Write(ref WriteContext context, in T? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public static void Write(ref WriteContext context, in T? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { switch (refMode) { @@ -81,7 +76,7 @@ public override void Write(ref WriteContext context, in T? value, RefMode refMod } T wrapped = value.Value; - _wrappedSerializer.Write(ref context, wrapped, RefMode.None, writeTypeInfo, hasGenerics); + WrappedSerializer.Write(ref context, wrapped, RefMode.None, writeTypeInfo, hasGenerics); break; case RefMode.NullOnly: if (!value.HasValue) @@ -91,7 +86,7 @@ public override void Write(ref WriteContext context, in T? value, RefMode refMod } context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); - _wrappedSerializer.Write(ref context, value.Value, RefMode.None, writeTypeInfo, hasGenerics); + WrappedSerializer.Write(ref context, value.Value, RefMode.None, writeTypeInfo, hasGenerics); break; case RefMode.Tracking: if (!value.HasValue) @@ -100,17 +95,17 @@ public override void Write(ref WriteContext context, in T? value, RefMode refMod return; } - _wrappedSerializer.Write(ref context, value.Value, RefMode.Tracking, writeTypeInfo, hasGenerics); + WrappedSerializer.Write(ref context, value.Value, RefMode.Tracking, writeTypeInfo, hasGenerics); break; } } - public override T? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + public static T? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { switch (refMode) { case RefMode.None: - return _wrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); + return WrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); case RefMode.NullOnly: { sbyte refFlag = context.Reader.ReadInt8(); @@ -119,7 +114,7 @@ public override void Write(ref WriteContext context, in T? value, RefMode refMod return null; } - return _wrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); + return WrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); } case RefMode.Tracking: { @@ -130,11 +125,10 @@ public override void Write(ref WriteContext context, in T? value, RefMode refMod } context.Reader.MoveBack(1); - return _wrappedSerializer.Read(ref context, RefMode.Tracking, readTypeInfo); + return WrappedSerializer.Read(ref context, RefMode.Tracking, readTypeInfo); } default: throw new ForyInvalidDataException($"unsupported ref mode {refMode}"); } } } - diff --git a/csharp/src/Fory/PrimitiveSerializers.cs b/csharp/src/Fory/PrimitiveSerializers.cs index 0404aacf81..77cf2828a8 100644 --- a/csharp/src/Fory/PrimitiveSerializers.cs +++ b/csharp/src/Fory/PrimitiveSerializers.cs @@ -68,202 +68,190 @@ private static ForyTimestamp Normalize(long seconds, long nanos) } } -public sealed class BoolSerializer : Serializer +public readonly struct BoolSerializer : IStaticSerializer { - public static BoolSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Bool; - public override bool DefaultValue => false; - public override void WriteData(ref WriteContext context, in bool value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.Bool; + public static bool DefaultValue => false; + public static void WriteData(ref WriteContext context, in bool value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt8(value ? (byte)1 : (byte)0); } - public override bool ReadData(ref ReadContext context) + public static bool ReadData(ref ReadContext context) { return context.Reader.ReadUInt8() != 0; } } -public sealed class Int8Serializer : Serializer +public readonly struct Int8Serializer : IStaticSerializer { - public static Int8Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Int8; - public override sbyte DefaultValue => 0; - public override void WriteData(ref WriteContext context, in sbyte value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.Int8; + public static sbyte DefaultValue => 0; + public static void WriteData(ref WriteContext context, in sbyte value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt8(value); } - public override sbyte ReadData(ref ReadContext context) + public static sbyte ReadData(ref ReadContext context) { return context.Reader.ReadInt8(); } } -public sealed class Int16Serializer : Serializer +public readonly struct Int16Serializer : IStaticSerializer { - public static Int16Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Int16; - public override short DefaultValue => 0; - public override void WriteData(ref WriteContext context, in short value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.Int16; + public static short DefaultValue => 0; + public static void WriteData(ref WriteContext context, in short value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt16(value); } - public override short ReadData(ref ReadContext context) + public static short ReadData(ref ReadContext context) { return context.Reader.ReadInt16(); } } -public sealed class Int32Serializer : Serializer +public readonly struct Int32Serializer : IStaticSerializer { - public static Int32Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.VarInt32; - public override int DefaultValue => 0; - public override void WriteData(ref WriteContext context, in int value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.VarInt32; + public static int DefaultValue => 0; + public static void WriteData(ref WriteContext context, in int value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarInt32(value); } - public override int ReadData(ref ReadContext context) + public static int ReadData(ref ReadContext context) { return context.Reader.ReadVarInt32(); } } -public sealed class Int64Serializer : Serializer +public readonly struct Int64Serializer : IStaticSerializer { - public static Int64Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.VarInt64; - public override long DefaultValue => 0; - public override void WriteData(ref WriteContext context, in long value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.VarInt64; + public static long DefaultValue => 0; + public static void WriteData(ref WriteContext context, in long value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarInt64(value); } - public override long ReadData(ref ReadContext context) + public static long ReadData(ref ReadContext context) { return context.Reader.ReadVarInt64(); } } -public sealed class UInt8Serializer : Serializer +public readonly struct UInt8Serializer : IStaticSerializer { - public static UInt8Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.UInt8; - public override byte DefaultValue => 0; - public override void WriteData(ref WriteContext context, in byte value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.UInt8; + public static byte DefaultValue => 0; + public static void WriteData(ref WriteContext context, in byte value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt8(value); } - public override byte ReadData(ref ReadContext context) + public static byte ReadData(ref ReadContext context) { return context.Reader.ReadUInt8(); } } -public sealed class UInt16Serializer : Serializer +public readonly struct UInt16Serializer : IStaticSerializer { - public static UInt16Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.UInt16; - public override ushort DefaultValue => 0; - public override void WriteData(ref WriteContext context, in ushort value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.UInt16; + public static ushort DefaultValue => 0; + public static void WriteData(ref WriteContext context, in ushort value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt16(value); } - public override ushort ReadData(ref ReadContext context) + public static ushort ReadData(ref ReadContext context) { return context.Reader.ReadUInt16(); } } -public sealed class UInt32Serializer : Serializer +public readonly struct UInt32Serializer : IStaticSerializer { - public static UInt32Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.VarUInt32; - public override uint DefaultValue => 0; - public override void WriteData(ref WriteContext context, in uint value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.VarUInt32; + public static uint DefaultValue => 0; + public static void WriteData(ref WriteContext context, in uint value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarUInt32(value); } - public override uint ReadData(ref ReadContext context) + public static uint ReadData(ref ReadContext context) { return context.Reader.ReadVarUInt32(); } } -public sealed class UInt64Serializer : Serializer +public readonly struct UInt64Serializer : IStaticSerializer { - public static UInt64Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.VarUInt64; - public override ulong DefaultValue => 0; - public override void WriteData(ref WriteContext context, in ulong value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.VarUInt64; + public static ulong DefaultValue => 0; + public static void WriteData(ref WriteContext context, in ulong value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarUInt64(value); } - public override ulong ReadData(ref ReadContext context) + public static ulong ReadData(ref ReadContext context) { return context.Reader.ReadVarUInt64(); } } -public sealed class Float32Serializer : Serializer +public readonly struct Float32Serializer : IStaticSerializer { - public static Float32Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Float32; - public override float DefaultValue => 0; - public override void WriteData(ref WriteContext context, in float value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.Float32; + public static float DefaultValue => 0; + public static void WriteData(ref WriteContext context, in float value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteFloat32(value); } - public override float ReadData(ref ReadContext context) + public static float ReadData(ref ReadContext context) { return context.Reader.ReadFloat32(); } } -public sealed class Float64Serializer : Serializer +public readonly struct Float64Serializer : IStaticSerializer { - public static Float64Serializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Float64; - public override double DefaultValue => 0; - public override void WriteData(ref WriteContext context, in double value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.Float64; + public static double DefaultValue => 0; + public static void WriteData(ref WriteContext context, in double value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteFloat64(value); } - public override double ReadData(ref ReadContext context) + public static double ReadData(ref ReadContext context) { return context.Reader.ReadFloat64(); } } -public sealed class StringSerializer : Serializer +public readonly struct StringSerializer : IStaticSerializer { - public static StringSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.String; - public override bool IsNullableType => true; - public override string DefaultValue => null!; - public override bool IsNone(string value) => value is null; + public static ForyTypeId StaticTypeId => ForyTypeId.String; + public static bool IsNullableType => true; + public static string DefaultValue => null!; + public static bool IsNone(in string value) => value is null; - public override void WriteData(ref WriteContext context, in string value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in string value, bool hasGenerics) { _ = hasGenerics; string safe = value ?? string.Empty; @@ -273,7 +261,7 @@ public override void WriteData(ref WriteContext context, in string value, bool h context.Writer.WriteBytes(utf8); } - public override string ReadData(ref ReadContext context) + public static string ReadData(ref ReadContext context) { ulong header = context.Reader.ReadVarUInt36Small(); ulong encoding = header & 0x03; @@ -310,15 +298,14 @@ private static string DecodeUtf16(byte[] bytes) } } -public sealed class BinarySerializer : Serializer +public readonly struct BinarySerializer : IStaticSerializer { - public static BinarySerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Binary; - public override bool IsNullableType => true; - public override byte[] DefaultValue => null!; - public override bool IsNone(byte[] value) => value is null; + public static ForyTypeId StaticTypeId => ForyTypeId.Binary; + public static bool IsNullableType => true; + public static byte[] DefaultValue => null!; + public static bool IsNone(in byte[] value) => value is null; - public override void WriteData(ref WriteContext context, in byte[] value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in byte[] value, bool hasGenerics) { _ = hasGenerics; byte[] safe = value ?? []; @@ -326,143 +313,135 @@ public override void WriteData(ref WriteContext context, in byte[] value, bool h context.Writer.WriteBytes(safe); } - public override byte[] ReadData(ref ReadContext context) + public static byte[] ReadData(ref ReadContext context) { uint length = context.Reader.ReadVarUInt32(); return context.Reader.ReadBytes(checked((int)length)); } } -public sealed class ForyInt32FixedSerializer : Serializer +public readonly struct ForyInt32FixedSerializer : IStaticSerializer { - public static ForyInt32FixedSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Int32; - public override ForyInt32Fixed DefaultValue => new(0); - public override void WriteData(ref WriteContext context, in ForyInt32Fixed value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.Int32; + public static ForyInt32Fixed DefaultValue => new(0); + public static void WriteData(ref WriteContext context, in ForyInt32Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt32(value.RawValue); } - public override ForyInt32Fixed ReadData(ref ReadContext context) + public static ForyInt32Fixed ReadData(ref ReadContext context) { return new ForyInt32Fixed(context.Reader.ReadInt32()); } } -public sealed class ForyInt64FixedSerializer : Serializer +public readonly struct ForyInt64FixedSerializer : IStaticSerializer { - public static ForyInt64FixedSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Int64; - public override ForyInt64Fixed DefaultValue => new(0); - public override void WriteData(ref WriteContext context, in ForyInt64Fixed value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.Int64; + public static ForyInt64Fixed DefaultValue => new(0); + public static void WriteData(ref WriteContext context, in ForyInt64Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt64(value.RawValue); } - public override ForyInt64Fixed ReadData(ref ReadContext context) + public static ForyInt64Fixed ReadData(ref ReadContext context) { return new ForyInt64Fixed(context.Reader.ReadInt64()); } } -public sealed class ForyInt64TaggedSerializer : Serializer +public readonly struct ForyInt64TaggedSerializer : IStaticSerializer { - public static ForyInt64TaggedSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.TaggedInt64; - public override ForyInt64Tagged DefaultValue => new(0); - public override void WriteData(ref WriteContext context, in ForyInt64Tagged value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.TaggedInt64; + public static ForyInt64Tagged DefaultValue => new(0); + public static void WriteData(ref WriteContext context, in ForyInt64Tagged value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteTaggedInt64(value.RawValue); } - public override ForyInt64Tagged ReadData(ref ReadContext context) + public static ForyInt64Tagged ReadData(ref ReadContext context) { return new ForyInt64Tagged(context.Reader.ReadTaggedInt64()); } } -public sealed class ForyUInt32FixedSerializer : Serializer +public readonly struct ForyUInt32FixedSerializer : IStaticSerializer { - public static ForyUInt32FixedSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.UInt32; - public override ForyUInt32Fixed DefaultValue => new(0); - public override void WriteData(ref WriteContext context, in ForyUInt32Fixed value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.UInt32; + public static ForyUInt32Fixed DefaultValue => new(0); + public static void WriteData(ref WriteContext context, in ForyUInt32Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt32(value.RawValue); } - public override ForyUInt32Fixed ReadData(ref ReadContext context) + public static ForyUInt32Fixed ReadData(ref ReadContext context) { return new ForyUInt32Fixed(context.Reader.ReadUInt32()); } } -public sealed class ForyUInt64FixedSerializer : Serializer +public readonly struct ForyUInt64FixedSerializer : IStaticSerializer { - public static ForyUInt64FixedSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.UInt64; - public override ForyUInt64Fixed DefaultValue => new(0); - public override void WriteData(ref WriteContext context, in ForyUInt64Fixed value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.UInt64; + public static ForyUInt64Fixed DefaultValue => new(0); + public static void WriteData(ref WriteContext context, in ForyUInt64Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt64(value.RawValue); } - public override ForyUInt64Fixed ReadData(ref ReadContext context) + public static ForyUInt64Fixed ReadData(ref ReadContext context) { return new ForyUInt64Fixed(context.Reader.ReadUInt64()); } } -public sealed class ForyUInt64TaggedSerializer : Serializer +public readonly struct ForyUInt64TaggedSerializer : IStaticSerializer { - public static ForyUInt64TaggedSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.TaggedUInt64; - public override ForyUInt64Tagged DefaultValue => new(0); - public override void WriteData(ref WriteContext context, in ForyUInt64Tagged value, bool hasGenerics) + public static ForyTypeId StaticTypeId => ForyTypeId.TaggedUInt64; + public static ForyUInt64Tagged DefaultValue => new(0); + public static void WriteData(ref WriteContext context, in ForyUInt64Tagged value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteTaggedUInt64(value.RawValue); } - public override ForyUInt64Tagged ReadData(ref ReadContext context) + public static ForyUInt64Tagged ReadData(ref ReadContext context) { return new ForyUInt64Tagged(context.Reader.ReadTaggedUInt64()); } } -public sealed class DateOnlySerializer : Serializer +public readonly struct DateOnlySerializer : IStaticSerializer { private static readonly DateOnly Epoch = new(1970, 1, 1); - public static DateOnlySerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Date; - public override DateOnly DefaultValue => Epoch; + public static ForyTypeId StaticTypeId => ForyTypeId.Date; + public static DateOnly DefaultValue => Epoch; - public override void WriteData(ref WriteContext context, in DateOnly value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in DateOnly value, bool hasGenerics) { _ = hasGenerics; int days = value.DayNumber - Epoch.DayNumber; context.Writer.WriteInt32(days); } - public override DateOnly ReadData(ref ReadContext context) + public static DateOnly ReadData(ref ReadContext context) { int days = context.Reader.ReadInt32(); return DateOnly.FromDayNumber(Epoch.DayNumber + days); } } -public sealed class DateTimeOffsetSerializer : Serializer +public readonly struct DateTimeOffsetSerializer : IStaticSerializer { - public static DateTimeOffsetSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Timestamp; - public override DateTimeOffset DefaultValue => DateTimeOffset.UnixEpoch; + public static ForyTypeId StaticTypeId => ForyTypeId.Timestamp; + public static DateTimeOffset DefaultValue => DateTimeOffset.UnixEpoch; - public override void WriteData(ref WriteContext context, in DateTimeOffset value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in DateTimeOffset value, bool hasGenerics) { _ = hasGenerics; ForyTimestamp ts = ForyTimestamp.FromDateTimeOffset(value); @@ -470,7 +449,7 @@ public override void WriteData(ref WriteContext context, in DateTimeOffset value context.Writer.WriteUInt32(ts.Nanos); } - public override DateTimeOffset ReadData(ref ReadContext context) + public static DateTimeOffset ReadData(ref ReadContext context) { long seconds = context.Reader.ReadInt64(); uint nanos = context.Reader.ReadUInt32(); @@ -478,13 +457,12 @@ public override DateTimeOffset ReadData(ref ReadContext context) } } -public sealed class DateTimeSerializer : Serializer +public readonly struct DateTimeSerializer : IStaticSerializer { - public static DateTimeSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Timestamp; - public override DateTime DefaultValue => DateTime.UnixEpoch; + public static ForyTypeId StaticTypeId => ForyTypeId.Timestamp; + public static DateTime DefaultValue => DateTime.UnixEpoch; - public override void WriteData(ref WriteContext context, in DateTime value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in DateTime value, bool hasGenerics) { _ = hasGenerics; DateTimeOffset dto = value.Kind switch @@ -498,7 +476,7 @@ public override void WriteData(ref WriteContext context, in DateTime value, bool context.Writer.WriteUInt32(ts.Nanos); } - public override DateTime ReadData(ref ReadContext context) + public static DateTime ReadData(ref ReadContext context) { long seconds = context.Reader.ReadInt64(); uint nanos = context.Reader.ReadUInt32(); @@ -506,13 +484,12 @@ public override DateTime ReadData(ref ReadContext context) } } -public sealed class TimeSpanSerializer : Serializer +public readonly struct TimeSpanSerializer : IStaticSerializer { - public static TimeSpanSerializer Instance { get; } = new(); - public override ForyTypeId StaticTypeId => ForyTypeId.Duration; - public override TimeSpan DefaultValue => TimeSpan.Zero; + public static ForyTypeId StaticTypeId => ForyTypeId.Duration; + public static TimeSpan DefaultValue => TimeSpan.Zero; - public override void WriteData(ref WriteContext context, in TimeSpan value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in TimeSpan value, bool hasGenerics) { _ = hasGenerics; long seconds = value.Ticks / TimeSpan.TicksPerSecond; @@ -521,7 +498,7 @@ public override void WriteData(ref WriteContext context, in TimeSpan value, bool context.Writer.WriteInt32(nanos); } - public override TimeSpan ReadData(ref ReadContext context) + public static TimeSpan ReadData(ref ReadContext context) { long seconds = context.Reader.ReadInt64(); int nanos = context.Reader.ReadInt32(); diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs index 1e4dca8474..2f998a8d35 100644 --- a/csharp/src/Fory/Serializer.cs +++ b/csharp/src/Fory/Serializer.cs @@ -17,87 +17,50 @@ namespace Apache.Fory; -public interface ISerializer +public interface IStaticSerializer + where TSerializer : IStaticSerializer { - Type Type { get; } + static abstract ForyTypeId StaticTypeId { get; } - ForyTypeId StaticTypeId { get; } + static virtual bool IsNullableType => false; - bool IsNullableType { get; } + static virtual bool IsReferenceTrackableType => false; - bool IsReferenceTrackableType { get; } + static virtual T DefaultValue => default!; - object? DefaultObject { get; } - - bool IsNoneObject(object? value); - - void WriteData(ref WriteContext context, object? value, bool hasGenerics); - - object? ReadData(ref ReadContext context); - - void Write(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics); - - object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo); - - void WriteTypeInfo(ref WriteContext context); - - void ReadTypeInfo(ref ReadContext context); - - IReadOnlyList CompatibleTypeMetaFields(bool trackRef); -} - -public abstract class Serializer : ISerializer -{ - public Type Type => typeof(T); - - public abstract ForyTypeId StaticTypeId { get; } - - public virtual bool IsNullableType => false; - - public virtual bool IsReferenceTrackableType => false; - - public virtual T DefaultValue => default!; - - public virtual bool IsNone(T value) => false; - - public object? DefaultObject => DefaultValue; - - public bool IsNoneObject(object? value) + static virtual bool IsNone(in T value) { - if (value is null) - { - return IsNullableType; - } - - return value is T typed && IsNone(typed); + _ = value; + return false; } - public abstract void WriteData(ref WriteContext context, in T value, bool hasGenerics); + static abstract void WriteData(ref WriteContext context, in T value, bool hasGenerics); - public abstract T ReadData(ref ReadContext context); + static abstract T ReadData(ref ReadContext context); - public virtual IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + static virtual IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { + _ = trackRef; return []; } - public virtual void WriteTypeInfo(ref WriteContext context) + static virtual void WriteTypeInfo(ref WriteContext context) { - SerializerTypeInfo.WriteTypeInfo(this, ref context); + SerializerTypeInfo.WriteTypeInfo(ref context); } - public virtual void ReadTypeInfo(ref ReadContext context) + static virtual void ReadTypeInfo(ref ReadContext context) { - SerializerTypeInfo.ReadTypeInfo(this, ref context); + SerializerTypeInfo.ReadTypeInfo(ref context); } - public virtual void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + static virtual void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { if (refMode != RefMode.None) { bool wroteTrackingRefFlag = false; if (refMode == RefMode.Tracking && - IsReferenceTrackableType && + TSerializer.IsReferenceTrackableType && value is object obj) { if (context.RefWriter.TryWriteReference(context.Writer, obj)) @@ -110,7 +73,7 @@ public virtual void Write(ref WriteContext context, in T value, RefMode refMode, if (!wroteTrackingRefFlag) { - if (IsNullableType && IsNone(value)) + if (TSerializer.IsNullableType && TSerializer.IsNone(value)) { context.Writer.WriteInt8((sbyte)RefFlag.Null); return; @@ -122,13 +85,13 @@ public virtual void Write(ref WriteContext context, in T value, RefMode refMode, if (writeTypeInfo) { - WriteTypeInfo(ref context); + TSerializer.WriteTypeInfo(ref context); } - WriteData(ref context, value, hasGenerics); + TSerializer.WriteData(ref context, value, hasGenerics); } - public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + static virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { if (refMode != RefMode.None) { @@ -137,7 +100,7 @@ public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInf switch (flag) { case RefFlag.Null: - return DefaultValue; + return TSerializer.DefaultValue; case RefFlag.Ref: uint refId = context.Reader.ReadVarUInt32(); return context.RefReader.ReadRef(refId); @@ -147,10 +110,10 @@ public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInf context.PushPendingReference(reservedRefId); if (readTypeInfo) { - ReadTypeInfo(ref context); + TSerializer.ReadTypeInfo(ref context); } - T value = ReadData(ref context); + T value = TSerializer.ReadData(ref context); context.FinishPendingReferenceIfNeeded(value); context.PopPendingReference(); return value; @@ -164,82 +127,112 @@ public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInf if (readTypeInfo) { - ReadTypeInfo(ref context); + TSerializer.ReadTypeInfo(ref context); } - return ReadData(ref context); + return TSerializer.ReadData(ref context); } +} - object? ISerializer.DefaultObject => DefaultValue; +public readonly struct Serializer +{ + private readonly TypedSerializerBinding? _binding; - bool ISerializer.IsNoneObject(object? value) + internal Serializer(TypedSerializerBinding binding) { - if (value is null) + _binding = binding; + } + + private TypedSerializerBinding Binding + { + get { - return IsNullableType; + if (_binding is null) + { + throw new ForyInvalidDataException($"serializer handle for {typeof(T)} is not initialized"); + } + + return _binding; } + } - return value is T typed && IsNone(typed); + public Type Type => typeof(T); + + public ForyTypeId StaticTypeId => Binding.StaticTypeId; + + public bool IsNullableType => Binding.IsNullableType; + + public bool IsReferenceTrackableType => Binding.IsReferenceTrackableType; + + public T DefaultValue => Binding.DefaultValue; + + public object? DefaultObject => Binding.DefaultValue; + + public bool IsNone(in T value) + { + return Binding.IsNone(value); } - void ISerializer.WriteData(ref WriteContext context, object? value, bool hasGenerics) + public bool IsNoneObject(object? value) { - if (value is T typed) + if (value is null) { - WriteData(ref context, typed, hasGenerics); - return; + return IsNullableType; } - if (value is null && IsNullableType) - { - WriteData(ref context, DefaultValue, hasGenerics); - return; - } + return value is T typed && IsNone(typed); + } - throw new ForyInvalidDataException( - $"serializer {GetType().Name} expected value of type {typeof(T)}, got {value?.GetType()}"); + public void WriteData(ref WriteContext context, in T value, bool hasGenerics) + { + Binding.WriteData(ref context, value, hasGenerics); } - object? ISerializer.ReadData(ref ReadContext context) + public T ReadData(ref ReadContext context) { - return ReadData(ref context); + return Binding.ReadData(ref context); } - void ISerializer.Write(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { - if (value is T typed) - { - Write(ref context, typed, refMode, writeTypeInfo, hasGenerics); - return; - } + Binding.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + } - if (value is null && IsNullableType) - { - Write(ref context, DefaultValue, refMode, writeTypeInfo, hasGenerics); - return; - } + public T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + return Binding.Read(ref context, refMode, readTypeInfo); + } - throw new ForyInvalidDataException( - $"serializer {GetType().Name} expected value of type {typeof(T)}, got {value?.GetType()}"); + public void WriteTypeInfo(ref WriteContext context) + { + Binding.WriteTypeInfo(ref context); + } + + public void ReadTypeInfo(ref ReadContext context) + { + Binding.ReadTypeInfo(ref context); } - object? ISerializer.Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { - return Read(ref context, refMode, readTypeInfo); + return Binding.CompatibleTypeMetaFields(trackRef); } } internal static class SerializerTypeInfo { - public static void WriteTypeInfo(ISerializer serializer, ref WriteContext context) + public static void WriteTypeInfo(ref WriteContext context) + where TSerializer : IStaticSerializer { - if (!serializer.StaticTypeId.IsUserTypeKind()) + ForyTypeId staticTypeId = TSerializer.StaticTypeId; + if (!staticTypeId.IsUserTypeKind()) { - context.Writer.WriteUInt8((byte)serializer.StaticTypeId); + context.Writer.WriteUInt8((byte)staticTypeId); return; } - RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(serializer.Type); + Type type = typeof(T); + RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); ForyTypeId wireTypeId = ResolveWireTypeId(info.Kind, info.RegisterByName, context.Compatible); context.Writer.WriteUInt8((byte)wireTypeId); switch (wireTypeId) @@ -247,8 +240,8 @@ public static void WriteTypeInfo(ISerializer serializer, ref WriteContext contex case ForyTypeId.CompatibleStruct: case ForyTypeId.NamedCompatibleStruct: { - TypeMeta typeMeta = BuildCompatibleTypeMeta(serializer, info, wireTypeId, context.TrackRef); - context.WriteCompatibleTypeMeta(serializer.Type, typeMeta); + TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); + context.WriteCompatibleTypeMeta(type, typeMeta); return; } case ForyTypeId.NamedEnum: @@ -258,8 +251,8 @@ public static void WriteTypeInfo(ISerializer serializer, ref WriteContext contex { if (context.Compatible) { - TypeMeta typeMeta = BuildCompatibleTypeMeta(serializer, info, wireTypeId, context.TrackRef); - context.WriteCompatibleTypeMeta(serializer.Type, typeMeta); + TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); + context.WriteCompatibleTypeMeta(type, typeMeta); } else { @@ -297,7 +290,8 @@ public static void WriteTypeInfo(ISerializer serializer, ref WriteContext contex } } - public static void ReadTypeInfo(ISerializer serializer, ref ReadContext context) + public static void ReadTypeInfo(ref ReadContext context) + where TSerializer : IStaticSerializer { uint rawTypeId = context.Reader.ReadVarUInt32(); if (!Enum.IsDefined(typeof(ForyTypeId), rawTypeId)) @@ -306,17 +300,19 @@ public static void ReadTypeInfo(ISerializer serializer, ref ReadContext context) } ForyTypeId typeId = (ForyTypeId)rawTypeId; - if (!serializer.StaticTypeId.IsUserTypeKind()) + ForyTypeId staticTypeId = TSerializer.StaticTypeId; + if (!staticTypeId.IsUserTypeKind()) { - if (typeId != serializer.StaticTypeId) + if (typeId != staticTypeId) { - throw new ForyTypeMismatchException((uint)serializer.StaticTypeId, rawTypeId); + throw new ForyTypeMismatchException((uint)staticTypeId, rawTypeId); } return; } - RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(serializer.Type); + Type type = typeof(T); + RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); HashSet allowed = AllowedWireTypeIds(info.Kind, info.RegisterByName, context.Compatible); if (!allowed.Contains(typeId)) { @@ -331,7 +327,7 @@ public static void ReadTypeInfo(ISerializer serializer, ref ReadContext context) { TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); - context.PushCompatibleTypeMeta(serializer.Type, remoteTypeMeta); + context.PushCompatibleTypeMeta(type, remoteTypeMeta); return; } case ForyTypeId.NamedEnum: @@ -345,7 +341,7 @@ public static void ReadTypeInfo(ISerializer serializer, ref ReadContext context) ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); if (typeId == ForyTypeId.NamedStruct) { - context.PushCompatibleTypeMeta(serializer.Type, remoteTypeMeta); + context.PushCompatibleTypeMeta(type, remoteTypeMeta); } } else @@ -456,13 +452,13 @@ private static bool WireTypeNeedsUserTypeId(ForyTypeId typeId) return typeId is ForyTypeId.Enum or ForyTypeId.Struct or ForyTypeId.Ext or ForyTypeId.TypedUnion; } - private static TypeMeta BuildCompatibleTypeMeta( - ISerializer serializer, + private static TypeMeta BuildCompatibleTypeMeta( RegisteredTypeInfo info, ForyTypeId wireTypeId, bool trackRef) + where TSerializer : IStaticSerializer { - IReadOnlyList fields = serializer.CompatibleTypeMetaFields(trackRef); + IReadOnlyList fields = TSerializer.CompatibleTypeMetaFields(trackRef); bool hasFieldsMeta = fields.Count > 0; if (info.RegisterByName) { diff --git a/csharp/src/Fory/SerializerRegistry.cs b/csharp/src/Fory/SerializerRegistry.cs index b0b0769428..66c9d2c77a 100644 --- a/csharp/src/Fory/SerializerRegistry.cs +++ b/csharp/src/Fory/SerializerRegistry.cs @@ -16,13 +16,15 @@ // under the License. using System.Collections.Concurrent; +using System.Threading; namespace Apache.Fory; public static class SerializerRegistry { - private static readonly ConcurrentDictionary Cache = new(); - private static readonly ConcurrentDictionary> GeneratedFactories = new(); + private static readonly ConcurrentDictionary Cache = new(); + private static readonly ConcurrentDictionary> GeneratedFactories = new(); + private static int _version; static SerializerRegistry() { @@ -30,182 +32,188 @@ static SerializerRegistry() GeneratedSerializerRegistry.Register(GeneratedFactories); } - public static void RegisterGenerated(Func> factory) + public static void RegisterGenerated() + where TSerializer : IStaticSerializer { - GeneratedFactories[typeof(T)] = () => factory(); + GeneratedFactories[typeof(T)] = StaticSerializerBindingFactory.Create; Cache.TryRemove(typeof(T), out _); + Interlocked.Increment(ref _version); } - public static void RegisterCustom(Type type, ISerializer serializer) + public static void RegisterCustom() + where TSerializer : IStaticSerializer { - Cache[type] = serializer; + Cache[typeof(T)] = StaticSerializerBindingFactory.Create(); + Interlocked.Increment(ref _version); } - public static Serializer Get() + internal static void RegisterCustom(Type type, SerializerBinding serializerBinding) { - ISerializer serializer = Get(typeof(T)); - if (serializer is Serializer typed) - { - return typed; - } + Cache[type] = serializerBinding; + Interlocked.Increment(ref _version); + } - throw new ForyInvalidDataException($"serializer type mismatch for {typeof(T)}"); + internal static int Version => Volatile.Read(ref _version); + + public static Serializer Get() + { + return TypedSerializerBindingCache.Get(); } - public static ISerializer Get(Type type) + internal static SerializerBinding GetBinding(Type type) { return Cache.GetOrAdd(type, Create); } - private static ISerializer Create(Type type) + private static SerializerBinding Create(Type type) { - if (GeneratedFactories.TryGetValue(type, out Func? generatedFactory)) + if (GeneratedFactories.TryGetValue(type, out Func? generatedFactory)) { return generatedFactory(); } if (type == typeof(bool)) { - return BoolSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(sbyte)) { - return Int8Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(short)) { - return Int16Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(int)) { - return Int32Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(long)) { - return Int64Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(byte)) { - return UInt8Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ushort)) { - return UInt16Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(uint)) { - return UInt32Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ulong)) { - return UInt64Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(float)) { - return Float32Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(double)) { - return Float64Serializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(string)) { - return StringSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(byte[])) { - return BinarySerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(DateOnly)) { - return DateOnlySerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(DateTimeOffset)) { - return DateTimeOffsetSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(DateTime)) { - return DateTimeSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(TimeSpan)) { - return TimeSpanSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ForyInt32Fixed)) { - return ForyInt32FixedSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ForyInt64Fixed)) { - return ForyInt64FixedSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ForyInt64Tagged)) { - return ForyInt64TaggedSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ForyUInt32Fixed)) { - return ForyUInt32FixedSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ForyUInt64Fixed)) { - return ForyUInt64FixedSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ForyUInt64Tagged)) { - return ForyUInt64TaggedSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(ForyAnyNullValue)) { - return ForyAnyNullValueSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (type == typeof(object)) { - return DynamicAnyObjectSerializer.Instance; + return StaticSerializerBindingFactory.Create(); } if (typeof(Union).IsAssignableFrom(type)) { Type serializerType = typeof(UnionSerializer<>).MakeGenericType(type); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } if (type.IsEnum) { Type serializerType = typeof(EnumSerializer<>).MakeGenericType(type); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } if (type.IsArray) { Type elementType = type.GetElementType()!; Type serializerType = typeof(ArraySerializer<>).MakeGenericType(elementType); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } if (type.IsGenericType) @@ -215,35 +223,35 @@ private static ISerializer Create(Type type) if (genericType == typeof(Nullable<>)) { Type serializerType = typeof(NullableSerializer<>).MakeGenericType(genericArgs[0]); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } if (genericType == typeof(List<>)) { Type serializerType = typeof(ListSerializer<>).MakeGenericType(genericArgs[0]); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } if (genericType == typeof(HashSet<>)) { Type serializerType = typeof(SetSerializer<>).MakeGenericType(genericArgs[0]); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } if (genericType == typeof(Dictionary<,>)) { Type serializerType = typeof(MapSerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } if (genericType == typeof(ForyMap<,>)) { Type serializerType = typeof(ForyMapSerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return (ISerializer)Activator.CreateInstance(serializerType)!; + return StaticSerializerBindingFactory.Create(type, serializerType); } } - if (GeneratedSerializerRegistry.TryCreate(type, out ISerializer? generatedSerializer)) + if (GeneratedSerializerRegistry.TryCreate(type, out SerializerBinding? generatedSerializer)) { return generatedSerializer!; } @@ -253,33 +261,33 @@ private static ISerializer Create(Type type) private static void RegisterBuiltins() { - Cache[typeof(bool)] = BoolSerializer.Instance; - Cache[typeof(sbyte)] = Int8Serializer.Instance; - Cache[typeof(short)] = Int16Serializer.Instance; - Cache[typeof(int)] = Int32Serializer.Instance; - Cache[typeof(long)] = Int64Serializer.Instance; - Cache[typeof(byte)] = UInt8Serializer.Instance; - Cache[typeof(ushort)] = UInt16Serializer.Instance; - Cache[typeof(uint)] = UInt32Serializer.Instance; - Cache[typeof(ulong)] = UInt64Serializer.Instance; - Cache[typeof(float)] = Float32Serializer.Instance; - Cache[typeof(double)] = Float64Serializer.Instance; - Cache[typeof(string)] = StringSerializer.Instance; - Cache[typeof(byte[])] = BinarySerializer.Instance; - Cache[typeof(object)] = DynamicAnyObjectSerializer.Instance; - Cache[typeof(ForyAnyNullValue)] = ForyAnyNullValueSerializer.Instance; - Cache[typeof(Union)] = new UnionSerializer(); + Cache[typeof(bool)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(sbyte)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(short)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(int)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(long)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(byte)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(ushort)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(uint)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(ulong)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(float)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(double)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(string)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(byte[])] = StaticSerializerBindingFactory.Create(); + Cache[typeof(object)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(ForyAnyNullValue)] = StaticSerializerBindingFactory.Create(); + Cache[typeof(Union)] = StaticSerializerBindingFactory.Create>(); } } internal static partial class GeneratedSerializerRegistry { - public static void Register(ConcurrentDictionary> factories) + public static void Register(ConcurrentDictionary> factories) { _ = factories; } - public static bool TryCreate(Type type, out ISerializer? serializer) + public static bool TryCreate(Type type, out SerializerBinding? serializer) { _ = type; serializer = null; diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index 8e3a33f58e..ef23942a1b 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -17,13 +17,13 @@ namespace Apache.Fory; -public sealed record RegisteredTypeInfo( +internal sealed record RegisteredTypeInfo( uint? UserTypeId, ForyTypeId Kind, bool RegisterByName, MetaString? NamespaceName, MetaString TypeName, - ISerializer Serializer); + SerializerBinding Serializer); internal enum DynamicRegistrationMode { @@ -48,9 +48,9 @@ public sealed class TypeResolver private readonly Dictionary _byTypeName = []; private readonly Dictionary _registrationModeByKind = []; - public void Register(Type type, uint id, ISerializer? explicitSerializer = null) + internal void Register(Type type, uint id, SerializerBinding? explicitSerializer = null) { - ISerializer serializer = explicitSerializer ?? SerializerRegistry.Get(type); + SerializerBinding serializer = explicitSerializer ?? SerializerRegistry.GetBinding(type); RegisteredTypeInfo info = new( id, serializer.StaticTypeId, @@ -72,9 +72,9 @@ public void Register(Type type, uint id, ISerializer? explicitSerializer = null) }; } - public void Register(Type type, string namespaceName, string typeName, ISerializer? explicitSerializer = null) + internal void Register(Type type, string namespaceName, string typeName, SerializerBinding? explicitSerializer = null) { - ISerializer serializer = explicitSerializer ?? SerializerRegistry.Get(type); + SerializerBinding serializer = explicitSerializer ?? SerializerRegistry.GetBinding(type); MetaString namespaceMeta = MetaStringEncoder.Namespace.Encode(namespaceName, TypeMetaEncodings.NamespaceMetaStringEncodings); MetaString typeNameMeta = MetaStringEncoder.TypeName.Encode(typeName, TypeMetaEncodings.TypeNameMetaStringEncodings); RegisteredTypeInfo info = new( @@ -98,12 +98,12 @@ public void Register(Type type, string namespaceName, string typeName, ISerializ }; } - public RegisteredTypeInfo? RegisteredTypeInfo(Type type) + internal RegisteredTypeInfo? RegisteredTypeInfo(Type type) { return _byType.TryGetValue(type, out RegisteredTypeInfo? info) ? info : null; } - public RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) + internal RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) { if (_byType.TryGetValue(type, out RegisteredTypeInfo? info)) { @@ -198,64 +198,64 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) switch (typeInfo.WireTypeId) { case ForyTypeId.Bool: - return BoolSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int8: - return Int8Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int16: - return Int16Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int32: - return ForyInt32FixedSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.VarInt32: - return Int32Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int64: - return ForyInt64FixedSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.VarInt64: - return Int64Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.TaggedInt64: - return ForyInt64TaggedSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.UInt8: - return UInt8Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.UInt16: - return UInt16Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.UInt32: - return ForyUInt32FixedSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.VarUInt32: - return UInt32Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.UInt64: - return ForyUInt64FixedSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.VarUInt64: - return UInt64Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.TaggedUInt64: - return ForyUInt64TaggedSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Float32: - return Float32Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Float64: - return Float64Serializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.String: - return StringSerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Binary: case ForyTypeId.UInt8Array: - return BinarySerializer.Instance.Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.BoolArray: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int8Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int16Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int32Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Int64Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.UInt16Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.UInt32Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.UInt64Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Float32Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Float64Array: - return new ArraySerializer().Read(ref context, RefMode.None, false); + return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.List: return DynamicAnyCodec.ReadAnyList(ref context, RefMode.None, false) ?? []; case ForyTypeId.Map: diff --git a/csharp/src/Fory/TypedSerializerBinding.cs b/csharp/src/Fory/TypedSerializerBinding.cs new file mode 100644 index 0000000000..096e3bf5b5 --- /dev/null +++ b/csharp/src/Fory/TypedSerializerBinding.cs @@ -0,0 +1,446 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Reflection; +using System.Threading; + +namespace Apache.Fory; + +internal delegate bool TypedIsNoneDelegate(in T value); + +internal delegate void TypedWriteDataDelegate( + ref WriteContext context, + in T value, + bool hasGenerics); + +internal delegate T TypedReadDataDelegate(ref ReadContext context); + +internal delegate void TypedWriteDelegate( + ref WriteContext context, + in T value, + RefMode refMode, + bool writeTypeInfo, + bool hasGenerics); + +internal delegate T TypedReadDelegate( + ref ReadContext context, + RefMode refMode, + bool readTypeInfo); + +internal delegate void TypeInfoWriteDelegate(ref WriteContext context); + +internal delegate void TypeInfoReadDelegate(ref ReadContext context); + +internal delegate IReadOnlyList CompatibleTypeMetaFieldsDelegate(bool trackRef); + +internal delegate bool UntypedIsNoneObjectDelegate(object? value); + +internal delegate void UntypedWriteDataDelegate(ref WriteContext context, object? value, bool hasGenerics); + +internal delegate object? UntypedReadDataDelegate(ref ReadContext context); + +internal delegate void UntypedWriteDelegate( + ref WriteContext context, + object? value, + RefMode refMode, + bool writeTypeInfo, + bool hasGenerics); + +internal delegate object? UntypedReadDelegate( + ref ReadContext context, + RefMode refMode, + bool readTypeInfo); + +internal sealed class TypedSerializerBinding +{ + public TypedSerializerBinding( + ForyTypeId staticTypeId, + bool isNullableType, + bool isReferenceTrackableType, + T defaultValue, + TypedIsNoneDelegate isNone, + TypedWriteDataDelegate writeData, + TypedReadDataDelegate readData, + TypedWriteDelegate write, + TypedReadDelegate read, + TypeInfoWriteDelegate writeTypeInfo, + TypeInfoReadDelegate readTypeInfo, + CompatibleTypeMetaFieldsDelegate compatibleTypeMetaFields) + { + StaticTypeId = staticTypeId; + IsNullableType = isNullableType; + IsReferenceTrackableType = isReferenceTrackableType; + DefaultValue = defaultValue; + _isNone = isNone; + _writeData = writeData; + _readData = readData; + _write = write; + _read = read; + _writeTypeInfo = writeTypeInfo; + _readTypeInfo = readTypeInfo; + _compatibleTypeMetaFields = compatibleTypeMetaFields; + } + + public ForyTypeId StaticTypeId { get; } + + public bool IsNullableType { get; } + + public bool IsReferenceTrackableType { get; } + + public T DefaultValue { get; } + + private readonly TypedIsNoneDelegate _isNone; + private readonly TypedWriteDataDelegate _writeData; + private readonly TypedReadDataDelegate _readData; + private readonly TypedWriteDelegate _write; + private readonly TypedReadDelegate _read; + private readonly TypeInfoWriteDelegate _writeTypeInfo; + private readonly TypeInfoReadDelegate _readTypeInfo; + private readonly CompatibleTypeMetaFieldsDelegate _compatibleTypeMetaFields; + + public bool IsNone(in T value) + { + return _isNone(value); + } + + public void WriteData(ref WriteContext context, in T value, bool hasGenerics) + { + _writeData(ref context, value, hasGenerics); + } + + public T ReadData(ref ReadContext context) + { + return _readData(ref context); + } + + public void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + _write(ref context, value, refMode, writeTypeInfo, hasGenerics); + } + + public T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + return _read(ref context, refMode, readTypeInfo); + } + + public void WriteTypeInfo(ref WriteContext context) + { + _writeTypeInfo(ref context); + } + + public void ReadTypeInfo(ref ReadContext context) + { + _readTypeInfo(ref context); + } + + public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + { + return _compatibleTypeMetaFields(trackRef); + } +} + +internal sealed class SerializerBinding +{ + public SerializerBinding( + Type type, + ForyTypeId staticTypeId, + bool isNullableType, + bool isReferenceTrackableType, + object? defaultObject, + UntypedIsNoneObjectDelegate isNoneObject, + UntypedWriteDataDelegate writeData, + UntypedReadDataDelegate readData, + UntypedWriteDelegate write, + UntypedReadDelegate read, + TypeInfoWriteDelegate writeTypeInfo, + TypeInfoReadDelegate readTypeInfo, + CompatibleTypeMetaFieldsDelegate compatibleTypeMetaFields, + object typedBinding) + { + Type = type; + StaticTypeId = staticTypeId; + IsNullableType = isNullableType; + IsReferenceTrackableType = isReferenceTrackableType; + DefaultObject = defaultObject; + _isNoneObject = isNoneObject; + _writeData = writeData; + _readData = readData; + _write = write; + _read = read; + _writeTypeInfo = writeTypeInfo; + _readTypeInfo = readTypeInfo; + _compatibleTypeMetaFields = compatibleTypeMetaFields; + _typedBinding = typedBinding; + } + + public Type Type { get; } + + public ForyTypeId StaticTypeId { get; } + + public bool IsNullableType { get; } + + public bool IsReferenceTrackableType { get; } + + public object? DefaultObject { get; } + + private readonly UntypedIsNoneObjectDelegate _isNoneObject; + private readonly UntypedWriteDataDelegate _writeData; + private readonly UntypedReadDataDelegate _readData; + private readonly UntypedWriteDelegate _write; + private readonly UntypedReadDelegate _read; + private readonly TypeInfoWriteDelegate _writeTypeInfo; + private readonly TypeInfoReadDelegate _readTypeInfo; + private readonly CompatibleTypeMetaFieldsDelegate _compatibleTypeMetaFields; + private readonly object _typedBinding; + + public bool IsNoneObject(object? value) + { + return _isNoneObject(value); + } + + public void WriteData(ref WriteContext context, object? value, bool hasGenerics) + { + _writeData(ref context, value, hasGenerics); + } + + public object? ReadData(ref ReadContext context) + { + return _readData(ref context); + } + + public void Write(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + _write(ref context, value, refMode, writeTypeInfo, hasGenerics); + } + + public object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + return _read(ref context, refMode, readTypeInfo); + } + + public void WriteTypeInfo(ref WriteContext context) + { + _writeTypeInfo(ref context); + } + + public void ReadTypeInfo(ref ReadContext context) + { + _readTypeInfo(ref context); + } + + public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + { + return _compatibleTypeMetaFields(trackRef); + } + + public TypedSerializerBinding RequireTypedBinding() + { + if (_typedBinding is TypedSerializerBinding typed) + { + return typed; + } + + throw new ForyInvalidDataException($"serializer type mismatch for {typeof(T)}"); + } +} + +internal static class StaticSerializerDispatch + where TSerializer : IStaticSerializer +{ + private static readonly TypedSerializerBinding TypedBindingValue = new( + TSerializer.StaticTypeId, + TSerializer.IsNullableType, + TSerializer.IsReferenceTrackableType, + TSerializer.DefaultValue, + IsNone, + WriteData, + ReadData, + Write, + Read, + WriteTypeInfo, + ReadTypeInfo, + CompatibleTypeMetaFields); + + public static TypedSerializerBinding TypedBinding => TypedBindingValue; + + public static bool IsNone(in T value) + { + return TSerializer.IsNone(value); + } + + public static bool IsNoneObject(object? value) + { + if (value is null) + { + return TSerializer.IsNullableType; + } + + return value is T typed && TSerializer.IsNone(typed); + } + + public static void WriteData(ref WriteContext context, in T value, bool hasGenerics) + { + TSerializer.WriteData(ref context, value, hasGenerics); + } + + public static T ReadData(ref ReadContext context) + { + return TSerializer.ReadData(ref context); + } + + public static void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + TSerializer.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + } + + public static T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + return TSerializer.Read(ref context, refMode, readTypeInfo); + } + + public static void WriteTypeInfo(ref WriteContext context) + { + TSerializer.WriteTypeInfo(ref context); + } + + public static void ReadTypeInfo(ref ReadContext context) + { + TSerializer.ReadTypeInfo(ref context); + } + + public static IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + { + return TSerializer.CompatibleTypeMetaFields(trackRef); + } + + public static void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics) + { + TSerializer.WriteData(ref context, CoerceValue(value), hasGenerics); + } + + public static object? ReadDataObject(ref ReadContext context) + { + return TSerializer.ReadData(ref context); + } + + public static void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + { + TSerializer.Write(ref context, CoerceValue(value), refMode, writeTypeInfo, hasGenerics); + } + + public static object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + return TSerializer.Read(ref context, refMode, readTypeInfo); + } + + private static T CoerceValue(object? value) + { + if (value is T typed) + { + return typed; + } + + if (value is null && TSerializer.IsNullableType) + { + return TSerializer.DefaultValue; + } + + throw new ForyInvalidDataException( + $"serializer {typeof(TSerializer).Name} expected value of type {typeof(T)}, got {value?.GetType()}"); + } +} + +internal static class StaticSerializerBindingFactory +{ + private static readonly MethodInfo CreateGenericMethod = + typeof(StaticSerializerBindingFactory).GetMethod( + nameof(CreateGeneric), + BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException("missing static serializer binding factory method"); + + public static SerializerBinding Create() + where TSerializer : IStaticSerializer + { + TypedSerializerBinding typed = StaticSerializerDispatch.TypedBinding; + return new SerializerBinding( + typeof(T), + typed.StaticTypeId, + typed.IsNullableType, + typed.IsReferenceTrackableType, + typed.DefaultValue, + StaticSerializerDispatch.IsNoneObject, + StaticSerializerDispatch.WriteDataObject, + StaticSerializerDispatch.ReadDataObject, + StaticSerializerDispatch.WriteObject, + StaticSerializerDispatch.ReadObject, + StaticSerializerDispatch.WriteTypeInfo, + StaticSerializerDispatch.ReadTypeInfo, + StaticSerializerDispatch.CompatibleTypeMetaFields, + typed); + } + + public static SerializerBinding Create(Type valueType, Type serializerType) + { + MethodInfo method = CreateGenericMethod.MakeGenericMethod(valueType, serializerType); + try + { + return (SerializerBinding)method.Invoke(null, null)!; + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + throw ex.InnerException; + } + } + + private static SerializerBinding CreateGeneric() + where TSerializer : IStaticSerializer + { + return Create(); + } +} + +internal static class TypedSerializerBindingCache +{ + private static readonly object Gate = new(); + private static TypedSerializerBinding? _binding; + private static int _bindingVersion = -1; + + public static Serializer Get() + { + int currentVersion = SerializerRegistry.Version; + TypedSerializerBinding? binding = Volatile.Read(ref _binding); + if (binding is not null && Volatile.Read(ref _bindingVersion) == currentVersion) + { + return new Serializer(binding); + } + + lock (Gate) + { + binding = _binding; + if (binding is not null && _bindingVersion == currentVersion) + { + return new Serializer(binding); + } + + SerializerBinding untyped = SerializerRegistry.GetBinding(typeof(T)); + binding = untyped.RequireTypedBinding(); + _binding = binding; + Volatile.Write(ref _bindingVersion, currentVersion); + return new Serializer(binding); + } + } +} diff --git a/csharp/src/Fory/UnionSerializer.cs b/csharp/src/Fory/UnionSerializer.cs index debbb8569f..35904e3b81 100644 --- a/csharp/src/Fory/UnionSerializer.cs +++ b/csharp/src/Fory/UnionSerializer.cs @@ -20,25 +20,25 @@ namespace Apache.Fory; -public sealed class UnionSerializer : Serializer +public readonly struct UnionSerializer : IStaticSerializer, TUnion> where TUnion : Union { private static readonly Func Factory = BuildFactory(); - public override ForyTypeId StaticTypeId => ForyTypeId.TypedUnion; + public static ForyTypeId StaticTypeId => ForyTypeId.TypedUnion; - public override bool IsNullableType => true; + public static bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; + public static bool IsReferenceTrackableType => true; - public override TUnion DefaultValue => null!; + public static TUnion DefaultValue => null!; - public override bool IsNone(TUnion value) + public static bool IsNone(in TUnion value) { return value is null; } - public override void WriteData(ref WriteContext context, in TUnion value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in TUnion value, bool hasGenerics) { _ = hasGenerics; if (value is null) @@ -50,7 +50,7 @@ public override void WriteData(ref WriteContext context, in TUnion value, bool h DynamicAnyCodec.WriteAny(ref context, value.Value, RefMode.Tracking, true, false); } - public override TUnion ReadData(ref ReadContext context) + public static TUnion ReadData(ref ReadContext context) { uint rawCaseId = context.Reader.ReadVarUInt32(); if (rawCaseId > int.MaxValue) diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index 7bf435d835..5bfd3bd216 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -313,7 +313,7 @@ public void MacroFieldOrderFollowsForyRules() long second = reader.ReadVarInt64(); int third = reader.ReadVarInt32(); ReadContext tailContext = new(reader, new TypeResolver(), false, false); - string fourth = StringSerializer.Instance.ReadData(ref tailContext); + string fourth = SerializerRegistry.Get().ReadData(ref tailContext); Assert.Equal(value.B, first); Assert.Equal(value.A, second); diff --git a/csharp/tests/Fory.Tests/GlobalUsings.cs b/csharp/tests/Fory.Tests/GlobalUsings.cs index 8c927eb747..32c5277813 100644 --- a/csharp/tests/Fory.Tests/GlobalUsings.cs +++ b/csharp/tests/Fory.Tests/GlobalUsings.cs @@ -1 +1,18 @@ -global using Xunit; \ No newline at end of file +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +global using Xunit; diff --git a/csharp/tests/Fory.XlangPeer/Program.cs b/csharp/tests/Fory.XlangPeer/Program.cs index 18668847cc..be37d83b2c 100644 --- a/csharp/tests/Fory.XlangPeer/Program.cs +++ b/csharp/tests/Fory.XlangPeer/Program.cs @@ -1026,21 +1026,21 @@ public sealed class MyExt public int Id { get; set; } } -public sealed class MyExtSerializer : Serializer +public readonly struct MyExtSerializer : IStaticSerializer { - public override ForyTypeId StaticTypeId => ForyTypeId.Ext; - public override bool IsNullableType => true; - public override bool IsReferenceTrackableType => true; - public override MyExt DefaultValue => null!; - public override bool IsNone(MyExt value) => value is null; + public static ForyTypeId StaticTypeId => ForyTypeId.Ext; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static MyExt DefaultValue => null!; + public static bool IsNone(in MyExt value) => value is null; - public override void WriteData(ref WriteContext context, in MyExt value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in MyExt value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarInt32((value ?? new MyExt()).Id); } - public override MyExt ReadData(ref ReadContext context) + public static MyExt ReadData(ref ReadContext context) { return new MyExt { Id = context.Reader.ReadVarInt32() }; } diff --git a/java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java b/java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java index 08aecfba03..17005fa0c8 100644 --- a/java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/xlang/CSharpXlangTest.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -36,7 +37,8 @@ public class CSharpXlangTest extends XlangTestBase { private static final String CSHARP_DLL = "Fory.XlangPeer.dll"; private static final File CSHARP_DIR = new File("../../csharp"); - private static final File CSHARP_BINARY_DIR = new File(CSHARP_DIR, "tests/Fory.XlangPeer/bin/Debug/net8.0"); + private static final File CSHARP_BINARY_DIR = + new File(CSHARP_DIR, "tests/Fory.XlangPeer/bin/Debug/net8.0"); private volatile boolean peerBuilt; @Override @@ -71,12 +73,6 @@ protected CommandContext buildCommandContext(String caseName, Path dataFile) thr return new CommandContext(command, env, CSHARP_BINARY_DIR); } - @Override - @Test(groups = "xlang", dataProvider = "enableCodegenParallel") - public void testUnionXlang(boolean enableCodegen) throws java.io.IOException { - super.testUnionXlang(enableCodegen); - } - private boolean isDotnetAvailable() { try { Process process = new ProcessBuilder("dotnet", "--version").start(); @@ -104,8 +100,10 @@ private void ensurePeerBuilt() throws IOException { } List buildCommand = - List.of("dotnet", "build", "tests/Fory.XlangPeer/Fory.XlangPeer.csproj", "-c", "Debug"); - boolean built = TestUtils.executeCommand(buildCommand, 180, Collections.emptyMap(), CSHARP_DIR); + Arrays.asList( + "dotnet", "build", "tests/Fory.XlangPeer/Fory.XlangPeer.csproj", "-c", "Debug"); + boolean built = + TestUtils.executeCommand(buildCommand, 180, Collections.emptyMap(), CSHARP_DIR); if (!built) { throw new IOException("dotnet build failed for csharp/tests/Fory.XlangPeer"); } @@ -118,4 +116,222 @@ private void ensurePeerBuilt() throws IOException { peerBuilt = true; } } + + // ============================================================================ + // Test methods - duplicated from XlangTestBase for Maven Surefire discovery + // ============================================================================ + + @Test(groups = "xlang") + public void testBuffer() throws java.io.IOException { + super.testBuffer(); + } + + @Test(groups = "xlang") + public void testBufferVar() throws java.io.IOException { + super.testBufferVar(); + } + + @Test(groups = "xlang") + public void testMurmurHash3() throws java.io.IOException { + super.testMurmurHash3(); + } + + @Test(groups = "xlang") + public void testStringSerializer() throws Exception { + super.testStringSerializer(); + } + + @Test(groups = "xlang") + public void testCrossLanguageSerializer() throws Exception { + super.testCrossLanguageSerializer(); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testSimpleStruct(boolean enableCodegen) throws java.io.IOException { + super.testSimpleStruct(enableCodegen); + } + + @Test(groups = "xlang") + public void testSimpleNamedStructCodegenEnabled() throws java.io.IOException { + super.testSimpleNamedStruct(false); + } + + @Test(groups = "xlang") + public void testSimpleNamedStructCodegenDisabled() throws java.io.IOException { + super.testSimpleNamedStruct(false); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testList(boolean enableCodegen) throws java.io.IOException { + super.testList(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testMap(boolean enableCodegen) throws java.io.IOException { + super.testMap(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testInteger(boolean enableCodegen) throws java.io.IOException { + super.testInteger(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testItem(boolean enableCodegen) throws java.io.IOException { + super.testItem(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testColor(boolean enableCodegen) throws java.io.IOException { + super.testColor(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testStructWithList(boolean enableCodegen) throws java.io.IOException { + super.testStructWithList(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testStructWithMap(boolean enableCodegen) throws java.io.IOException { + super.testStructWithMap(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testCollectionElementRefOverride(boolean enableCodegen) throws java.io.IOException { + super.testCollectionElementRefOverride(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testSkipIdCustom(boolean enableCodegen) throws java.io.IOException { + super.testSkipIdCustom(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testSkipNameCustom(boolean enableCodegen) throws java.io.IOException { + super.testSkipNameCustom(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testConsistentNamed(boolean enableCodegen) throws java.io.IOException { + super.testConsistentNamed(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testStructVersionCheck(boolean enableCodegen) throws java.io.IOException { + super.testStructVersionCheck(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testPolymorphicList(boolean enableCodegen) throws java.io.IOException { + super.testPolymorphicList(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testPolymorphicMap(boolean enableCodegen) throws java.io.IOException { + super.testPolymorphicMap(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testOneStringFieldSchemaConsistent(boolean enableCodegen) throws java.io.IOException { + super.testOneStringFieldSchemaConsistent(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testOneStringFieldCompatible(boolean enableCodegen) throws java.io.IOException { + super.testOneStringFieldCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testTwoStringFieldCompatible(boolean enableCodegen) throws java.io.IOException { + super.testTwoStringFieldCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testSchemaEvolutionCompatible(boolean enableCodegen) throws java.io.IOException { + super.testSchemaEvolutionCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testOneEnumFieldSchemaConsistent(boolean enableCodegen) throws java.io.IOException { + super.testOneEnumFieldSchemaConsistent(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testOneEnumFieldCompatible(boolean enableCodegen) throws java.io.IOException { + super.testOneEnumFieldCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testTwoEnumFieldCompatible(boolean enableCodegen) throws java.io.IOException { + super.testTwoEnumFieldCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testEnumSchemaEvolutionCompatible(boolean enableCodegen) throws java.io.IOException { + super.testEnumSchemaEvolutionCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testNullableFieldSchemaConsistentNotNull(boolean enableCodegen) + throws java.io.IOException { + super.testNullableFieldSchemaConsistentNotNull(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testNullableFieldSchemaConsistentNull(boolean enableCodegen) + throws java.io.IOException { + super.testNullableFieldSchemaConsistentNull(enableCodegen); + } + + @Override + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testNullableFieldCompatibleNotNull(boolean enableCodegen) throws java.io.IOException { + super.testNullableFieldCompatibleNotNull(enableCodegen); + } + + @Override + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testNullableFieldCompatibleNull(boolean enableCodegen) throws java.io.IOException { + super.testNullableFieldCompatibleNull(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testUnionXlang(boolean enableCodegen) throws java.io.IOException { + super.testUnionXlang(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testRefSchemaConsistent(boolean enableCodegen) throws java.io.IOException { + super.testRefSchemaConsistent(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testRefCompatible(boolean enableCodegen) throws java.io.IOException { + super.testRefCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testCircularRefSchemaConsistent(boolean enableCodegen) throws java.io.IOException { + super.testCircularRefSchemaConsistent(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testCircularRefCompatible(boolean enableCodegen) throws java.io.IOException { + super.testCircularRefCompatible(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testUnsignedSchemaConsistent(boolean enableCodegen) throws java.io.IOException { + super.testUnsignedSchemaConsistent(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testUnsignedSchemaConsistentSimple(boolean enableCodegen) throws java.io.IOException { + super.testUnsignedSchemaConsistentSimple(enableCodegen); + } + + @Test(groups = "xlang", dataProvider = "enableCodegenParallel") + public void testUnsignedSchemaCompatible(boolean enableCodegen) throws java.io.IOException { + super.testUnsignedSchemaCompatible(enableCodegen); + } } diff --git a/licenserc.toml b/licenserc.toml index e4982ff2e8..cc0ecab0be 100644 --- a/licenserc.toml +++ b/licenserc.toml @@ -61,5 +61,5 @@ excludes = [ ] [mapping.DOUBLESLASH_STYLE] -extensions = ['go'] +extensions = ['go', 'cs'] filenames = ['go.mod'] From 206de9837aa6f54ed76e4b902d2591c062b16920 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 09:52:28 +0800 Subject: [PATCH 03/16] ci: add csharp and csharp xlang jobs --- .github/workflows/ci.yml | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 942a2a1695..a78931c8e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,6 +142,72 @@ jobs: shell: bash run: python ./ci/run_ci.py java --version windows_java21 + csharp: + name: C# CI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + cache: true + cache-dependency-path: | + csharp/**/*.csproj + csharp/Fory.sln + - name: Restore C# dependencies + run: | + cd csharp + dotnet restore Fory.sln + - name: Build C# solution + run: | + cd csharp + dotnet build Fory.sln -c Release --no-restore + - name: Run C# tests + run: | + cd csharp + dotnet test tests/Fory.Tests/Fory.Tests.csproj -c Release --no-build + + csharp_xlang: + name: C# Xlang Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + cache: true + cache-dependency-path: | + csharp/**/*.csproj + csharp/Fory.sln + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: "temurin" + - name: Cache Maven local repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Restore and build C# xlang peer + run: | + cd csharp + dotnet restore tests/Fory.XlangPeer/Fory.XlangPeer.csproj + dotnet build tests/Fory.XlangPeer/Fory.XlangPeer.csproj -c Debug --no-restore + - name: Run C# Xlang Test + env: + FORY_CSHARP_JAVA_CI: "1" + ENABLE_FORY_DEBUG_OUTPUT: "1" + run: | + cd java + mvn -T16 --no-transfer-progress clean install -DskipTests + cd fory-core + mvn -T16 --no-transfer-progress test -Dtest=org.apache.fory.xlang.CSharpXlangTest + swift: name: Swift CI runs-on: macos-latest From 07508ffe5197e50453b8395bb1dd1c0ba5cd6982 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 10:07:47 +0800 Subject: [PATCH 04/16] refactor(csharp): rework object any serialization and static dynamic dispatch --- .../src/Fory.Generator/ForyObjectGenerator.cs | 61 ++---- csharp/src/Fory/AnySerializer.cs | 204 ++++++------------ csharp/src/Fory/CollectionSerializers.cs | 115 +++++++++- csharp/src/Fory/ForyMap.cs | 115 +++++++++- csharp/src/Fory/SerializerRegistry.cs | 6 - csharp/src/Fory/TypeResolver.cs | 4 +- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 72 +++++++ 7 files changed, 383 insertions(+), 194 deletions(-) diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index c0cb978c8a..f3c8abee95 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -54,14 +54,6 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); - private static readonly DiagnosticDescriptor UnsupportedDynamicAny = new( - id: "FORY004", - title: "Unsupported dynamic Any field shape", - messageFormat: "Member '{0}' has unsupported dynamic Any shape '{1}'. Only object, List and Dictionary are supported.", - category: "Fory", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - public void Initialize(IncrementalGeneratorInitializationContext context) { IncrementalValuesProvider typeModels = context.SyntaxProvider @@ -318,14 +310,6 @@ private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool c sb.AppendLine( $" global::Apache.Fory.DynamicAnyCodec.WriteAnyList(ref context, {memberAccess}, {refModeExpr}, false, true);"); return; - case DynamicAnyKind.StringAnyMap: - sb.AppendLine( - $" global::Apache.Fory.DynamicAnyCodec.WriteStringAnyMap(ref context, {memberAccess}, {refModeExpr}, false, true);"); - return; - case DynamicAnyKind.Int32AnyMap: - sb.AppendLine( - $" global::Apache.Fory.DynamicAnyCodec.WriteInt32AnyMap(ref context, {memberAccess}, {refModeExpr}, false, true);"); - return; case DynamicAnyKind.None: break; default: @@ -368,23 +352,16 @@ private static void EmitReadMemberAssignment( { string indent = new(' ', indentLevel * 2); string assignmentTarget = $"{valueVar}.{member.Name}"; + string typeOfTypeName = StripNullableForTypeOf(member.TypeName); switch (member.DynamicAnyKind) { case DynamicAnyKind.AnyValue: sb.AppendLine( - $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAny(ref context, {refModeExpr}, true), typeof({member.TypeName}))!;"); + $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAny(ref context, {refModeExpr}, true), typeof({typeOfTypeName}))!;"); return; case DynamicAnyKind.AnyList: sb.AppendLine( - $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAnyList(ref context, {refModeExpr}, false), typeof({member.TypeName}))!;"); - return; - case DynamicAnyKind.StringAnyMap: - sb.AppendLine( - $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadStringAnyMap(ref context, {refModeExpr}, false), typeof({member.TypeName}))!;"); - return; - case DynamicAnyKind.Int32AnyMap: - sb.AppendLine( - $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadInt32AnyMap(ref context, {refModeExpr}, false), typeof({member.TypeName}))!;"); + $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAnyList(ref context, {refModeExpr}, false), typeof({typeOfTypeName}))!;"); return; case DynamicAnyKind.None: break; @@ -415,6 +392,11 @@ private static void EmitReadMemberAssignment( $"{indent}{assignmentTarget} = global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().Read(ref context, {refModeExpr}, {readTypeInfoExpr});"); } + private static string StripNullableForTypeOf(string typeName) + { + return typeName.Replace("?", string.Empty); + } + private static string BuildSchemaFingerprintExpression(ImmutableArray members) { if (members.IsDefaultOrEmpty) @@ -436,7 +418,7 @@ private static string BuildSchemaFingerprintExpression(ImmutableArray "(trackRef ? 1 : 0)", - DynamicAnyKind.AnyList or DynamicAnyKind.StringAnyMap or DynamicAnyKind.Int32AnyMap => "0", + DynamicAnyKind.AnyList => "0", _ => member.Classification.IsBuiltIn ? "0" : $"((trackRef && global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)", @@ -475,7 +457,7 @@ private static string BuildWriteRefModeExpression(MemberModel member) return member.DynamicAnyKind switch { DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)", - DynamicAnyKind.AnyList or DynamicAnyKind.StringAnyMap or DynamicAnyKind.Int32AnyMap => + DynamicAnyKind.AnyList => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)", _ => member.Classification.IsBuiltIn ? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)" @@ -610,11 +592,6 @@ private static string BuildWriteRefModeExpression(MemberModel member) } DynamicAnyKind dynamicAnyKind = ResolveDynamicAnyKind(unwrappedType); - if (dynamicAnyKind == DynamicAnyKind.Unsupported) - { - return null; - } - TypeResolution resolution = ResolveTypeResolution(unwrappedType, fieldEncoding); if (!resolution.Supported) { @@ -1051,7 +1028,7 @@ private static DynamicAnyKind ResolveDynamicAnyKind(ITypeSymbol type) } } - if (TryGetMapTypeArguments(type, out ITypeSymbol? keyType, out ITypeSymbol? valueType)) + if (TryGetMapTypeArguments(type, out _, out ITypeSymbol? valueType)) { (bool _, ITypeSymbol unwrappedValue) = UnwrapNullable(valueType); if (unwrappedValue.SpecialType != SpecialType.System_Object) @@ -1059,18 +1036,7 @@ private static DynamicAnyKind ResolveDynamicAnyKind(ITypeSymbol type) return DynamicAnyKind.None; } - (bool _, ITypeSymbol unwrappedKey) = UnwrapNullable(keyType); - if (unwrappedKey.SpecialType == SpecialType.System_String) - { - return DynamicAnyKind.StringAnyMap; - } - - if (unwrappedKey.SpecialType == SpecialType.System_Int32) - { - return DynamicAnyKind.Int32AnyMap; - } - - return DynamicAnyKind.Unsupported; + return DynamicAnyKind.None; } return DynamicAnyKind.None; @@ -1398,9 +1364,6 @@ private enum DynamicAnyKind None, AnyValue, AnyList, - StringAnyMap, - Int32AnyMap, - Unsupported, } private enum FieldEncoding diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index 12907c318b..ede871b208 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -17,44 +17,13 @@ namespace Apache.Fory; -internal static class DynamicAnyMapBits -{ - public const byte KeyNull = 0b0000_0010; - public const byte DeclaredKeyType = 0b0000_0100; - public const byte ValueNull = 0b0001_0000; -} - -public readonly struct ForyAnyNullValue -{ -} - -public readonly struct ForyAnyNullValueSerializer : IStaticSerializer -{ - public static ForyTypeId StaticTypeId => ForyTypeId.None; - public static bool IsNullableType => true; - public static ForyAnyNullValue DefaultValue => new(); - public static bool IsNone(in ForyAnyNullValue value) => true; - public static void WriteData(ref WriteContext context, in ForyAnyNullValue value, bool hasGenerics) - { - _ = context; - _ = value; - _ = hasGenerics; - } - - public static ForyAnyNullValue ReadData(ref ReadContext context) - { - _ = context; - return new ForyAnyNullValue(); - } -} - public readonly struct DynamicAnyObjectSerializer : IStaticSerializer { public static ForyTypeId StaticTypeId => ForyTypeId.Unknown; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; - public static object? DefaultValue => new ForyAnyNullValue(); - public static bool IsNone(in object? value) => value is null || value is ForyAnyNullValue; + public static object? DefaultValue => null; + public static bool IsNone(in object? value) => value is null; public static void WriteData(ref WriteContext context, in object? value, bool hasGenerics) { @@ -74,12 +43,6 @@ public static void WriteData(ref WriteContext context, in object? value, bool ha throw new ForyInvalidDataException("dynamic Any value requires type info"); } - context.ClearDynamicTypeInfo(typeof(object)); - if (dynamicTypeInfo.WireTypeId == ForyTypeId.None) - { - return new ForyAnyNullValue(); - } - return context.TypeResolver.ReadDynamicValue(dynamicTypeInfo, ref context); } @@ -138,12 +101,11 @@ public static void Write(ref WriteContext context, in object? value, RefMode ref switch (flag) { case RefFlag.Null: - return new ForyAnyNullValue(); + return null; case RefFlag.Ref: { uint refId = context.Reader.ReadVarUInt32(); - object? referenced = context.RefReader.ReadRefValue(refId); - return referenced ?? new ForyAnyNullValue(); + return context.RefReader.ReadRefValue(refId); } case RefFlag.RefValue: { @@ -155,6 +117,11 @@ public static void Write(ref WriteContext context, in object? value, RefMode ref } object? value = ReadData(ref context); + if (readTypeInfo) + { + context.ClearDynamicTypeInfo(typeof(object)); + } + context.FinishPendingReferenceIfNeeded(value); context.PopPendingReference(); return value; @@ -171,7 +138,13 @@ public static void Write(ref WriteContext context, in object? value, RefMode ref ReadTypeInfo(ref context); } - return ReadData(ref context); + object? result = ReadData(ref context); + if (readTypeInfo) + { + context.ClearDynamicTypeInfo(typeof(object)); + } + + return result; } private static bool AnyValueIsReferenceTrackable(object value) @@ -186,19 +159,21 @@ public static class DynamicAnyCodec { internal static void WriteAnyTypeInfo(object value, ref WriteContext context) { - if (value is ForyAnyNullValue) + if (value is IList) { - context.Writer.WriteUInt8((byte)ForyTypeId.None); + context.Writer.WriteUInt8((byte)ForyTypeId.List); return; } - if (value is IList) + if (value is ISet) { - context.Writer.WriteUInt8((byte)ForyTypeId.List); + context.Writer.WriteUInt8((byte)ForyTypeId.Set); return; } - if (value is IDictionary || value is IDictionary) + if (value is IDictionary || + value is IDictionary || + value is IDictionary) { context.Writer.WriteUInt8((byte)ForyTypeId.Map); return; @@ -210,22 +185,19 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) public static object? CastAnyDynamicValue(object? value, Type targetType) { - if (value is null || value is ForyAnyNullValue) + if (value is null) { - if (targetType == typeof(object) || targetType == typeof(ForyAnyNullValue)) + if (targetType == typeof(object)) { - return new ForyAnyNullValue(); + return null; } if (!targetType.IsValueType || Nullable.GetUnderlyingType(targetType) is not null) { return null; } - } - if (value is null) - { - return null; + throw new ForyInvalidDataException($"cannot cast null dynamic Any value to non-nullable {targetType}"); } if (targetType.IsInstanceOfType(value)) @@ -243,8 +215,7 @@ public static void WriteAny(ref WriteContext context, object? value, RefMode ref public static object? ReadAny(ref ReadContext context, RefMode refMode, bool readTypeInfo = true) { - object? value = SerializerRegistry.Get().Read(ref context, refMode, readTypeInfo); - return value is ForyAnyNullValue ? null : value; + return SerializerRegistry.Get().Read(ref context, refMode, readTypeInfo); } public static void WriteAnyList(ref WriteContext context, IList? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) @@ -257,21 +228,20 @@ public static void WriteAnyList(ref WriteContext context, IList? value, public static List? ReadAnyList(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) { Serializer> serializer = SerializerRegistry.Get>(); - List? wrapped = serializer.Read(ref context, refMode, readTypeInfo); - if (wrapped is null) - { - return null; - } + return serializer.Read(ref context, refMode, readTypeInfo); + } - for (int i = 0; i < wrapped.Count; i++) - { - if (wrapped[i] is ForyAnyNullValue) - { - wrapped[i] = null; - } - } + public static void WriteAnySet(ref WriteContext context, ISet? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) + { + HashSet? wrapped = value is null ? null : [.. value]; + Serializer> serializer = SerializerRegistry.Get>(); + serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); + } - return wrapped; + public static HashSet? ReadAnySet(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) + { + Serializer> serializer = SerializerRegistry.Get>(); + return serializer.Read(ref context, refMode, readTypeInfo); } public static void WriteStringAnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) @@ -284,19 +254,7 @@ public static void WriteStringAnyMap(ref WriteContext context, IDictionary? ReadStringAnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) { Serializer> serializer = SerializerRegistry.Get>(); - Dictionary? wrapped = serializer.Read(ref context, refMode, readTypeInfo); - if (wrapped is null) - { - return null; - } - - Dictionary normalized = new(wrapped.Count); - foreach ((string key, object? value) in wrapped) - { - normalized[key] = value is ForyAnyNullValue ? null : value; - } - - return normalized; + return serializer.Read(ref context, refMode, readTypeInfo); } public static void WriteInt32AnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) @@ -309,66 +267,32 @@ public static void WriteInt32AnyMap(ref WriteContext context, IDictionary? ReadInt32AnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) { Serializer> serializer = SerializerRegistry.Get>(); - Dictionary? wrapped = serializer.Read(ref context, refMode, readTypeInfo); - if (wrapped is null) - { - return null; - } - - Dictionary normalized = new(wrapped.Count); - foreach ((int key, object? value) in wrapped) - { - normalized[key] = value is ForyAnyNullValue ? null : value; - } - - return normalized; + return serializer.Read(ref context, refMode, readTypeInfo); } - public static object ReadDynamicAnyMapValue(ref ReadContext context) + public static void WriteObjectAnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) { - int mapStart = context.Reader.Cursor; - ForyTypeId? keyTypeId = PeekDynamicMapKeyTypeId(ref context); - context.Reader.SetCursor(mapStart); - return keyTypeId switch - { - ForyTypeId.Int32 or ForyTypeId.VarInt32 => ReadInt32AnyMap(ref context, RefMode.None, false) ?? new Dictionary(), - ForyTypeId.String or null => ReadStringAnyMap(ref context, RefMode.None, false) ?? new Dictionary(), - _ => throw new ForyInvalidDataException($"unsupported dynamic map key type {keyTypeId}"), - }; + Dictionary? wrapped = value is null ? null : new Dictionary(value); + Serializer> serializer = SerializerRegistry.Get>(); + serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); } - private static ForyTypeId? PeekDynamicMapKeyTypeId(ref ReadContext context) + public static Dictionary? ReadObjectAnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) { - int start = context.Reader.Cursor; - try - { - int length = checked((int)context.Reader.ReadVarUInt32()); - if (length == 0) - { - return null; - } - - byte header = context.Reader.ReadUInt8(); - bool keyNull = (header & DynamicAnyMapBits.KeyNull) != 0; - bool keyDeclared = (header & DynamicAnyMapBits.DeclaredKeyType) != 0; - bool valueNull = (header & DynamicAnyMapBits.ValueNull) != 0; - if (keyDeclared || keyNull) - { - return null; - } - - if (!valueNull) - { - _ = context.Reader.ReadUInt8(); - } + Serializer> serializer = SerializerRegistry.Get>(); + return serializer.Read(ref context, refMode, readTypeInfo); + } - uint rawTypeId = context.Reader.ReadVarUInt32(); - return Enum.IsDefined(typeof(ForyTypeId), rawTypeId) ? (ForyTypeId)rawTypeId : null; - } - finally + public static object ReadDynamicAnyMapValue(ref ReadContext context) + { + ForyMap map = + SerializerRegistry.Get>().Read(ref context, RefMode.None, false); + if (map.HasNullKey) { - context.Reader.SetCursor(start); + return map; } + + return new Dictionary(map.NonNullEntries); } public static void WriteAnyPayload(object value, ref WriteContext context, bool hasGenerics) @@ -379,6 +303,12 @@ public static void WriteAnyPayload(object value, ref WriteContext context, bool return; } + if (value is ISet set) + { + WriteAnySet(ref context, set, RefMode.None, false, hasGenerics); + return; + } + if (value is IDictionary stringMap) { WriteStringAnyMap(ref context, stringMap, RefMode.None, false, false); @@ -391,6 +321,12 @@ public static void WriteAnyPayload(object value, ref WriteContext context, bool return; } + if (value is IDictionary objectMap) + { + WriteObjectAnyMap(ref context, objectMap, RefMode.None, false, false); + return; + } + SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); serializer.WriteData(ref context, value, hasGenerics); } diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index e9b7d9b4bf..0815e02585 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -663,12 +663,35 @@ public static Dictionary ReadData(ref ReadContext context) if (keyNull) { + DynamicTypeInfo? valueDynamicInfo = null; + if (!valueDeclared) + { + if (valueDynamicType) + { + valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + valueSerializer.ReadTypeInfo(ref context); + } + } + + if (valueDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); + } + TValue value = ReadValueElement( ref context, trackValueRef, - valueDynamicType || !valueDeclared, + false, canonicalizeValues, valueSerializer); + if (valueDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + map[(TKey)keySerializer.DefaultObject!] = value; readCount += 1; continue; @@ -676,13 +699,101 @@ public static Dictionary ReadData(ref ReadContext context) if (valueNull) { - TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared); + DynamicTypeInfo? keyDynamicInfo = null; + if (!keyDeclared) + { + if (keyDynamicType) + { + keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + keySerializer.ReadTypeInfo(ref context); + } + } + + if (keyDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); + } + + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + if (keyDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + map[key] = (TValue)valueSerializer.DefaultObject!; readCount += 1; continue; } int chunkSize = context.Reader.ReadUInt8(); + if (keyDynamicType || valueDynamicType) + { + for (int i = 0; i < chunkSize; i++) + { + DynamicTypeInfo? keyDynamicInfo = null; + DynamicTypeInfo? valueDynamicInfo = null; + + if (!keyDeclared) + { + if (keyDynamicType) + { + keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + keySerializer.ReadTypeInfo(ref context); + } + } + + if (!valueDeclared) + { + if (valueDynamicType) + { + valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + valueSerializer.ReadTypeInfo(ref context); + } + } + + if (keyDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); + } + + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + if (keyDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + + if (valueDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); + } + + TValue value = ReadValueElement( + ref context, + trackValueRef, + false, + canonicalizeValues, + valueSerializer); + if (valueDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + + map[key] = value; + } + + readCount += chunkSize; + continue; + } + if (!keyDeclared) { keySerializer.ReadTypeInfo(ref context); diff --git a/csharp/src/Fory/ForyMap.cs b/csharp/src/Fory/ForyMap.cs index 85a7967e06..41a582b94b 100644 --- a/csharp/src/Fory/ForyMap.cs +++ b/csharp/src/Fory/ForyMap.cs @@ -260,12 +260,35 @@ public static ForyMap ReadData(ref ReadContext context) if (keyNull) { + DynamicTypeInfo? valueDynamicInfo = null; + if (!valueDeclared) + { + if (valueDynamicType) + { + valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + valueSerializer.ReadTypeInfo(ref context); + } + } + + if (valueDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); + } + TValue valueRead = ReadValueElement( ref context, trackValueRef, - valueDynamicType || !valueDeclared, + false, canonicalizeValues, valueSerializer); + if (valueDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + map.Add(default, valueRead); readCount += 1; continue; @@ -273,13 +296,101 @@ public static ForyMap ReadData(ref ReadContext context) if (valueNull) { - TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, keyDynamicType || !keyDeclared); + DynamicTypeInfo? keyDynamicInfo = null; + if (!keyDeclared) + { + if (keyDynamicType) + { + keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + keySerializer.ReadTypeInfo(ref context); + } + } + + if (keyDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); + } + + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + if (keyDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + map.Add(key, (TValue)valueSerializer.DefaultObject!); readCount += 1; continue; } int chunkSize = context.Reader.ReadUInt8(); + if (keyDynamicType || valueDynamicType) + { + for (int i = 0; i < chunkSize; i++) + { + DynamicTypeInfo? keyDynamicInfo = null; + DynamicTypeInfo? valueDynamicInfo = null; + + if (!keyDeclared) + { + if (keyDynamicType) + { + keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + keySerializer.ReadTypeInfo(ref context); + } + } + + if (!valueDeclared) + { + if (valueDynamicType) + { + valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + valueSerializer.ReadTypeInfo(ref context); + } + } + + if (keyDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); + } + + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + if (keyDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + + if (valueDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); + } + + TValue valueRead = ReadValueElement( + ref context, + trackValueRef, + false, + canonicalizeValues, + valueSerializer); + if (valueDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + + map.Add(key, valueRead); + } + + readCount += chunkSize; + continue; + } + if (!keyDeclared) { keySerializer.ReadTypeInfo(ref context); diff --git a/csharp/src/Fory/SerializerRegistry.cs b/csharp/src/Fory/SerializerRegistry.cs index 66c9d2c77a..7bde12d43f 100644 --- a/csharp/src/Fory/SerializerRegistry.cs +++ b/csharp/src/Fory/SerializerRegistry.cs @@ -187,11 +187,6 @@ private static SerializerBinding Create(Type type) return StaticSerializerBindingFactory.Create(); } - if (type == typeof(ForyAnyNullValue)) - { - return StaticSerializerBindingFactory.Create(); - } - if (type == typeof(object)) { return StaticSerializerBindingFactory.Create(); @@ -275,7 +270,6 @@ private static void RegisterBuiltins() Cache[typeof(string)] = StaticSerializerBindingFactory.Create(); Cache[typeof(byte[])] = StaticSerializerBindingFactory.Create(); Cache[typeof(object)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(ForyAnyNullValue)] = StaticSerializerBindingFactory.Create(); Cache[typeof(Union)] = StaticSerializerBindingFactory.Create>(); } } diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index ef23942a1b..a5452ff131 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -258,6 +258,8 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.List: return DynamicAnyCodec.ReadAnyList(ref context, RefMode.None, false) ?? []; + case ForyTypeId.Set: + return DynamicAnyCodec.ReadAnySet(ref context, RefMode.None, false) ?? new HashSet(); case ForyTypeId.Map: return DynamicAnyCodec.ReadDynamicAnyMapValue(ref context); case ForyTypeId.Union: @@ -317,7 +319,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) return ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value, ref context, compatibleTypeMeta); } case ForyTypeId.None: - return new ForyAnyNullValue(); + return null; default: throw new ForyInvalidDataException($"unsupported dynamic type id {typeInfo.WireTypeId}"); } diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index 5bfd3bd216..7d9d24d5a7 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -108,6 +108,14 @@ public sealed class StructWithUnion2 public Union2 Union { get; set; } = Union2.OfT1(string.Empty); } +[ForyObject] +public sealed class DynamicAnyHolder +{ + public object? AnyValue { get; set; } + public HashSet AnySet { get; set; } = []; + public Dictionary AnyMap { get; set; } = []; +} + public sealed class ForyRuntimeTests { [Fact] @@ -399,4 +407,68 @@ public void EnumRoundTrip() Assert.Equal(value.Color, decoded.Color); Assert.Equal(value.Value, decoded.Value); } + + [Fact] + public void DynamicObjectSupportsObjectKeyMapAndSet() + { + ForyRuntime fory = ForyRuntime.Builder().Build(); + + Dictionary map = new() + { + ["k1"] = 7, + [2] = "v2", + [true] = null, + }; + Dictionary mapDecoded = + Assert.IsType>(fory.DeserializeObject(fory.SerializeObject(map))); + Assert.Equal(3, mapDecoded.Count); + Assert.Equal(7, mapDecoded["k1"]); + Assert.Equal("v2", mapDecoded[2]); + Assert.True(mapDecoded.ContainsKey(true)); + Assert.Null(mapDecoded[true]); + + HashSet set = ["a", 7, false]; + HashSet setDecoded = + Assert.IsType>(fory.DeserializeObject(fory.SerializeObject(set))); + Assert.Equal(3, setDecoded.Count); + Assert.Contains("a", setDecoded); + Assert.Contains(7, setDecoded); + Assert.Contains(false, setDecoded); + } + + [Fact] + public void GeneratedSerializerSupportsObjectKeyMap() + { + ForyRuntime fory = ForyRuntime.Builder().TrackRef(true).Build(); + fory.Register(400); + + DynamicAnyHolder source = new() + { + AnyValue = new Dictionary + { + ["inner"] = 9, + [10] = "ten", + }, + AnySet = ["x", 123], + AnyMap = new Dictionary + { + ["key1"] = null, + [99] = new List { "n", 1 }, + }, + }; + + DynamicAnyHolder decoded = fory.Deserialize(fory.Serialize(source)); + Dictionary dynamicMap = Assert.IsType>(decoded.AnyValue); + Assert.Equal(9, dynamicMap["inner"]); + Assert.Equal("ten", dynamicMap[10]); + Assert.Equal(source.AnySet.Count, decoded.AnySet.Count); + Assert.Contains("x", decoded.AnySet); + Assert.Contains(123, decoded.AnySet); + Assert.Equal(source.AnyMap.Count, decoded.AnyMap.Count); + Assert.True(decoded.AnyMap.ContainsKey("key1")); + Assert.Null(decoded.AnyMap["key1"]); + List nested = Assert.IsType>(decoded.AnyMap[99]); + Assert.Equal("n", nested[0]); + Assert.Equal(1, nested[1]); + } } From 4485e235f587be405152828623f89e068cb7a497 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 10:33:13 +0800 Subject: [PATCH 05/16] refactor(csharp): remove legacy any container APIs and split map serializers --- .../src/Fory.Generator/ForyObjectGenerator.cs | 32 - csharp/src/Fory/AnySerializer.cs | 349 +------ csharp/src/Fory/CollectionSerializers.cs | 878 ++++++++---------- csharp/src/Fory/ForyMap.cs | 186 +++- csharp/src/Fory/MapSerializers.cs | 474 ++++++++++ csharp/src/Fory/TypeResolver.cs | 6 +- 6 files changed, 986 insertions(+), 939 deletions(-) create mode 100644 csharp/src/Fory/MapSerializers.cs diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index f3c8abee95..daec57b15b 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -306,10 +306,6 @@ private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool c sb.AppendLine( $" global::Apache.Fory.DynamicAnyCodec.WriteAny(ref context, {memberAccess}, {refModeExpr}, true, false);"); return; - case DynamicAnyKind.AnyList: - sb.AppendLine( - $" global::Apache.Fory.DynamicAnyCodec.WriteAnyList(ref context, {memberAccess}, {refModeExpr}, false, true);"); - return; case DynamicAnyKind.None: break; default: @@ -359,10 +355,6 @@ private static void EmitReadMemberAssignment( sb.AppendLine( $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAny(ref context, {refModeExpr}, true), typeof({typeOfTypeName}))!;"); return; - case DynamicAnyKind.AnyList: - sb.AppendLine( - $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAnyList(ref context, {refModeExpr}, false), typeof({typeOfTypeName}))!;"); - return; case DynamicAnyKind.None: break; default: @@ -418,7 +410,6 @@ private static string BuildSchemaFingerprintExpression(ImmutableArray "(trackRef ? 1 : 0)", - DynamicAnyKind.AnyList => "0", _ => member.Classification.IsBuiltIn ? "0" : $"((trackRef && global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)", @@ -457,8 +448,6 @@ private static string BuildWriteRefModeExpression(MemberModel member) return member.DynamicAnyKind switch { DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)", - DynamicAnyKind.AnyList => - $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)", _ => member.Classification.IsBuiltIn ? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)" : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().IsReferenceTrackableType)", @@ -1019,26 +1008,6 @@ private static DynamicAnyKind ResolveDynamicAnyKind(ITypeSymbol type) return DynamicAnyKind.AnyValue; } - if (TryGetListElementType(type, out ITypeSymbol? elementType)) - { - (bool _, ITypeSymbol unwrappedElement) = UnwrapNullable(elementType); - if (unwrappedElement.SpecialType == SpecialType.System_Object) - { - return DynamicAnyKind.AnyList; - } - } - - if (TryGetMapTypeArguments(type, out _, out ITypeSymbol? valueType)) - { - (bool _, ITypeSymbol unwrappedValue) = UnwrapNullable(valueType); - if (unwrappedValue.SpecialType != SpecialType.System_Object) - { - return DynamicAnyKind.None; - } - - return DynamicAnyKind.None; - } - return DynamicAnyKind.None; } @@ -1363,7 +1332,6 @@ private enum DynamicAnyKind { None, AnyValue, - AnyList, } private enum FieldEncoding diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index ede871b208..4da8c5b1be 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -159,23 +159,9 @@ public static class DynamicAnyCodec { internal static void WriteAnyTypeInfo(object value, ref WriteContext context) { - if (value is IList) + if (DynamicContainerCodec.TryGetTypeId(value, out ForyTypeId containerTypeId)) { - context.Writer.WriteUInt8((byte)ForyTypeId.List); - return; - } - - if (value is ISet) - { - context.Writer.WriteUInt8((byte)ForyTypeId.Set); - return; - } - - if (value is IDictionary || - value is IDictionary || - value is IDictionary) - { - context.Writer.WriteUInt8((byte)ForyTypeId.Map); + context.Writer.WriteUInt8((byte)containerTypeId); return; } @@ -218,343 +204,14 @@ public static void WriteAny(ref WriteContext context, object? value, RefMode ref return SerializerRegistry.Get().Read(ref context, refMode, readTypeInfo); } - public static void WriteAnyList(ref WriteContext context, IList? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) - { - List? wrapped = value is null ? null : [.. value]; - Serializer> serializer = SerializerRegistry.Get>(); - serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); - } - - public static List? ReadAnyList(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) - { - Serializer> serializer = SerializerRegistry.Get>(); - return serializer.Read(ref context, refMode, readTypeInfo); - } - - public static void WriteAnySet(ref WriteContext context, ISet? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) - { - HashSet? wrapped = value is null ? null : [.. value]; - Serializer> serializer = SerializerRegistry.Get>(); - serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); - } - - public static HashSet? ReadAnySet(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) - { - Serializer> serializer = SerializerRegistry.Get>(); - return serializer.Read(ref context, refMode, readTypeInfo); - } - - public static void WriteStringAnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) - { - Dictionary? wrapped = value is null ? null : new Dictionary(value); - Serializer> serializer = SerializerRegistry.Get>(); - serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); - } - - public static Dictionary? ReadStringAnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) - { - Serializer> serializer = SerializerRegistry.Get>(); - return serializer.Read(ref context, refMode, readTypeInfo); - } - - public static void WriteInt32AnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) - { - Dictionary? wrapped = value is null ? null : new Dictionary(value); - Serializer> serializer = SerializerRegistry.Get>(); - serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); - } - - public static Dictionary? ReadInt32AnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) - { - Serializer> serializer = SerializerRegistry.Get>(); - return serializer.Read(ref context, refMode, readTypeInfo); - } - - public static void WriteObjectAnyMap(ref WriteContext context, IDictionary? value, RefMode refMode, bool writeTypeInfo = false, bool hasGenerics = true) - { - Dictionary? wrapped = value is null ? null : new Dictionary(value); - Serializer> serializer = SerializerRegistry.Get>(); - serializer.Write(ref context, wrapped!, refMode, writeTypeInfo, hasGenerics); - } - - public static Dictionary? ReadObjectAnyMap(ref ReadContext context, RefMode refMode, bool readTypeInfo = false) - { - Serializer> serializer = SerializerRegistry.Get>(); - return serializer.Read(ref context, refMode, readTypeInfo); - } - - public static object ReadDynamicAnyMapValue(ref ReadContext context) - { - ForyMap map = - SerializerRegistry.Get>().Read(ref context, RefMode.None, false); - if (map.HasNullKey) - { - return map; - } - - return new Dictionary(map.NonNullEntries); - } - public static void WriteAnyPayload(object value, ref WriteContext context, bool hasGenerics) { - if (value is IList list) + if (DynamicContainerCodec.TryWritePayload(value, ref context, hasGenerics)) { - WriteAnyList(ref context, list, RefMode.None, false, hasGenerics); - return; - } - - if (value is ISet set) - { - WriteAnySet(ref context, set, RefMode.None, false, hasGenerics); - return; - } - - if (value is IDictionary stringMap) - { - WriteStringAnyMap(ref context, stringMap, RefMode.None, false, false); - return; - } - - if (value is IDictionary intMap) - { - WriteInt32AnyMap(ref context, intMap, RefMode.None, false, false); - return; - } - - if (value is IDictionary objectMap) - { - WriteObjectAnyMap(ref context, objectMap, RefMode.None, false, false); return; } SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); serializer.WriteData(ref context, value, hasGenerics); } - - public static void WriteCollectionData( - IEnumerable values, - Serializer elementSerializer, - ref WriteContext context, - bool hasGenerics) - { - List list = values as List ?? [.. values]; - context.Writer.WriteVarUInt32((uint)list.Count); - if (list.Count == 0) - { - return; - } - - bool hasNull = elementSerializer.IsNullableType && list.Any(v => elementSerializer.IsNoneObject(v)); - bool trackRef = context.TrackRef && elementSerializer.IsReferenceTrackableType; - bool declaredElementType = hasGenerics && !elementSerializer.StaticTypeId.NeedsTypeInfoForField(); - bool dynamicElementType = elementSerializer.StaticTypeId == ForyTypeId.Unknown; - - byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType; - if (trackRef) - { - header |= CollectionBits.TrackingRef; - } - - if (hasNull) - { - header |= CollectionBits.HasNull; - } - - if (declaredElementType) - { - header |= CollectionBits.DeclaredElementType; - } - - context.Writer.WriteUInt8(header); - if (!dynamicElementType && !declaredElementType) - { - elementSerializer.WriteTypeInfo(ref context); - } - - if (dynamicElementType) - { - RefMode refMode = trackRef ? RefMode.Tracking : hasNull ? RefMode.NullOnly : RefMode.None; - foreach (T element in list) - { - elementSerializer.Write(ref context, element, refMode, true, hasGenerics); - } - - return; - } - - if (trackRef) - { - foreach (T element in list) - { - elementSerializer.Write(ref context, element, RefMode.Tracking, false, hasGenerics); - } - - return; - } - - if (hasNull) - { - foreach (T element in list) - { - if (elementSerializer.IsNoneObject(element)) - { - context.Writer.WriteInt8((sbyte)RefFlag.Null); - } - else - { - context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); - elementSerializer.WriteData(ref context, element, hasGenerics); - } - } - - return; - } - - foreach (T element in list) - { - elementSerializer.WriteData(ref context, element, hasGenerics); - } - } - - public static List ReadCollectionData(Serializer elementSerializer, ref ReadContext context) - { - int length = checked((int)context.Reader.ReadVarUInt32()); - if (length == 0) - { - return []; - } - - byte header = context.Reader.ReadUInt8(); - bool trackRef = (header & CollectionBits.TrackingRef) != 0; - bool hasNull = (header & CollectionBits.HasNull) != 0; - bool declared = (header & CollectionBits.DeclaredElementType) != 0; - bool sameType = (header & CollectionBits.SameType) != 0; - bool canonicalizeElements = context.TrackRef && !trackRef && elementSerializer.IsReferenceTrackableType; - - List values = new(length); - if (!sameType) - { - if (trackRef) - { - for (int i = 0; i < length; i++) - { - values.Add(elementSerializer.Read(ref context, RefMode.Tracking, true)); - } - - return values; - } - - if (hasNull) - { - for (int i = 0; i < length; i++) - { - sbyte refFlag = context.Reader.ReadInt8(); - if (refFlag == (sbyte)RefFlag.Null) - { - values.Add((T)elementSerializer.DefaultObject!); - } - else if (refFlag == (sbyte)RefFlag.NotNullValue) - { - values.Add(ReadCollectionElementWithCanonicalization(elementSerializer, ref context, true, canonicalizeElements)); - } - else - { - throw new ForyRefException($"invalid nullability flag {refFlag}"); - } - } - } - else - { - for (int i = 0; i < length; i++) - { - values.Add(ReadCollectionElementWithCanonicalization(elementSerializer, ref context, true, canonicalizeElements)); - } - } - - return values; - } - - if (!declared) - { - elementSerializer.ReadTypeInfo(ref context); - } - - if (trackRef) - { - for (int i = 0; i < length; i++) - { - values.Add(elementSerializer.Read(ref context, RefMode.Tracking, false)); - } - - if (!declared) - { - context.ClearDynamicTypeInfo(typeof(T)); - } - - return values; - } - - if (hasNull) - { - for (int i = 0; i < length; i++) - { - sbyte refFlag = context.Reader.ReadInt8(); - if (refFlag == (sbyte)RefFlag.Null) - { - values.Add((T)elementSerializer.DefaultObject!); - } - else - { - values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, ref context, canonicalizeElements)); - } - } - } - else - { - for (int i = 0; i < length; i++) - { - values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, ref context, canonicalizeElements)); - } - } - - if (!declared) - { - context.ClearDynamicTypeInfo(typeof(T)); - } - - return values; - } - - private static T ReadCollectionElementWithCanonicalization( - Serializer elementSerializer, - ref ReadContext context, - bool readTypeInfo, - bool canonicalize) - { - if (!canonicalize) - { - return elementSerializer.Read(ref context, RefMode.None, readTypeInfo); - } - - int start = context.Reader.Cursor; - T value = elementSerializer.Read(ref context, RefMode.None, readTypeInfo); - int end = context.Reader.Cursor; - return context.CanonicalizeNonTrackingReference(value, start, end); - } - - private static T ReadCollectionElementDataWithCanonicalization( - Serializer elementSerializer, - ref ReadContext context, - bool canonicalize) - { - if (!canonicalize) - { - return elementSerializer.ReadData(ref context); - } - - int start = context.Reader.Cursor; - T value = elementSerializer.ReadData(ref context); - int end = context.Reader.Cursor; - return context.CanonicalizeNonTrackingReference(value, start, end); - } } diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index 0815e02585..de8003427c 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +using System.Collections; + namespace Apache.Fory; internal static class CollectionBits @@ -25,14 +27,368 @@ internal static class CollectionBits public const byte SameType = 0b0000_1000; } -internal static class MapBits + +internal static class CollectionCodec +{ + public static void WriteCollectionData( + IEnumerable values, + Serializer elementSerializer, + ref WriteContext context, + bool hasGenerics) + { + List list = values as List ?? [.. values]; + context.Writer.WriteVarUInt32((uint)list.Count); + if (list.Count == 0) + { + return; + } + + bool hasNull = false; + if (elementSerializer.IsNullableType) + { + for (int i = 0; i < list.Count; i++) + { + if (!elementSerializer.IsNoneObject(list[i])) + { + continue; + } + + hasNull = true; + break; + } + } + + bool trackRef = context.TrackRef && elementSerializer.IsReferenceTrackableType; + bool declaredElementType = hasGenerics && !elementSerializer.StaticTypeId.NeedsTypeInfoForField(); + bool dynamicElementType = elementSerializer.StaticTypeId == ForyTypeId.Unknown; + + byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType; + if (trackRef) + { + header |= CollectionBits.TrackingRef; + } + + if (hasNull) + { + header |= CollectionBits.HasNull; + } + + if (declaredElementType) + { + header |= CollectionBits.DeclaredElementType; + } + + context.Writer.WriteUInt8(header); + if (!dynamicElementType && !declaredElementType) + { + elementSerializer.WriteTypeInfo(ref context); + } + + if (dynamicElementType) + { + RefMode refMode = trackRef ? RefMode.Tracking : hasNull ? RefMode.NullOnly : RefMode.None; + foreach (T element in list) + { + elementSerializer.Write(ref context, element, refMode, true, hasGenerics); + } + + return; + } + + if (trackRef) + { + foreach (T element in list) + { + elementSerializer.Write(ref context, element, RefMode.Tracking, false, hasGenerics); + } + + return; + } + + if (hasNull) + { + foreach (T element in list) + { + if (elementSerializer.IsNoneObject(element)) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + } + else + { + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); + elementSerializer.WriteData(ref context, element, hasGenerics); + } + } + + return; + } + + foreach (T element in list) + { + elementSerializer.WriteData(ref context, element, hasGenerics); + } + } + + public static List ReadCollectionData(Serializer elementSerializer, ref ReadContext context) + { + int length = checked((int)context.Reader.ReadVarUInt32()); + if (length == 0) + { + return []; + } + + byte header = context.Reader.ReadUInt8(); + bool trackRef = (header & CollectionBits.TrackingRef) != 0; + bool hasNull = (header & CollectionBits.HasNull) != 0; + bool declared = (header & CollectionBits.DeclaredElementType) != 0; + bool sameType = (header & CollectionBits.SameType) != 0; + bool canonicalizeElements = context.TrackRef && !trackRef && elementSerializer.IsReferenceTrackableType; + + List values = new(length); + if (!sameType) + { + if (trackRef) + { + for (int i = 0; i < length; i++) + { + values.Add(elementSerializer.Read(ref context, RefMode.Tracking, true)); + } + + return values; + } + + if (hasNull) + { + for (int i = 0; i < length; i++) + { + sbyte refFlag = context.Reader.ReadInt8(); + if (refFlag == (sbyte)RefFlag.Null) + { + values.Add((T)elementSerializer.DefaultObject!); + } + else if (refFlag == (sbyte)RefFlag.NotNullValue) + { + values.Add(ReadCollectionElementWithCanonicalization(elementSerializer, ref context, true, canonicalizeElements)); + } + else + { + throw new ForyRefException($"invalid nullability flag {refFlag}"); + } + } + } + else + { + for (int i = 0; i < length; i++) + { + values.Add(ReadCollectionElementWithCanonicalization(elementSerializer, ref context, true, canonicalizeElements)); + } + } + + return values; + } + + if (!declared) + { + elementSerializer.ReadTypeInfo(ref context); + } + + if (trackRef) + { + for (int i = 0; i < length; i++) + { + values.Add(elementSerializer.Read(ref context, RefMode.Tracking, false)); + } + + if (!declared) + { + context.ClearDynamicTypeInfo(typeof(T)); + } + + return values; + } + + if (hasNull) + { + for (int i = 0; i < length; i++) + { + sbyte refFlag = context.Reader.ReadInt8(); + if (refFlag == (sbyte)RefFlag.Null) + { + values.Add((T)elementSerializer.DefaultObject!); + } + else + { + values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, ref context, canonicalizeElements)); + } + } + } + else + { + for (int i = 0; i < length; i++) + { + values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, ref context, canonicalizeElements)); + } + } + + if (!declared) + { + context.ClearDynamicTypeInfo(typeof(T)); + } + + return values; + } + + private static T ReadCollectionElementWithCanonicalization( + Serializer elementSerializer, + ref ReadContext context, + bool readTypeInfo, + bool canonicalize) + { + if (!canonicalize) + { + return elementSerializer.Read(ref context, RefMode.None, readTypeInfo); + } + + int start = context.Reader.Cursor; + T value = elementSerializer.Read(ref context, RefMode.None, readTypeInfo); + int end = context.Reader.Cursor; + return context.CanonicalizeNonTrackingReference(value, start, end); + } + + private static T ReadCollectionElementDataWithCanonicalization( + Serializer elementSerializer, + ref ReadContext context, + bool canonicalize) + { + if (!canonicalize) + { + return elementSerializer.ReadData(ref context); + } + + int start = context.Reader.Cursor; + T value = elementSerializer.ReadData(ref context); + int end = context.Reader.Cursor; + return context.CanonicalizeNonTrackingReference(value, start, end); + } +} + +internal static class DynamicContainerCodec { - public const byte TrackingKeyRef = 0b0000_0001; - public const byte KeyNull = 0b0000_0010; - public const byte DeclaredKeyType = 0b0000_0100; - public const byte TrackingValueRef = 0b0000_1000; - public const byte ValueNull = 0b0001_0000; - public const byte DeclaredValueType = 0b0010_0000; + public static bool TryGetTypeId(object value, out ForyTypeId typeId) + { + if (value is IDictionary) + { + typeId = ForyTypeId.Map; + return true; + } + + Type valueType = value.GetType(); + if (value is IList && !valueType.IsArray) + { + typeId = ForyTypeId.List; + return true; + } + + if (IsSet(valueType)) + { + typeId = ForyTypeId.Set; + return true; + } + + typeId = default; + return false; + } + + public static bool TryWritePayload(object value, ref WriteContext context, bool hasGenerics) + { + if (value is IDictionary dictionary) + { + ForyMap map = new(); + foreach (DictionaryEntry entry in dictionary) + { + map.Add(entry.Key, entry.Value); + } + + SerializerRegistry.Get>().WriteData(ref context, map, false); + return true; + } + + Type valueType = value.GetType(); + if (value is IList list && !valueType.IsArray) + { + List values = new(list.Count); + for (int i = 0; i < list.Count; i++) + { + values.Add(list[i]); + } + + SerializerRegistry.Get>().WriteData(ref context, values, hasGenerics); + return true; + } + + if (!IsSet(valueType)) + { + return false; + } + + HashSet set = []; + foreach (object? item in (IEnumerable)value) + { + set.Add(item); + } + + SerializerRegistry.Get>().WriteData(ref context, set, hasGenerics); + return true; + } + + public static List ReadListPayload(ref ReadContext context) + { + return SerializerRegistry.Get>().ReadData(ref context); + } + + public static HashSet ReadSetPayload(ref ReadContext context) + { + return SerializerRegistry.Get>().ReadData(ref context); + } + + public static object ReadMapPayload(ref ReadContext context) + { + ForyMap map = SerializerRegistry.Get>().ReadData(ref context); + if (map.HasNullKey) + { + return map; + } + + return new Dictionary(map.NonNullEntries); + } + + private static bool IsSet(Type valueType) + { + if (!valueType.IsGenericType) + { + return false; + } + + if (valueType.GetGenericTypeDefinition() == typeof(ISet<>)) + { + return true; + } + + foreach (Type iface in valueType.GetInterfaces()) + { + if (!iface.IsGenericType) + { + continue; + } + + if (iface.GetGenericTypeDefinition() == typeof(ISet<>)) + { + return true; + } + } + + return false; + } } internal static class PrimitiveArrayCodec @@ -400,7 +756,7 @@ public static void WriteData(ref WriteContext context, in T[] value, bool hasGen return; } - DynamicAnyCodec.WriteCollectionData( + CollectionCodec.WriteCollectionData( safe, ElementSerializer, ref context, @@ -414,7 +770,7 @@ public static T[] ReadData(ref ReadContext context) return PrimitiveArrayCodec.ReadPrimitiveArray(ref context); } - List values = DynamicAnyCodec.ReadCollectionData(ElementSerializer, ref context); + List values = CollectionCodec.ReadCollectionData(ElementSerializer, ref context); return values.ToArray(); } } @@ -432,12 +788,12 @@ public static T[] ReadData(ref ReadContext context) public static void WriteData(ref WriteContext context, in List value, bool hasGenerics) { List safe = value ?? []; - DynamicAnyCodec.WriteCollectionData(safe, ElementSerializer, ref context, hasGenerics); + CollectionCodec.WriteCollectionData(safe, ElementSerializer, ref context, hasGenerics); } public static List ReadData(ref ReadContext context) { - return DynamicAnyCodec.ReadCollectionData(ElementSerializer, ref context); + return CollectionCodec.ReadCollectionData(ElementSerializer, ref context); } } @@ -463,503 +819,3 @@ public static HashSet ReadData(ref ReadContext context) } } -public readonly struct MapSerializer : IStaticSerializer, Dictionary> - where TKey : notnull -{ - private static Serializer KeySerializer => SerializerRegistry.Get(); - private static Serializer ValueSerializer => SerializerRegistry.Get(); - - public static ForyTypeId StaticTypeId => ForyTypeId.Map; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static Dictionary DefaultValue => null!; - public static bool IsNone(in Dictionary value) => value is null; - - public static void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Serializer keySerializer = KeySerializer; - Serializer valueSerializer = ValueSerializer; - Dictionary map = value ?? []; - context.Writer.WriteVarUInt32((uint)map.Count); - if (map.Count == 0) - { - return; - } - - bool trackKeyRef = context.TrackRef && keySerializer.IsReferenceTrackableType; - bool trackValueRef = context.TrackRef && valueSerializer.IsReferenceTrackableType; - bool keyDeclared = hasGenerics && !keySerializer.StaticTypeId.NeedsTypeInfoForField(); - bool valueDeclared = hasGenerics && !valueSerializer.StaticTypeId.NeedsTypeInfoForField(); - bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; - bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; - - KeyValuePair[] pairs = [.. map]; - if (keyDynamicType || valueDynamicType) - { - WriteDynamicMapPairs( - pairs, - ref context, - hasGenerics, - trackKeyRef, - trackValueRef, - keyDeclared, - valueDeclared, - keyDynamicType, - valueDynamicType, - keySerializer, - valueSerializer); - return; - } - - int index = 0; - while (index < pairs.Length) - { - KeyValuePair pair = pairs[index]; - bool keyIsNull = keySerializer.IsNoneObject(pair.Key); - bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); - if (keyIsNull || valueIsNull) - { - byte header = 0; - if (trackKeyRef) - { - header |= MapBits.TrackingKeyRef; - } - - if (trackValueRef) - { - header |= MapBits.TrackingValueRef; - } - - if (keyIsNull) - { - header |= MapBits.KeyNull; - } - - if (valueIsNull) - { - header |= MapBits.ValueNull; - } - - if (!keyIsNull && keyDeclared) - { - header |= MapBits.DeclaredKeyType; - } - - if (!valueIsNull && valueDeclared) - { - header |= MapBits.DeclaredValueType; - } - - context.Writer.WriteUInt8(header); - if (!keyIsNull) - { - if (!keyDeclared) - { - keySerializer.WriteTypeInfo(ref context); - } - - keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - } - - if (!valueIsNull) - { - if (!valueDeclared) - { - valueSerializer.WriteTypeInfo(ref context); - } - - valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - } - - index += 1; - continue; - } - - byte blockHeader = 0; - if (trackKeyRef) - { - blockHeader |= MapBits.TrackingKeyRef; - } - - if (trackValueRef) - { - blockHeader |= MapBits.TrackingValueRef; - } - - if (keyDeclared) - { - blockHeader |= MapBits.DeclaredKeyType; - } - - if (valueDeclared) - { - blockHeader |= MapBits.DeclaredValueType; - } - - context.Writer.WriteUInt8(blockHeader); - int chunkSizeOffset = context.Writer.Count; - context.Writer.WriteUInt8(0); - if (!keyDeclared) - { - keySerializer.WriteTypeInfo(ref context); - } - - if (!valueDeclared) - { - valueSerializer.WriteTypeInfo(ref context); - } - - byte chunkSize = 0; - while (index < pairs.Length && chunkSize < byte.MaxValue) - { - KeyValuePair current = pairs[index]; - if (keySerializer.IsNoneObject(current.Key) || valueSerializer.IsNoneObject(current.Value)) - { - break; - } - - keySerializer.Write(ref context, current.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - valueSerializer.Write(ref context, current.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - chunkSize += 1; - index += 1; - } - - context.Writer.SetByte(chunkSizeOffset, chunkSize); - } - } - - public static Dictionary ReadData(ref ReadContext context) - { - Serializer keySerializer = KeySerializer; - Serializer valueSerializer = ValueSerializer; - int totalLength = checked((int)context.Reader.ReadVarUInt32()); - if (totalLength == 0) - { - return []; - } - - Dictionary map = new(totalLength); - bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; - bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; - bool canonicalizeValues = context.TrackRef && valueSerializer.IsReferenceTrackableType; - - int readCount = 0; - while (readCount < totalLength) - { - byte header = context.Reader.ReadUInt8(); - bool trackKeyRef = (header & MapBits.TrackingKeyRef) != 0; - bool keyNull = (header & MapBits.KeyNull) != 0; - bool keyDeclared = (header & MapBits.DeclaredKeyType) != 0; - bool trackValueRef = (header & MapBits.TrackingValueRef) != 0; - bool valueNull = (header & MapBits.ValueNull) != 0; - bool valueDeclared = (header & MapBits.DeclaredValueType) != 0; - - if (keyNull && valueNull) - { - map[(TKey)keySerializer.DefaultObject!] = (TValue)valueSerializer.DefaultObject!; - readCount += 1; - continue; - } - - if (keyNull) - { - DynamicTypeInfo? valueDynamicInfo = null; - if (!valueDeclared) - { - if (valueDynamicType) - { - valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); - } - else - { - valueSerializer.ReadTypeInfo(ref context); - } - } - - if (valueDynamicInfo is not null) - { - context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); - } - - TValue value = ReadValueElement( - ref context, - trackValueRef, - false, - canonicalizeValues, - valueSerializer); - if (valueDynamicInfo is not null) - { - context.ClearDynamicTypeInfo(typeof(TValue)); - } - - map[(TKey)keySerializer.DefaultObject!] = value; - readCount += 1; - continue; - } - - if (valueNull) - { - DynamicTypeInfo? keyDynamicInfo = null; - if (!keyDeclared) - { - if (keyDynamicType) - { - keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); - } - else - { - keySerializer.ReadTypeInfo(ref context); - } - } - - if (keyDynamicInfo is not null) - { - context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); - } - - TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); - if (keyDynamicInfo is not null) - { - context.ClearDynamicTypeInfo(typeof(TKey)); - } - - map[key] = (TValue)valueSerializer.DefaultObject!; - readCount += 1; - continue; - } - - int chunkSize = context.Reader.ReadUInt8(); - if (keyDynamicType || valueDynamicType) - { - for (int i = 0; i < chunkSize; i++) - { - DynamicTypeInfo? keyDynamicInfo = null; - DynamicTypeInfo? valueDynamicInfo = null; - - if (!keyDeclared) - { - if (keyDynamicType) - { - keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); - } - else - { - keySerializer.ReadTypeInfo(ref context); - } - } - - if (!valueDeclared) - { - if (valueDynamicType) - { - valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); - } - else - { - valueSerializer.ReadTypeInfo(ref context); - } - } - - if (keyDynamicInfo is not null) - { - context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); - } - - TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); - if (keyDynamicInfo is not null) - { - context.ClearDynamicTypeInfo(typeof(TKey)); - } - - if (valueDynamicInfo is not null) - { - context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); - } - - TValue value = ReadValueElement( - ref context, - trackValueRef, - false, - canonicalizeValues, - valueSerializer); - if (valueDynamicInfo is not null) - { - context.ClearDynamicTypeInfo(typeof(TValue)); - } - - map[key] = value; - } - - readCount += chunkSize; - continue; - } - - if (!keyDeclared) - { - keySerializer.ReadTypeInfo(ref context); - } - - if (!valueDeclared) - { - valueSerializer.ReadTypeInfo(ref context); - } - - for (int i = 0; i < chunkSize; i++) - { - TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); - TValue value = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, valueSerializer); - map[key] = value; - } - - if (!keyDeclared) - { - context.ClearDynamicTypeInfo(typeof(TKey)); - } - - if (!valueDeclared) - { - context.ClearDynamicTypeInfo(typeof(TValue)); - } - - readCount += chunkSize; - } - - return map; - } - - private static void WriteDynamicMapPairs( - KeyValuePair[] pairs, - ref WriteContext context, - bool hasGenerics, - bool trackKeyRef, - bool trackValueRef, - bool keyDeclared, - bool valueDeclared, - bool keyDynamicType, - bool valueDynamicType, - Serializer keySerializer, - Serializer valueSerializer) - { - foreach (KeyValuePair pair in pairs) - { - bool keyIsNull = keySerializer.IsNoneObject(pair.Key); - bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); - byte header = 0; - if (trackKeyRef) - { - header |= MapBits.TrackingKeyRef; - } - - if (trackValueRef) - { - header |= MapBits.TrackingValueRef; - } - - if (keyIsNull) - { - header |= MapBits.KeyNull; - } - else if (!keyDynamicType && keyDeclared) - { - header |= MapBits.DeclaredKeyType; - } - - if (valueIsNull) - { - header |= MapBits.ValueNull; - } - else if (!valueDynamicType && valueDeclared) - { - header |= MapBits.DeclaredValueType; - } - - context.Writer.WriteUInt8(header); - if (keyIsNull && valueIsNull) - { - continue; - } - - if (keyIsNull) - { - if (!valueDeclared) - { - if (valueDynamicType) - { - DynamicAnyCodec.WriteAnyTypeInfo(pair.Value!, ref context); - } - else - { - valueSerializer.WriteTypeInfo(ref context); - } - } - - valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - continue; - } - - if (valueIsNull) - { - if (!keyDeclared) - { - if (keyDynamicType) - { - DynamicAnyCodec.WriteAnyTypeInfo(pair.Key!, ref context); - } - else - { - keySerializer.WriteTypeInfo(ref context); - } - } - - keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - continue; - } - - context.Writer.WriteUInt8(1); - if (!keyDeclared) - { - if (keyDynamicType) - { - DynamicAnyCodec.WriteAnyTypeInfo(pair.Key!, ref context); - } - else - { - keySerializer.WriteTypeInfo(ref context); - } - } - - if (!valueDeclared) - { - if (valueDynamicType) - { - DynamicAnyCodec.WriteAnyTypeInfo(pair.Value!, ref context); - } - else - { - valueSerializer.WriteTypeInfo(ref context); - } - } - - keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); - } - } - - private static TValue ReadValueElement( - ref ReadContext context, - bool trackValueRef, - bool readTypeInfo, - bool canonicalizeValues, - Serializer valueSerializer) - { - if (trackValueRef || !canonicalizeValues) - { - return valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo); - } - - int start = context.Reader.Cursor; - TValue value = valueSerializer.Read(ref context, RefMode.None, readTypeInfo); - int end = context.Reader.Cursor; - return context.CanonicalizeNonTrackingReference(value, start, end); - } -} diff --git a/csharp/src/Fory/ForyMap.cs b/csharp/src/Fory/ForyMap.cs index 41a582b94b..17010560f8 100644 --- a/csharp/src/Fory/ForyMap.cs +++ b/csharp/src/Fory/ForyMap.cs @@ -127,8 +127,27 @@ public static void WriteData(ref WriteContext context, in ForyMap bool trackValueRef = context.TrackRef && valueSerializer.IsReferenceTrackableType; bool keyDeclared = hasGenerics && !keySerializer.StaticTypeId.NeedsTypeInfoForField(); bool valueDeclared = hasGenerics && !valueSerializer.StaticTypeId.NeedsTypeInfoForField(); + bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + KeyValuePair[] pairs = [.. map]; + if (keyDynamicType || valueDynamicType) + { + WriteDynamicMapPairs( + pairs, + ref context, + hasGenerics, + trackKeyRef, + trackValueRef, + keyDeclared, + valueDeclared, + keyDynamicType, + valueDynamicType, + keySerializer, + valueSerializer); + return; + } - foreach (KeyValuePair entry in map) + foreach (KeyValuePair entry in pairs) { bool keyIsNull = entry.Key is null || keySerializer.IsNoneObject(entry.Key); bool valueIsNull = valueSerializer.IsNoneObject(entry.Value); @@ -260,34 +279,12 @@ public static ForyMap ReadData(ref ReadContext context) if (keyNull) { - DynamicTypeInfo? valueDynamicInfo = null; - if (!valueDeclared) - { - if (valueDynamicType) - { - valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); - } - else - { - valueSerializer.ReadTypeInfo(ref context); - } - } - - if (valueDynamicInfo is not null) - { - context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); - } - TValue valueRead = ReadValueElement( ref context, trackValueRef, - false, + !valueDeclared, canonicalizeValues, valueSerializer); - if (valueDynamicInfo is not null) - { - context.ClearDynamicTypeInfo(typeof(TValue)); - } map.Add(default, valueRead); readCount += 1; @@ -296,29 +293,10 @@ public static ForyMap ReadData(ref ReadContext context) if (valueNull) { - DynamicTypeInfo? keyDynamicInfo = null; - if (!keyDeclared) - { - if (keyDynamicType) - { - keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); - } - else - { - keySerializer.ReadTypeInfo(ref context); - } - } - - if (keyDynamicInfo is not null) - { - context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); - } - - TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); - if (keyDynamicInfo is not null) - { - context.ClearDynamicTypeInfo(typeof(TKey)); - } + TKey key = keySerializer.Read( + ref context, + trackKeyRef ? RefMode.Tracking : RefMode.None, + !keyDeclared); map.Add(key, (TValue)valueSerializer.DefaultObject!); readCount += 1; @@ -424,6 +402,120 @@ public static ForyMap ReadData(ref ReadContext context) return map; } + private static void WriteDynamicMapPairs( + KeyValuePair[] pairs, + ref WriteContext context, + bool hasGenerics, + bool trackKeyRef, + bool trackValueRef, + bool keyDeclared, + bool valueDeclared, + bool keyDynamicType, + bool valueDynamicType, + Serializer keySerializer, + Serializer valueSerializer) + { + foreach (KeyValuePair pair in pairs) + { + bool keyIsNull = pair.Key is null || keySerializer.IsNoneObject(pair.Key); + bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); + byte header = 0; + if (trackKeyRef) + { + header |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + header |= MapBits.TrackingValueRef; + } + + if (keyIsNull) + { + header |= MapBits.KeyNull; + } + else if (!keyDynamicType && keyDeclared) + { + header |= MapBits.DeclaredKeyType; + } + + if (valueIsNull) + { + header |= MapBits.ValueNull; + } + else if (!valueDynamicType && valueDeclared) + { + header |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + if (keyIsNull && valueIsNull) + { + continue; + } + + if (keyIsNull) + { + valueSerializer.Write( + ref context, + pair.Value, + trackValueRef ? RefMode.Tracking : RefMode.None, + !valueDeclared, + hasGenerics); + continue; + } + + if (valueIsNull) + { + keySerializer.Write( + ref context, + pair.Key!, + trackKeyRef ? RefMode.Tracking : RefMode.None, + !keyDeclared, + hasGenerics); + continue; + } + + context.Writer.WriteUInt8(1); + if (!keyDeclared) + { + if (keyDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Key!, ref context); + } + else + { + keySerializer.WriteTypeInfo(ref context); + } + } + + if (!valueDeclared) + { + if (valueDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Value!, ref context); + } + else + { + valueSerializer.WriteTypeInfo(ref context); + } + } + + keySerializer.Write( + ref context, + pair.Key!, + trackKeyRef ? RefMode.Tracking : RefMode.None, + false, + hasGenerics); + valueSerializer.Write( + ref context, + pair.Value, + trackValueRef ? RefMode.Tracking : RefMode.None, + false, + hasGenerics); + } + } + private static TValue ReadValueElement( ref ReadContext context, bool trackValueRef, diff --git a/csharp/src/Fory/MapSerializers.cs b/csharp/src/Fory/MapSerializers.cs new file mode 100644 index 0000000000..878e65ab9a --- /dev/null +++ b/csharp/src/Fory/MapSerializers.cs @@ -0,0 +1,474 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +internal static class MapBits +{ + public const byte TrackingKeyRef = 0b0000_0001; + public const byte KeyNull = 0b0000_0010; + public const byte DeclaredKeyType = 0b0000_0100; + public const byte TrackingValueRef = 0b0000_1000; + public const byte ValueNull = 0b0001_0000; + public const byte DeclaredValueType = 0b0010_0000; +} + +public readonly struct MapSerializer : IStaticSerializer, Dictionary> + where TKey : notnull +{ + private static Serializer KeySerializer => SerializerRegistry.Get(); + private static Serializer ValueSerializer => SerializerRegistry.Get(); + + public static ForyTypeId StaticTypeId => ForyTypeId.Map; + public static bool IsNullableType => true; + public static bool IsReferenceTrackableType => true; + public static Dictionary DefaultValue => null!; + public static bool IsNone(in Dictionary value) => value is null; + + public static void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Serializer keySerializer = KeySerializer; + Serializer valueSerializer = ValueSerializer; + Dictionary map = value ?? []; + context.Writer.WriteVarUInt32((uint)map.Count); + if (map.Count == 0) + { + return; + } + + bool trackKeyRef = context.TrackRef && keySerializer.IsReferenceTrackableType; + bool trackValueRef = context.TrackRef && valueSerializer.IsReferenceTrackableType; + bool keyDeclared = hasGenerics && !keySerializer.StaticTypeId.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !valueSerializer.StaticTypeId.NeedsTypeInfoForField(); + bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + + KeyValuePair[] pairs = [.. map]; + if (keyDynamicType || valueDynamicType) + { + WriteDynamicMapPairs( + pairs, + ref context, + hasGenerics, + trackKeyRef, + trackValueRef, + keyDeclared, + valueDeclared, + keyDynamicType, + valueDynamicType, + keySerializer, + valueSerializer); + return; + } + + int index = 0; + while (index < pairs.Length) + { + KeyValuePair pair = pairs[index]; + bool keyIsNull = keySerializer.IsNoneObject(pair.Key); + bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); + if (keyIsNull || valueIsNull) + { + byte header = 0; + if (trackKeyRef) + { + header |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + header |= MapBits.TrackingValueRef; + } + + if (keyIsNull) + { + header |= MapBits.KeyNull; + } + + if (valueIsNull) + { + header |= MapBits.ValueNull; + } + + if (!keyIsNull && keyDeclared) + { + header |= MapBits.DeclaredKeyType; + } + + if (!valueIsNull && valueDeclared) + { + header |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + if (!keyIsNull) + { + if (!keyDeclared) + { + keySerializer.WriteTypeInfo(ref context); + } + + keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + } + + if (!valueIsNull) + { + if (!valueDeclared) + { + valueSerializer.WriteTypeInfo(ref context); + } + + valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + } + + index += 1; + continue; + } + + byte blockHeader = 0; + if (trackKeyRef) + { + blockHeader |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + blockHeader |= MapBits.TrackingValueRef; + } + + if (keyDeclared) + { + blockHeader |= MapBits.DeclaredKeyType; + } + + if (valueDeclared) + { + blockHeader |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(blockHeader); + int chunkSizeOffset = context.Writer.Count; + context.Writer.WriteUInt8(0); + if (!keyDeclared) + { + keySerializer.WriteTypeInfo(ref context); + } + + if (!valueDeclared) + { + valueSerializer.WriteTypeInfo(ref context); + } + + byte chunkSize = 0; + while (index < pairs.Length && chunkSize < byte.MaxValue) + { + KeyValuePair current = pairs[index]; + if (keySerializer.IsNoneObject(current.Key) || valueSerializer.IsNoneObject(current.Value)) + { + break; + } + + keySerializer.Write(ref context, current.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + valueSerializer.Write(ref context, current.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + chunkSize += 1; + index += 1; + } + + context.Writer.SetByte(chunkSizeOffset, chunkSize); + } + } + + public static Dictionary ReadData(ref ReadContext context) + { + Serializer keySerializer = KeySerializer; + Serializer valueSerializer = ValueSerializer; + int totalLength = checked((int)context.Reader.ReadVarUInt32()); + if (totalLength == 0) + { + return []; + } + + Dictionary map = new(totalLength); + bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool canonicalizeValues = context.TrackRef && valueSerializer.IsReferenceTrackableType; + + int readCount = 0; + while (readCount < totalLength) + { + byte header = context.Reader.ReadUInt8(); + bool trackKeyRef = (header & MapBits.TrackingKeyRef) != 0; + bool keyNull = (header & MapBits.KeyNull) != 0; + bool keyDeclared = (header & MapBits.DeclaredKeyType) != 0; + bool trackValueRef = (header & MapBits.TrackingValueRef) != 0; + bool valueNull = (header & MapBits.ValueNull) != 0; + bool valueDeclared = (header & MapBits.DeclaredValueType) != 0; + + if (keyNull && valueNull) + { + map[(TKey)keySerializer.DefaultObject!] = (TValue)valueSerializer.DefaultObject!; + readCount += 1; + continue; + } + + if (keyNull) + { + TValue value = ReadValueElement( + ref context, + trackValueRef, + !valueDeclared, + canonicalizeValues, + valueSerializer); + + map[(TKey)keySerializer.DefaultObject!] = value; + readCount += 1; + continue; + } + + if (valueNull) + { + TKey key = keySerializer.Read( + ref context, + trackKeyRef ? RefMode.Tracking : RefMode.None, + !keyDeclared); + + map[key] = (TValue)valueSerializer.DefaultObject!; + readCount += 1; + continue; + } + + int chunkSize = context.Reader.ReadUInt8(); + if (keyDynamicType || valueDynamicType) + { + for (int i = 0; i < chunkSize; i++) + { + DynamicTypeInfo? keyDynamicInfo = null; + DynamicTypeInfo? valueDynamicInfo = null; + + if (!keyDeclared) + { + if (keyDynamicType) + { + keyDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + keySerializer.ReadTypeInfo(ref context); + } + } + + if (!valueDeclared) + { + if (valueDynamicType) + { + valueDynamicInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); + } + else + { + valueSerializer.ReadTypeInfo(ref context); + } + } + + if (keyDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TKey), keyDynamicInfo); + } + + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + if (keyDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + + if (valueDynamicInfo is not null) + { + context.SetDynamicTypeInfo(typeof(TValue), valueDynamicInfo); + } + + TValue value = ReadValueElement( + ref context, + trackValueRef, + false, + canonicalizeValues, + valueSerializer); + if (valueDynamicInfo is not null) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + + map[key] = value; + } + + readCount += chunkSize; + continue; + } + + if (!keyDeclared) + { + keySerializer.ReadTypeInfo(ref context); + } + + if (!valueDeclared) + { + valueSerializer.ReadTypeInfo(ref context); + } + + for (int i = 0; i < chunkSize; i++) + { + TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); + TValue value = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, valueSerializer); + map[key] = value; + } + + if (!keyDeclared) + { + context.ClearDynamicTypeInfo(typeof(TKey)); + } + + if (!valueDeclared) + { + context.ClearDynamicTypeInfo(typeof(TValue)); + } + + readCount += chunkSize; + } + + return map; + } + + private static void WriteDynamicMapPairs( + KeyValuePair[] pairs, + ref WriteContext context, + bool hasGenerics, + bool trackKeyRef, + bool trackValueRef, + bool keyDeclared, + bool valueDeclared, + bool keyDynamicType, + bool valueDynamicType, + Serializer keySerializer, + Serializer valueSerializer) + { + foreach (KeyValuePair pair in pairs) + { + bool keyIsNull = keySerializer.IsNoneObject(pair.Key); + bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); + byte header = 0; + if (trackKeyRef) + { + header |= MapBits.TrackingKeyRef; + } + + if (trackValueRef) + { + header |= MapBits.TrackingValueRef; + } + + if (keyIsNull) + { + header |= MapBits.KeyNull; + } + else if (!keyDynamicType && keyDeclared) + { + header |= MapBits.DeclaredKeyType; + } + + if (valueIsNull) + { + header |= MapBits.ValueNull; + } + else if (!valueDynamicType && valueDeclared) + { + header |= MapBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + if (keyIsNull && valueIsNull) + { + continue; + } + + if (keyIsNull) + { + valueSerializer.Write( + ref context, + pair.Value, + trackValueRef ? RefMode.Tracking : RefMode.None, + !valueDeclared, + hasGenerics); + continue; + } + + if (valueIsNull) + { + keySerializer.Write( + ref context, + pair.Key, + trackKeyRef ? RefMode.Tracking : RefMode.None, + !keyDeclared, + hasGenerics); + continue; + } + + context.Writer.WriteUInt8(1); + if (!keyDeclared) + { + if (keyDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Key!, ref context); + } + else + { + keySerializer.WriteTypeInfo(ref context); + } + } + + if (!valueDeclared) + { + if (valueDynamicType) + { + DynamicAnyCodec.WriteAnyTypeInfo(pair.Value!, ref context); + } + else + { + valueSerializer.WriteTypeInfo(ref context); + } + } + + keySerializer.Write(ref context, pair.Key, trackKeyRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + valueSerializer.Write(ref context, pair.Value, trackValueRef ? RefMode.Tracking : RefMode.None, false, hasGenerics); + } + } + + private static TValue ReadValueElement( + ref ReadContext context, + bool trackValueRef, + bool readTypeInfo, + bool canonicalizeValues, + Serializer valueSerializer) + { + if (trackValueRef || !canonicalizeValues) + { + return valueSerializer.Read(ref context, trackValueRef ? RefMode.Tracking : RefMode.None, readTypeInfo); + } + + int start = context.Reader.Cursor; + TValue value = valueSerializer.Read(ref context, RefMode.None, readTypeInfo); + int end = context.Reader.Cursor; + return context.CanonicalizeNonTrackingReference(value, start, end); + } +} diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index a5452ff131..ebf6357186 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -257,11 +257,11 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) case ForyTypeId.Float64Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.List: - return DynamicAnyCodec.ReadAnyList(ref context, RefMode.None, false) ?? []; + return DynamicContainerCodec.ReadListPayload(ref context); case ForyTypeId.Set: - return DynamicAnyCodec.ReadAnySet(ref context, RefMode.None, false) ?? new HashSet(); + return DynamicContainerCodec.ReadSetPayload(ref context); case ForyTypeId.Map: - return DynamicAnyCodec.ReadDynamicAnyMapValue(ref context); + return DynamicContainerCodec.ReadMapPayload(ref context); case ForyTypeId.Union: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); case ForyTypeId.Struct: From 010f9e0f1283ad9c306ac8fb9e4f422ad4b4a7b6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 10:45:12 +0800 Subject: [PATCH 06/16] refactor(csharp): rename field encoding API and reuse runtime murmur hash --- .../src/Fory.Generator/ForyObjectGenerator.cs | 18 +- csharp/src/Fory/Attributes.cs | 11 +- csharp/src/Fory/MurmurHash3.cs | 3 +- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 4 +- csharp/tests/Fory.XlangPeer/Program.cs | 162 ++---------------- 5 files changed, 36 insertions(+), 162 deletions(-) diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index daec57b15b..20809bb3f7 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -48,8 +48,8 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator private static readonly DiagnosticDescriptor UnsupportedEncoding = new( id: "FORY003", - title: "Unsupported ForyField encoding", - messageFormat: "Member '{0}' uses unsupported [ForyField] encoding for type '{1}'.", + title: "Unsupported Field encoding", + messageFormat: "Member '{0}' uses unsupported [Field] encoding for type '{1}'.", category: "Fory", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -569,14 +569,22 @@ private static string BuildWriteRefModeExpression(MemberModel member) foreach (AttributeData attribute in memberSymbol.GetAttributes()) { string? attrName = attribute.AttributeClass?.ToDisplayString(); - if (!string.Equals(attrName, "Apache.Fory.ForyFieldAttribute", StringComparison.Ordinal)) + if (!string.Equals(attrName, "Apache.Fory.FieldAttribute", StringComparison.Ordinal)) { continue; } - if (attribute.ConstructorArguments.Length == 1) + foreach (KeyValuePair namedArg in attribute.NamedArguments) { - fieldEncoding = (FieldEncoding)(int)attribute.ConstructorArguments[0].Value!; + if (!string.Equals(namedArg.Key, "Encoding", StringComparison.Ordinal)) + { + continue; + } + + if (namedArg.Value.Value is int encoding) + { + fieldEncoding = (FieldEncoding)encoding; + } } } diff --git a/csharp/src/Fory/Attributes.cs b/csharp/src/Fory/Attributes.cs index 89fa8854bf..84fe5dc380 100644 --- a/csharp/src/Fory/Attributes.cs +++ b/csharp/src/Fory/Attributes.cs @@ -22,7 +22,7 @@ public sealed class ForyObjectAttribute : Attribute { } -public enum ForyFieldEncoding +public enum FieldEncoding { Varint, Fixed, @@ -30,12 +30,7 @@ public enum ForyFieldEncoding } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public sealed class ForyFieldAttribute : Attribute +public sealed class FieldAttribute : Attribute { - public ForyFieldAttribute(ForyFieldEncoding encoding) - { - Encoding = encoding; - } - - public ForyFieldEncoding Encoding { get; } + public FieldEncoding Encoding { get; set; } = FieldEncoding.Varint; } diff --git a/csharp/src/Fory/MurmurHash3.cs b/csharp/src/Fory/MurmurHash3.cs index b934316b41..d36c7f0a2d 100644 --- a/csharp/src/Fory/MurmurHash3.cs +++ b/csharp/src/Fory/MurmurHash3.cs @@ -19,7 +19,7 @@ namespace Apache.Fory; -internal static class MurmurHash3 +public static class MurmurHash3 { public static (ulong H1, ulong H2) X64_128(ReadOnlySpan bytes, ulong seed = 47) { @@ -143,4 +143,3 @@ private static ulong Fmix64(ulong x) return x; } } - diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index 7d9d24d5a7..bc28bc211a 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -68,10 +68,10 @@ public sealed class FieldOrder [ForyObject] public sealed class EncodedNumbers { - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public uint U32Fixed { get; set; } - [ForyField(ForyFieldEncoding.Tagged)] + [Field(Encoding = FieldEncoding.Tagged)] public ulong U64Tagged { get; set; } } diff --git a/csharp/tests/Fory.XlangPeer/Program.cs b/csharp/tests/Fory.XlangPeer/Program.cs index be37d83b2c..8b4db14ca6 100644 --- a/csharp/tests/Fory.XlangPeer/Program.cs +++ b/csharp/tests/Fory.XlangPeer/Program.cs @@ -16,7 +16,6 @@ // under the License. using System.Buffers; -using System.Buffers.Binary; using System.Text; using Apache.Fory; using ForyRuntime = Apache.Fory.Fory; @@ -302,8 +301,8 @@ private static byte[] CaseMurmurHash3(byte[] input) { if (input.Length == 32) { - (ulong h1a, ulong h1b) = PeerMurmurHash3.X64_128([1, 2, 8], 47); - (ulong h2a, ulong h2b) = PeerMurmurHash3.X64_128(Encoding.UTF8.GetBytes("01234567890123456789"), 47); + (ulong h1a, ulong h1b) = MurmurHash3.X64_128([1, 2, 8], 47); + (ulong h2a, ulong h2b) = MurmurHash3.X64_128(Encoding.UTF8.GetBytes("01234567890123456789"), 47); ByteWriter writer = new(); writer.WriteInt64(unchecked((long)h1a)); writer.WriteInt64(unchecked((long)h1b)); @@ -317,7 +316,7 @@ private static byte[] CaseMurmurHash3(byte[] input) ByteReader reader = new(input); long h1 = reader.ReadInt64(); long h2 = reader.ReadInt64(); - (ulong expected1, ulong expected2) = PeerMurmurHash3.X64_128([1, 2, 8], 47); + (ulong expected1, ulong expected2) = MurmurHash3.X64_128([1, 2, 8], 47); Ensure(h1 == unchecked((long)expected1), "murmur hash h1 mismatch"); Ensure(h2 == unchecked((long)expected2), "murmur hash h2 mismatch"); return []; @@ -1246,10 +1245,10 @@ public sealed class CircularRefStruct [ForyObject] public sealed class UnsignedSchemaConsistentSimple { - [ForyField(ForyFieldEncoding.Tagged)] + [Field(Encoding = FieldEncoding.Tagged)] public ulong U64Tagged { get; set; } - [ForyField(ForyFieldEncoding.Tagged)] + [Field(Encoding = FieldEncoding.Tagged)] public ulong? U64TaggedNullable { get; set; } } @@ -1260,30 +1259,30 @@ public sealed class UnsignedSchemaConsistent public ushort U16Field { get; set; } public uint U32VarField { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public uint U32FixedField { get; set; } public ulong U64VarField { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public ulong U64FixedField { get; set; } - [ForyField(ForyFieldEncoding.Tagged)] + [Field(Encoding = FieldEncoding.Tagged)] public ulong U64TaggedField { get; set; } public byte? U8NullableField { get; set; } public ushort? U16NullableField { get; set; } public uint? U32VarNullableField { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public uint? U32FixedNullableField { get; set; } public ulong? U64VarNullableField { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public ulong? U64FixedNullableField { get; set; } - [ForyField(ForyFieldEncoding.Tagged)] + [Field(Encoding = FieldEncoding.Tagged)] public ulong? U64TaggedNullableField { get; set; } } @@ -1294,156 +1293,29 @@ public sealed class UnsignedSchemaCompatible public ushort? U16Field1 { get; set; } public uint? U32VarField1 { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public uint? U32FixedField1 { get; set; } public ulong? U64VarField1 { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public ulong? U64FixedField1 { get; set; } - [ForyField(ForyFieldEncoding.Tagged)] + [Field(Encoding = FieldEncoding.Tagged)] public ulong? U64TaggedField1 { get; set; } public byte U8Field2 { get; set; } public ushort U16Field2 { get; set; } public uint U32VarField2 { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public uint U32FixedField2 { get; set; } public ulong U64VarField2 { get; set; } - [ForyField(ForyFieldEncoding.Fixed)] + [Field(Encoding = FieldEncoding.Fixed)] public ulong U64FixedField2 { get; set; } - [ForyField(ForyFieldEncoding.Tagged)] + [Field(Encoding = FieldEncoding.Tagged)] public ulong U64TaggedField2 { get; set; } } - -internal static class PeerMurmurHash3 -{ - public static (ulong H1, ulong H2) X64_128(ReadOnlySpan bytes, ulong seed) - { - const ulong c1 = 0x87c37b91114253d5; - const ulong c2 = 0x4cf5ad432745937f; - - ulong h1 = seed; - ulong h2 = seed; - - int nblocks = bytes.Length / 16; - for (int i = 0; i < nblocks; i++) - { - int offset = i * 16; - ulong k1 = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(offset, 8)); - ulong k2 = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(offset + 8, 8)); - - k1 *= c1; - k1 = RotateLeft(k1, 31); - k1 *= c2; - h1 ^= k1; - - h1 = RotateLeft(h1, 27); - h1 += h2; - h1 = h1 * 5 + 0x52dce729; - - k2 *= c2; - k2 = RotateLeft(k2, 33); - k2 *= c1; - h2 ^= k2; - - h2 = RotateLeft(h2, 31); - h2 += h1; - h2 = h2 * 5 + 0x38495ab5; - } - - ulong tk1 = 0; - ulong tk2 = 0; - int tailStart = nblocks * 16; - ReadOnlySpan tail = bytes.Slice(tailStart); - switch (bytes.Length & 15) - { - case 15: - tk2 ^= (ulong)tail[14] << 48; - goto case 14; - case 14: - tk2 ^= (ulong)tail[13] << 40; - goto case 13; - case 13: - tk2 ^= (ulong)tail[12] << 32; - goto case 12; - case 12: - tk2 ^= (ulong)tail[11] << 24; - goto case 11; - case 11: - tk2 ^= (ulong)tail[10] << 16; - goto case 10; - case 10: - tk2 ^= (ulong)tail[9] << 8; - goto case 9; - case 9: - tk2 ^= tail[8]; - tk2 *= c2; - tk2 = RotateLeft(tk2, 33); - tk2 *= c1; - h2 ^= tk2; - goto case 8; - case 8: - tk1 ^= (ulong)tail[7] << 56; - goto case 7; - case 7: - tk1 ^= (ulong)tail[6] << 48; - goto case 6; - case 6: - tk1 ^= (ulong)tail[5] << 40; - goto case 5; - case 5: - tk1 ^= (ulong)tail[4] << 32; - goto case 4; - case 4: - tk1 ^= (ulong)tail[3] << 24; - goto case 3; - case 3: - tk1 ^= (ulong)tail[2] << 16; - goto case 2; - case 2: - tk1 ^= (ulong)tail[1] << 8; - goto case 1; - case 1: - tk1 ^= tail[0]; - tk1 *= c1; - tk1 = RotateLeft(tk1, 31); - tk1 *= c2; - h1 ^= tk1; - break; - } - - h1 ^= (ulong)bytes.Length; - h2 ^= (ulong)bytes.Length; - h1 += h2; - h2 += h1; - - h1 = Fmix64(h1); - h2 = Fmix64(h2); - - h1 += h2; - h2 += h1; - - return (h1, h2); - } - - private static ulong RotateLeft(ulong value, int count) - { - return (value << count) | (value >> (64 - count)); - } - - private static ulong Fmix64(ulong x) - { - x ^= x >> 33; - x *= 0xff51afd7ed558ccd; - x ^= x >> 33; - x *= 0xc4ceb9fe1a85ec53; - x ^= x >> 33; - return x; - } -} From 0ea44ede4f332e3964e484a7140e7b1bdb0897ed Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 11:01:43 +0800 Subject: [PATCH 07/16] refactor(csharp): rename type and config APIs --- .../src/Fory.Generator/ForyObjectGenerator.cs | 30 ++--- csharp/src/Fory/AnySerializer.cs | 4 +- csharp/src/Fory/CollectionSerializers.cs | 42 +++--- csharp/src/Fory/{ForyConfig.cs => Config.cs} | 4 +- csharp/src/Fory/Context.cs | 2 +- csharp/src/Fory/EnumSerializer.cs | 2 +- csharp/src/Fory/FieldSkipper.cs | 38 +++--- csharp/src/Fory/Fory.cs | 4 +- csharp/src/Fory/ForyMap.cs | 10 +- csharp/src/Fory/MapSerializers.cs | 10 +- csharp/src/Fory/OptionalSerializer.cs | 2 +- csharp/src/Fory/PrimitiveSerializers.cs | 46 +++---- csharp/src/Fory/Serializer.cs | 102 +++++++------- csharp/src/Fory/{ForyTypeId.cs => TypeId.cs} | 42 +++--- csharp/src/Fory/TypeMeta.cs | 14 +- csharp/src/Fory/TypeResolver.cs | 124 +++++++++--------- csharp/src/Fory/TypedSerializerBinding.cs | 8 +- csharp/src/Fory/Union.cs | 4 +- csharp/src/Fory/UnionSerializer.cs | 2 +- csharp/tests/Fory.XlangPeer/Program.cs | 2 +- 20 files changed, 246 insertions(+), 246 deletions(-) rename csharp/src/Fory/{ForyConfig.cs => Config.cs} (97%) rename csharp/src/Fory/{ForyTypeId.cs => TypeId.cs} (69%) diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index 20809bb3f7..d085a76ed2 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -147,17 +147,17 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" return nullable ? global::Apache.Fory.RefMode.NullOnly : global::Apache.Fory.RefMode.None;"); sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" private static bool __ForyNeedsTypeInfoForField(global::Apache.Fory.ForyTypeId typeId)"); + sb.AppendLine(" private static bool __ForyNeedsTypeInfoForField(global::Apache.Fory.TypeId typeId)"); sb.AppendLine(" {"); sb.AppendLine(" return typeId switch"); sb.AppendLine(" {"); - sb.AppendLine(" global::Apache.Fory.ForyTypeId.Struct or"); - sb.AppendLine(" global::Apache.Fory.ForyTypeId.CompatibleStruct or"); - sb.AppendLine(" global::Apache.Fory.ForyTypeId.NamedStruct or"); - sb.AppendLine(" global::Apache.Fory.ForyTypeId.NamedCompatibleStruct or"); - sb.AppendLine(" global::Apache.Fory.ForyTypeId.Ext or"); - sb.AppendLine(" global::Apache.Fory.ForyTypeId.NamedExt or"); - sb.AppendLine(" global::Apache.Fory.ForyTypeId.Unknown => true,"); + sb.AppendLine(" global::Apache.Fory.TypeId.Struct or"); + sb.AppendLine(" global::Apache.Fory.TypeId.CompatibleStruct or"); + sb.AppendLine(" global::Apache.Fory.TypeId.NamedStruct or"); + sb.AppendLine(" global::Apache.Fory.TypeId.NamedCompatibleStruct or"); + sb.AppendLine(" global::Apache.Fory.TypeId.Ext or"); + sb.AppendLine(" global::Apache.Fory.TypeId.NamedExt or"); + sb.AppendLine(" global::Apache.Fory.TypeId.Unknown => true,"); sb.AppendLine(" _ => false,"); sb.AppendLine(" };"); sb.AppendLine(" }"); @@ -169,7 +169,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(");"); sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" public static global::Apache.Fory.ForyTypeId StaticTypeId => global::Apache.Fory.ForyTypeId.Struct;"); + sb.AppendLine(" public static global::Apache.Fory.TypeId StaticTypeId => global::Apache.Fory.TypeId.Struct;"); if (model.Kind == DeclKind.Class) { sb.AppendLine(" public static bool IsNullableType => true;"); @@ -248,7 +248,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" foreach (global::Apache.Fory.TypeMetaFieldInfo remoteField in typeMeta.Fields)"); sb.AppendLine(" {"); sb.AppendLine(" global::Apache.Fory.RefMode remoteRefMode = __ForyRefMode(remoteField.FieldType.Nullable, remoteField.FieldType.TrackRef);"); - sb.AppendLine(" bool remoteReadTypeInfo = __ForyNeedsTypeInfoForField((global::Apache.Fory.ForyTypeId)remoteField.FieldType.TypeId);"); + sb.AppendLine(" bool remoteReadTypeInfo = __ForyNeedsTypeInfoForField((global::Apache.Fory.TypeId)remoteField.FieldType.TypeId);"); sb.AppendLine(" switch (remoteField.FieldName)"); sb.AppendLine(" {"); foreach (MemberModel member in model.SortedMembers) @@ -657,7 +657,7 @@ private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel( null, 0); return new TypeMetaFieldTypeModel( - "(uint)global::Apache.Fory.ForyTypeId.List", + "(uint)global::Apache.Fory.TypeId.List", nullable, false, ImmutableArray.Create(element)); @@ -673,7 +673,7 @@ private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel( null, 0); return new TypeMetaFieldTypeModel( - "(uint)global::Apache.Fory.ForyTypeId.Set", + "(uint)global::Apache.Fory.TypeId.Set", nullable, false, ImmutableArray.Create(element)); @@ -696,7 +696,7 @@ private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel( null, 0); return new TypeMetaFieldTypeModel( - "(uint)global::Apache.Fory.ForyTypeId.Map", + "(uint)global::Apache.Fory.TypeId.Map", nullable, false, ImmutableArray.Create(key, value)); @@ -714,7 +714,7 @@ private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel( if (IsUnionType(unwrapped)) { return new TypeMetaFieldTypeModel( - "(uint)global::Apache.Fory.ForyTypeId.Union", + "(uint)global::Apache.Fory.TypeId.Union", nullable, true, ImmutableArray.Empty); @@ -723,7 +723,7 @@ private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel( if (dynamicAnyKind == DynamicAnyKind.AnyValue) { return new TypeMetaFieldTypeModel( - "(uint)global::Apache.Fory.ForyTypeId.Unknown", + "(uint)global::Apache.Fory.TypeId.Unknown", nullable, true, ImmutableArray.Empty); diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index 4da8c5b1be..a0f977c41e 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -19,7 +19,7 @@ namespace Apache.Fory; public readonly struct DynamicAnyObjectSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Unknown; + public static TypeId StaticTypeId => TypeId.Unknown; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; public static object? DefaultValue => null; @@ -159,7 +159,7 @@ public static class DynamicAnyCodec { internal static void WriteAnyTypeInfo(object value, ref WriteContext context) { - if (DynamicContainerCodec.TryGetTypeId(value, out ForyTypeId containerTypeId)) + if (DynamicContainerCodec.TryGetTypeId(value, out TypeId containerTypeId)) { context.Writer.WriteUInt8((byte)containerTypeId); return; diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index de8003427c..61b9cc4567 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -60,7 +60,7 @@ public static void WriteCollectionData( bool trackRef = context.TrackRef && elementSerializer.IsReferenceTrackableType; bool declaredElementType = hasGenerics && !elementSerializer.StaticTypeId.NeedsTypeInfoForField(); - bool dynamicElementType = elementSerializer.StaticTypeId == ForyTypeId.Unknown; + bool dynamicElementType = elementSerializer.StaticTypeId == TypeId.Unknown; byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType; if (trackRef) @@ -274,24 +274,24 @@ private static T ReadCollectionElementDataWithCanonicalization( internal static class DynamicContainerCodec { - public static bool TryGetTypeId(object value, out ForyTypeId typeId) + public static bool TryGetTypeId(object value, out TypeId typeId) { if (value is IDictionary) { - typeId = ForyTypeId.Map; + typeId = TypeId.Map; return true; } Type valueType = value.GetType(); if (value is IList && !valueType.IsArray) { - typeId = ForyTypeId.List; + typeId = TypeId.List; return true; } if (IsSet(valueType)) { - typeId = ForyTypeId.Set; + typeId = TypeId.Set; return true; } @@ -393,61 +393,61 @@ private static bool IsSet(Type valueType) internal static class PrimitiveArrayCodec { - public static ForyTypeId? PrimitiveArrayTypeId(Type elementType) + public static TypeId? PrimitiveArrayTypeId(Type elementType) { if (elementType == typeof(byte)) { - return ForyTypeId.Binary; + return TypeId.Binary; } if (elementType == typeof(bool)) { - return ForyTypeId.BoolArray; + return TypeId.BoolArray; } if (elementType == typeof(sbyte)) { - return ForyTypeId.Int8Array; + return TypeId.Int8Array; } if (elementType == typeof(short)) { - return ForyTypeId.Int16Array; + return TypeId.Int16Array; } if (elementType == typeof(int)) { - return ForyTypeId.Int32Array; + return TypeId.Int32Array; } if (elementType == typeof(long)) { - return ForyTypeId.Int64Array; + return TypeId.Int64Array; } if (elementType == typeof(ushort)) { - return ForyTypeId.UInt16Array; + return TypeId.UInt16Array; } if (elementType == typeof(uint)) { - return ForyTypeId.UInt32Array; + return TypeId.UInt32Array; } if (elementType == typeof(ulong)) { - return ForyTypeId.UInt64Array; + return TypeId.UInt64Array; } if (elementType == typeof(float)) { - return ForyTypeId.Float32Array; + return TypeId.Float32Array; } if (elementType == typeof(double)) { - return ForyTypeId.Float64Array; + return TypeId.Float64Array; } return null; @@ -739,9 +739,9 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) public readonly struct ArraySerializer : IStaticSerializer, T[]> { private static Serializer ElementSerializer => SerializerRegistry.Get(); - private static readonly ForyTypeId? PrimitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); + private static readonly TypeId? PrimitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); - public static ForyTypeId StaticTypeId => PrimitiveArrayTypeId ?? ForyTypeId.List; + public static TypeId StaticTypeId => PrimitiveArrayTypeId ?? TypeId.List; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; public static T[] DefaultValue => null!; @@ -779,7 +779,7 @@ public static T[] ReadData(ref ReadContext context) { private static Serializer ElementSerializer => SerializerRegistry.Get(); - public static ForyTypeId StaticTypeId => ForyTypeId.List; + public static TypeId StaticTypeId => TypeId.List; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; public static List DefaultValue => null!; @@ -801,7 +801,7 @@ public static List ReadData(ref ReadContext context) { private static Serializer> ListSerializer => SerializerRegistry.Get>(); - public static ForyTypeId StaticTypeId => ForyTypeId.Set; + public static TypeId StaticTypeId => TypeId.Set; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; public static HashSet DefaultValue => null!; diff --git a/csharp/src/Fory/ForyConfig.cs b/csharp/src/Fory/Config.cs similarity index 97% rename from csharp/src/Fory/ForyConfig.cs rename to csharp/src/Fory/Config.cs index 434a2356cd..5fd8f2609c 100644 --- a/csharp/src/Fory/ForyConfig.cs +++ b/csharp/src/Fory/Config.cs @@ -17,7 +17,7 @@ namespace Apache.Fory; -public sealed record ForyConfig( +public sealed record Config( bool Xlang = true, bool TrackRef = false, bool Compatible = false, @@ -78,7 +78,7 @@ public ForyBuilder MaxDepth(int value) public Fory Build() { return new Fory( - new ForyConfig( + new Config( Xlang: _xlang, TrackRef: _trackRef, Compatible: _compatible, diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs index 4f7d5c45d4..9fa9deaeb6 100644 --- a/csharp/src/Fory/Context.cs +++ b/csharp/src/Fory/Context.cs @@ -136,7 +136,7 @@ public void Reset() } public sealed record DynamicTypeInfo( - ForyTypeId WireTypeId, + TypeId WireTypeId, uint? UserTypeId, MetaString? NamespaceName, MetaString? TypeName, diff --git a/csharp/src/Fory/EnumSerializer.cs b/csharp/src/Fory/EnumSerializer.cs index 7efd3c31e5..4b17c5ac1f 100644 --- a/csharp/src/Fory/EnumSerializer.cs +++ b/csharp/src/Fory/EnumSerializer.cs @@ -19,7 +19,7 @@ namespace Apache.Fory; public readonly struct EnumSerializer : IStaticSerializer, TEnum> where TEnum : struct, Enum { - public static ForyTypeId StaticTypeId => ForyTypeId.Enum; + public static TypeId StaticTypeId => TypeId.Enum; public static TEnum DefaultValue => default; public static void WriteData(ref WriteContext context, in TEnum value, bool hasGenerics) diff --git a/csharp/src/Fory/FieldSkipper.cs b/csharp/src/Fory/FieldSkipper.cs index 16c06f2292..75cd05be1e 100644 --- a/csharp/src/Fory/FieldSkipper.cs +++ b/csharp/src/Fory/FieldSkipper.cs @@ -56,56 +56,56 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie RefMode refMode = RefModeExtensions.From(fieldType.Nullable, fieldType.TrackRef); switch (fieldType.TypeId) { - case (uint)ForyTypeId.Bool: + case (uint)TypeId.Bool: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.Int8: + case (uint)TypeId.Int8: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.Int16: + case (uint)TypeId.Int16: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.VarInt32: + case (uint)TypeId.VarInt32: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.VarInt64: + case (uint)TypeId.VarInt64: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.Float32: + case (uint)TypeId.Float32: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.Float64: + case (uint)TypeId.Float64: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.String: + case (uint)TypeId.String: return SerializerRegistry.Get().Read(ref context, refMode, false); - case (uint)ForyTypeId.List: + case (uint)TypeId.List: { - if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)ForyTypeId.String) + if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)TypeId.String) { throw new ForyInvalidDataException("unsupported compatible list element type"); } return SerializerRegistry.Get>().Read(ref context, refMode, false); } - case (uint)ForyTypeId.Set: + case (uint)TypeId.Set: { - if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)ForyTypeId.String) + if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)TypeId.String) { throw new ForyInvalidDataException("unsupported compatible set element type"); } return SerializerRegistry.Get>().Read(ref context, refMode, false); } - case (uint)ForyTypeId.Map: + case (uint)TypeId.Map: { if (fieldType.Generics.Count != 2 || - fieldType.Generics[0].TypeId != (uint)ForyTypeId.String || - fieldType.Generics[1].TypeId != (uint)ForyTypeId.String) + fieldType.Generics[0].TypeId != (uint)TypeId.String || + fieldType.Generics[1].TypeId != (uint)TypeId.String) { throw new ForyInvalidDataException("unsupported compatible map key/value type"); } return SerializerRegistry.Get>().Read(ref context, refMode, false); } - case (uint)ForyTypeId.Enum: + case (uint)TypeId.Enum: return ReadEnumOrdinal(ref context, refMode); - case (uint)ForyTypeId.Union: - case (uint)ForyTypeId.TypedUnion: - case (uint)ForyTypeId.NamedUnion: + case (uint)TypeId.Union: + case (uint)TypeId.TypedUnion: + case (uint)TypeId.NamedUnion: return SerializerRegistry.Get().Read(ref context, refMode, false); default: throw new ForyInvalidDataException($"unsupported compatible field type id {fieldType.TypeId}"); diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs index d2f107ec0d..8ebdcc122d 100644 --- a/csharp/src/Fory/Fory.cs +++ b/csharp/src/Fory/Fory.cs @@ -23,13 +23,13 @@ public sealed class Fory { private readonly TypeResolver _typeResolver; - internal Fory(ForyConfig config) + internal Fory(Config config) { Config = config; _typeResolver = new TypeResolver(); } - public ForyConfig Config { get; } + public Config Config { get; } public static ForyBuilder Builder() { diff --git a/csharp/src/Fory/ForyMap.cs b/csharp/src/Fory/ForyMap.cs index 17010560f8..6ce283ef8f 100644 --- a/csharp/src/Fory/ForyMap.cs +++ b/csharp/src/Fory/ForyMap.cs @@ -106,7 +106,7 @@ IEnumerator IEnumerable.GetEnumerator() private static Serializer KeySerializer => SerializerRegistry.Get(); private static Serializer ValueSerializer => SerializerRegistry.Get(); - public static ForyTypeId StaticTypeId => ForyTypeId.Map; + public static TypeId StaticTypeId => TypeId.Map; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; public static ForyMap DefaultValue => null!; @@ -127,8 +127,8 @@ public static void WriteData(ref WriteContext context, in ForyMap bool trackValueRef = context.TrackRef && valueSerializer.IsReferenceTrackableType; bool keyDeclared = hasGenerics && !keySerializer.StaticTypeId.NeedsTypeInfoForField(); bool valueDeclared = hasGenerics && !valueSerializer.StaticTypeId.NeedsTypeInfoForField(); - bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; - bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool keyDynamicType = keySerializer.StaticTypeId == TypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == TypeId.Unknown; KeyValuePair[] pairs = [.. map]; if (keyDynamicType || valueDynamicType) { @@ -255,8 +255,8 @@ public static ForyMap ReadData(ref ReadContext context) } ForyMap map = new(); - bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; - bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool keyDynamicType = keySerializer.StaticTypeId == TypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == TypeId.Unknown; bool canonicalizeValues = context.TrackRef && valueSerializer.IsReferenceTrackableType; int readCount = 0; diff --git a/csharp/src/Fory/MapSerializers.cs b/csharp/src/Fory/MapSerializers.cs index 878e65ab9a..3d981469e6 100644 --- a/csharp/src/Fory/MapSerializers.cs +++ b/csharp/src/Fory/MapSerializers.cs @@ -33,7 +33,7 @@ internal static class MapBits private static Serializer KeySerializer => SerializerRegistry.Get(); private static Serializer ValueSerializer => SerializerRegistry.Get(); - public static ForyTypeId StaticTypeId => ForyTypeId.Map; + public static TypeId StaticTypeId => TypeId.Map; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; public static Dictionary DefaultValue => null!; @@ -54,8 +54,8 @@ public static void WriteData(ref WriteContext context, in Dictionary[] pairs = [.. map]; if (keyDynamicType || valueDynamicType) @@ -203,8 +203,8 @@ public static Dictionary ReadData(ref ReadContext context) } Dictionary map = new(totalLength); - bool keyDynamicType = keySerializer.StaticTypeId == ForyTypeId.Unknown; - bool valueDynamicType = valueSerializer.StaticTypeId == ForyTypeId.Unknown; + bool keyDynamicType = keySerializer.StaticTypeId == TypeId.Unknown; + bool valueDynamicType = valueSerializer.StaticTypeId == TypeId.Unknown; bool canonicalizeValues = context.TrackRef && valueSerializer.IsReferenceTrackableType; int readCount = 0; diff --git a/csharp/src/Fory/OptionalSerializer.cs b/csharp/src/Fory/OptionalSerializer.cs index dcccc1a128..e0ba4782a6 100644 --- a/csharp/src/Fory/OptionalSerializer.cs +++ b/csharp/src/Fory/OptionalSerializer.cs @@ -21,7 +21,7 @@ namespace Apache.Fory; { private static Serializer WrappedSerializer => SerializerRegistry.Get(); - public static ForyTypeId StaticTypeId => WrappedSerializer.StaticTypeId; + public static TypeId StaticTypeId => WrappedSerializer.StaticTypeId; public static bool IsNullableType => true; diff --git a/csharp/src/Fory/PrimitiveSerializers.cs b/csharp/src/Fory/PrimitiveSerializers.cs index 77cf2828a8..957626b179 100644 --- a/csharp/src/Fory/PrimitiveSerializers.cs +++ b/csharp/src/Fory/PrimitiveSerializers.cs @@ -70,7 +70,7 @@ private static ForyTimestamp Normalize(long seconds, long nanos) public readonly struct BoolSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Bool; + public static TypeId StaticTypeId => TypeId.Bool; public static bool DefaultValue => false; public static void WriteData(ref WriteContext context, in bool value, bool hasGenerics) { @@ -86,7 +86,7 @@ public static bool ReadData(ref ReadContext context) public readonly struct Int8Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Int8; + public static TypeId StaticTypeId => TypeId.Int8; public static sbyte DefaultValue => 0; public static void WriteData(ref WriteContext context, in sbyte value, bool hasGenerics) { @@ -102,7 +102,7 @@ public static sbyte ReadData(ref ReadContext context) public readonly struct Int16Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Int16; + public static TypeId StaticTypeId => TypeId.Int16; public static short DefaultValue => 0; public static void WriteData(ref WriteContext context, in short value, bool hasGenerics) { @@ -118,7 +118,7 @@ public static short ReadData(ref ReadContext context) public readonly struct Int32Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.VarInt32; + public static TypeId StaticTypeId => TypeId.VarInt32; public static int DefaultValue => 0; public static void WriteData(ref WriteContext context, in int value, bool hasGenerics) { @@ -134,7 +134,7 @@ public static int ReadData(ref ReadContext context) public readonly struct Int64Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.VarInt64; + public static TypeId StaticTypeId => TypeId.VarInt64; public static long DefaultValue => 0; public static void WriteData(ref WriteContext context, in long value, bool hasGenerics) { @@ -150,7 +150,7 @@ public static long ReadData(ref ReadContext context) public readonly struct UInt8Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.UInt8; + public static TypeId StaticTypeId => TypeId.UInt8; public static byte DefaultValue => 0; public static void WriteData(ref WriteContext context, in byte value, bool hasGenerics) { @@ -166,7 +166,7 @@ public static byte ReadData(ref ReadContext context) public readonly struct UInt16Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.UInt16; + public static TypeId StaticTypeId => TypeId.UInt16; public static ushort DefaultValue => 0; public static void WriteData(ref WriteContext context, in ushort value, bool hasGenerics) { @@ -182,7 +182,7 @@ public static ushort ReadData(ref ReadContext context) public readonly struct UInt32Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.VarUInt32; + public static TypeId StaticTypeId => TypeId.VarUInt32; public static uint DefaultValue => 0; public static void WriteData(ref WriteContext context, in uint value, bool hasGenerics) { @@ -198,7 +198,7 @@ public static uint ReadData(ref ReadContext context) public readonly struct UInt64Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.VarUInt64; + public static TypeId StaticTypeId => TypeId.VarUInt64; public static ulong DefaultValue => 0; public static void WriteData(ref WriteContext context, in ulong value, bool hasGenerics) { @@ -214,7 +214,7 @@ public static ulong ReadData(ref ReadContext context) public readonly struct Float32Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Float32; + public static TypeId StaticTypeId => TypeId.Float32; public static float DefaultValue => 0; public static void WriteData(ref WriteContext context, in float value, bool hasGenerics) { @@ -230,7 +230,7 @@ public static float ReadData(ref ReadContext context) public readonly struct Float64Serializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Float64; + public static TypeId StaticTypeId => TypeId.Float64; public static double DefaultValue => 0; public static void WriteData(ref WriteContext context, in double value, bool hasGenerics) { @@ -246,7 +246,7 @@ public static double ReadData(ref ReadContext context) public readonly struct StringSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.String; + public static TypeId StaticTypeId => TypeId.String; public static bool IsNullableType => true; public static string DefaultValue => null!; public static bool IsNone(in string value) => value is null; @@ -300,7 +300,7 @@ private static string DecodeUtf16(byte[] bytes) public readonly struct BinarySerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Binary; + public static TypeId StaticTypeId => TypeId.Binary; public static bool IsNullableType => true; public static byte[] DefaultValue => null!; public static bool IsNone(in byte[] value) => value is null; @@ -322,7 +322,7 @@ public static byte[] ReadData(ref ReadContext context) public readonly struct ForyInt32FixedSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Int32; + public static TypeId StaticTypeId => TypeId.Int32; public static ForyInt32Fixed DefaultValue => new(0); public static void WriteData(ref WriteContext context, in ForyInt32Fixed value, bool hasGenerics) { @@ -338,7 +338,7 @@ public static ForyInt32Fixed ReadData(ref ReadContext context) public readonly struct ForyInt64FixedSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Int64; + public static TypeId StaticTypeId => TypeId.Int64; public static ForyInt64Fixed DefaultValue => new(0); public static void WriteData(ref WriteContext context, in ForyInt64Fixed value, bool hasGenerics) { @@ -354,7 +354,7 @@ public static ForyInt64Fixed ReadData(ref ReadContext context) public readonly struct ForyInt64TaggedSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.TaggedInt64; + public static TypeId StaticTypeId => TypeId.TaggedInt64; public static ForyInt64Tagged DefaultValue => new(0); public static void WriteData(ref WriteContext context, in ForyInt64Tagged value, bool hasGenerics) { @@ -370,7 +370,7 @@ public static ForyInt64Tagged ReadData(ref ReadContext context) public readonly struct ForyUInt32FixedSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.UInt32; + public static TypeId StaticTypeId => TypeId.UInt32; public static ForyUInt32Fixed DefaultValue => new(0); public static void WriteData(ref WriteContext context, in ForyUInt32Fixed value, bool hasGenerics) { @@ -386,7 +386,7 @@ public static ForyUInt32Fixed ReadData(ref ReadContext context) public readonly struct ForyUInt64FixedSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.UInt64; + public static TypeId StaticTypeId => TypeId.UInt64; public static ForyUInt64Fixed DefaultValue => new(0); public static void WriteData(ref WriteContext context, in ForyUInt64Fixed value, bool hasGenerics) { @@ -402,7 +402,7 @@ public static ForyUInt64Fixed ReadData(ref ReadContext context) public readonly struct ForyUInt64TaggedSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.TaggedUInt64; + public static TypeId StaticTypeId => TypeId.TaggedUInt64; public static ForyUInt64Tagged DefaultValue => new(0); public static void WriteData(ref WriteContext context, in ForyUInt64Tagged value, bool hasGenerics) { @@ -419,7 +419,7 @@ public static ForyUInt64Tagged ReadData(ref ReadContext context) public readonly struct DateOnlySerializer : IStaticSerializer { private static readonly DateOnly Epoch = new(1970, 1, 1); - public static ForyTypeId StaticTypeId => ForyTypeId.Date; + public static TypeId StaticTypeId => TypeId.Date; public static DateOnly DefaultValue => Epoch; public static void WriteData(ref WriteContext context, in DateOnly value, bool hasGenerics) @@ -438,7 +438,7 @@ public static DateOnly ReadData(ref ReadContext context) public readonly struct DateTimeOffsetSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Timestamp; + public static TypeId StaticTypeId => TypeId.Timestamp; public static DateTimeOffset DefaultValue => DateTimeOffset.UnixEpoch; public static void WriteData(ref WriteContext context, in DateTimeOffset value, bool hasGenerics) @@ -459,7 +459,7 @@ public static DateTimeOffset ReadData(ref ReadContext context) public readonly struct DateTimeSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Timestamp; + public static TypeId StaticTypeId => TypeId.Timestamp; public static DateTime DefaultValue => DateTime.UnixEpoch; public static void WriteData(ref WriteContext context, in DateTime value, bool hasGenerics) @@ -486,7 +486,7 @@ public static DateTime ReadData(ref ReadContext context) public readonly struct TimeSpanSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Duration; + public static TypeId StaticTypeId => TypeId.Duration; public static TimeSpan DefaultValue => TimeSpan.Zero; public static void WriteData(ref WriteContext context, in TimeSpan value, bool hasGenerics) diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs index 2f998a8d35..23439470e6 100644 --- a/csharp/src/Fory/Serializer.cs +++ b/csharp/src/Fory/Serializer.cs @@ -20,7 +20,7 @@ namespace Apache.Fory; public interface IStaticSerializer where TSerializer : IStaticSerializer { - static abstract ForyTypeId StaticTypeId { get; } + static abstract TypeId StaticTypeId { get; } static virtual bool IsNullableType => false; @@ -158,7 +158,7 @@ private TypedSerializerBinding Binding public Type Type => typeof(T); - public ForyTypeId StaticTypeId => Binding.StaticTypeId; + public TypeId StaticTypeId => Binding.StaticTypeId; public bool IsNullableType => Binding.IsNullableType; @@ -224,7 +224,7 @@ internal static class SerializerTypeInfo public static void WriteTypeInfo(ref WriteContext context) where TSerializer : IStaticSerializer { - ForyTypeId staticTypeId = TSerializer.StaticTypeId; + TypeId staticTypeId = TSerializer.StaticTypeId; if (!staticTypeId.IsUserTypeKind()) { context.Writer.WriteUInt8((byte)staticTypeId); @@ -233,21 +233,21 @@ public static void WriteTypeInfo(ref WriteContext context) Type type = typeof(T); RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); - ForyTypeId wireTypeId = ResolveWireTypeId(info.Kind, info.RegisterByName, context.Compatible); + TypeId wireTypeId = ResolveWireTypeId(info.Kind, info.RegisterByName, context.Compatible); context.Writer.WriteUInt8((byte)wireTypeId); switch (wireTypeId) { - case ForyTypeId.CompatibleStruct: - case ForyTypeId.NamedCompatibleStruct: + case TypeId.CompatibleStruct: + case TypeId.NamedCompatibleStruct: { TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); context.WriteCompatibleTypeMeta(type, typeMeta); return; } - case ForyTypeId.NamedEnum: - case ForyTypeId.NamedStruct: - case ForyTypeId.NamedExt: - case ForyTypeId.NamedUnion: + case TypeId.NamedEnum: + case TypeId.NamedStruct: + case TypeId.NamedExt: + case TypeId.NamedUnion: { if (context.Compatible) { @@ -294,13 +294,13 @@ public static void ReadTypeInfo(ref ReadContext context) where TSerializer : IStaticSerializer { uint rawTypeId = context.Reader.ReadVarUInt32(); - if (!Enum.IsDefined(typeof(ForyTypeId), rawTypeId)) + if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) { throw new ForyInvalidDataException($"unknown type id {rawTypeId}"); } - ForyTypeId typeId = (ForyTypeId)rawTypeId; - ForyTypeId staticTypeId = TSerializer.StaticTypeId; + TypeId typeId = (TypeId)rawTypeId; + TypeId staticTypeId = TSerializer.StaticTypeId; if (!staticTypeId.IsUserTypeKind()) { if (typeId != staticTypeId) @@ -313,7 +313,7 @@ public static void ReadTypeInfo(ref ReadContext context) Type type = typeof(T); RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); - HashSet allowed = AllowedWireTypeIds(info.Kind, info.RegisterByName, context.Compatible); + HashSet allowed = AllowedWireTypeIds(info.Kind, info.RegisterByName, context.Compatible); if (!allowed.Contains(typeId)) { uint expected = allowed.Count > 0 ? (uint)allowed.First() : 0; @@ -322,24 +322,24 @@ public static void ReadTypeInfo(ref ReadContext context) switch (typeId) { - case ForyTypeId.CompatibleStruct: - case ForyTypeId.NamedCompatibleStruct: + case TypeId.CompatibleStruct: + case TypeId.NamedCompatibleStruct: { TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); context.PushCompatibleTypeMeta(type, remoteTypeMeta); return; } - case ForyTypeId.NamedEnum: - case ForyTypeId.NamedStruct: - case ForyTypeId.NamedExt: - case ForyTypeId.NamedUnion: + case TypeId.NamedEnum: + case TypeId.NamedStruct: + case TypeId.NamedExt: + case TypeId.NamedUnion: { if (context.Compatible) { TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); - if (typeId == ForyTypeId.NamedStruct) + if (typeId == TypeId.NamedStruct) { context.PushCompatibleTypeMeta(type, remoteTypeMeta); } @@ -387,9 +387,9 @@ public static void ReadTypeInfo(ref ReadContext context) } } - public static ForyTypeId ResolveWireTypeId(ForyTypeId declaredKind, bool registerByName, bool compatible) + public static TypeId ResolveWireTypeId(TypeId declaredKind, bool registerByName, bool compatible) { - ForyTypeId baseKind = NormalizeBaseKind(declaredKind); + TypeId baseKind = NormalizeBaseKind(declaredKind); if (registerByName) { return NamedKind(baseKind, compatible); @@ -398,63 +398,63 @@ public static ForyTypeId ResolveWireTypeId(ForyTypeId declaredKind, bool registe return IdKind(baseKind, compatible); } - public static HashSet AllowedWireTypeIds(ForyTypeId declaredKind, bool registerByName, bool compatible) + public static HashSet AllowedWireTypeIds(TypeId declaredKind, bool registerByName, bool compatible) { - ForyTypeId baseKind = NormalizeBaseKind(declaredKind); - ForyTypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible); - HashSet allowed = [expected]; - if (baseKind == ForyTypeId.Struct && compatible) + TypeId baseKind = NormalizeBaseKind(declaredKind); + TypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible); + HashSet allowed = [expected]; + if (baseKind == TypeId.Struct && compatible) { - allowed.Add(ForyTypeId.CompatibleStruct); - allowed.Add(ForyTypeId.NamedCompatibleStruct); - allowed.Add(ForyTypeId.Struct); - allowed.Add(ForyTypeId.NamedStruct); + allowed.Add(TypeId.CompatibleStruct); + allowed.Add(TypeId.NamedCompatibleStruct); + allowed.Add(TypeId.Struct); + allowed.Add(TypeId.NamedStruct); } return allowed; } - private static ForyTypeId NormalizeBaseKind(ForyTypeId kind) + private static TypeId NormalizeBaseKind(TypeId kind) { return kind switch { - ForyTypeId.NamedEnum => ForyTypeId.Enum, - ForyTypeId.CompatibleStruct or ForyTypeId.NamedCompatibleStruct or ForyTypeId.NamedStruct => ForyTypeId.Struct, - ForyTypeId.NamedExt => ForyTypeId.Ext, - ForyTypeId.NamedUnion => ForyTypeId.TypedUnion, + TypeId.NamedEnum => TypeId.Enum, + TypeId.CompatibleStruct or TypeId.NamedCompatibleStruct or TypeId.NamedStruct => TypeId.Struct, + TypeId.NamedExt => TypeId.Ext, + TypeId.NamedUnion => TypeId.TypedUnion, _ => kind, }; } - private static ForyTypeId NamedKind(ForyTypeId baseKind, bool compatible) + private static TypeId NamedKind(TypeId baseKind, bool compatible) { return baseKind switch { - ForyTypeId.Struct => compatible ? ForyTypeId.NamedCompatibleStruct : ForyTypeId.NamedStruct, - ForyTypeId.Enum => ForyTypeId.NamedEnum, - ForyTypeId.Ext => ForyTypeId.NamedExt, - ForyTypeId.TypedUnion => ForyTypeId.NamedUnion, + TypeId.Struct => compatible ? TypeId.NamedCompatibleStruct : TypeId.NamedStruct, + TypeId.Enum => TypeId.NamedEnum, + TypeId.Ext => TypeId.NamedExt, + TypeId.TypedUnion => TypeId.NamedUnion, _ => baseKind, }; } - private static ForyTypeId IdKind(ForyTypeId baseKind, bool compatible) + private static TypeId IdKind(TypeId baseKind, bool compatible) { return baseKind switch { - ForyTypeId.Struct => compatible ? ForyTypeId.CompatibleStruct : ForyTypeId.Struct, + TypeId.Struct => compatible ? TypeId.CompatibleStruct : TypeId.Struct, _ => baseKind, }; } - private static bool WireTypeNeedsUserTypeId(ForyTypeId typeId) + private static bool WireTypeNeedsUserTypeId(TypeId typeId) { - return typeId is ForyTypeId.Enum or ForyTypeId.Struct or ForyTypeId.Ext or ForyTypeId.TypedUnion; + return typeId is TypeId.Enum or TypeId.Struct or TypeId.Ext or TypeId.TypedUnion; } private static TypeMeta BuildCompatibleTypeMeta( RegisteredTypeInfo info, - ForyTypeId wireTypeId, + TypeId wireTypeId, bool trackRef) where TSerializer : IStaticSerializer { @@ -495,8 +495,8 @@ private static TypeMeta BuildCompatibleTypeMeta( private static void ValidateCompatibleTypeMeta( TypeMeta remoteTypeMeta, RegisteredTypeInfo localInfo, - HashSet expectedWireTypes, - ForyTypeId actualWireTypeId) + HashSet expectedWireTypes, + TypeId actualWireTypeId) { if (remoteTypeMeta.RegisterByName) { @@ -543,9 +543,9 @@ private static void ValidateCompatibleTypeMeta( } if (remoteTypeMeta.TypeId.HasValue && - Enum.IsDefined(typeof(ForyTypeId), remoteTypeMeta.TypeId.Value)) + Enum.IsDefined(typeof(TypeId), remoteTypeMeta.TypeId.Value)) { - ForyTypeId remoteWireTypeId = (ForyTypeId)remoteTypeMeta.TypeId.Value; + TypeId remoteWireTypeId = (TypeId)remoteTypeMeta.TypeId.Value; if (!expectedWireTypes.Contains(remoteWireTypeId)) { throw new ForyTypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value); diff --git a/csharp/src/Fory/ForyTypeId.cs b/csharp/src/Fory/TypeId.cs similarity index 69% rename from csharp/src/Fory/ForyTypeId.cs rename to csharp/src/Fory/TypeId.cs index c6e2443fb0..47857e0915 100644 --- a/csharp/src/Fory/ForyTypeId.cs +++ b/csharp/src/Fory/TypeId.cs @@ -17,7 +17,7 @@ namespace Apache.Fory; -public enum ForyTypeId : uint +public enum TypeId : uint { Unknown = 0, Bool = 1, @@ -78,37 +78,37 @@ public enum ForyTypeId : uint Float64Array = 56, } -internal static class ForyTypeIdExtensions +internal static class TypeIdExtensions { - public static bool IsUserTypeKind(this ForyTypeId typeId) + public static bool IsUserTypeKind(this TypeId typeId) { return typeId switch { - ForyTypeId.Enum or - ForyTypeId.NamedEnum or - ForyTypeId.Struct or - ForyTypeId.CompatibleStruct or - ForyTypeId.NamedStruct or - ForyTypeId.NamedCompatibleStruct or - ForyTypeId.Ext or - ForyTypeId.NamedExt or - ForyTypeId.TypedUnion or - ForyTypeId.NamedUnion => true, + TypeId.Enum or + TypeId.NamedEnum or + TypeId.Struct or + TypeId.CompatibleStruct or + TypeId.NamedStruct or + TypeId.NamedCompatibleStruct or + TypeId.Ext or + TypeId.NamedExt or + TypeId.TypedUnion or + TypeId.NamedUnion => true, _ => false, }; } - public static bool NeedsTypeInfoForField(this ForyTypeId typeId) + public static bool NeedsTypeInfoForField(this TypeId typeId) { return typeId switch { - ForyTypeId.Struct or - ForyTypeId.CompatibleStruct or - ForyTypeId.NamedStruct or - ForyTypeId.NamedCompatibleStruct or - ForyTypeId.Ext or - ForyTypeId.NamedExt or - ForyTypeId.Unknown => true, + TypeId.Struct or + TypeId.CompatibleStruct or + TypeId.NamedStruct or + TypeId.NamedCompatibleStruct or + TypeId.Ext or + TypeId.NamedExt or + TypeId.Unknown => true, _ => false, }; } diff --git a/csharp/src/Fory/TypeMeta.cs b/csharp/src/Fory/TypeMeta.cs index 0e56db3747..b9465f2e56 100644 --- a/csharp/src/Fory/TypeMeta.cs +++ b/csharp/src/Fory/TypeMeta.cs @@ -151,21 +151,21 @@ internal void Write(ByteWriter writer, bool writeFlags, bool? nullableOverride = writer.WriteUInt8(unchecked((byte)TypeId)); } - if (TypeId is (uint)ForyTypeId.List or (uint)ForyTypeId.Set) + if (TypeId is (uint)global::Apache.Fory.TypeId.List or (uint)global::Apache.Fory.TypeId.Set) { TypeMetaFieldType element = Generics.Count > 0 ? Generics[0] - : new TypeMetaFieldType((uint)ForyTypeId.Unknown, true); + : new TypeMetaFieldType((uint)global::Apache.Fory.TypeId.Unknown, true); element.Write(writer, true, element.Nullable); } - else if (TypeId == (uint)ForyTypeId.Map) + else if (TypeId == (uint)global::Apache.Fory.TypeId.Map) { TypeMetaFieldType key = Generics.Count > 0 ? Generics[0] - : new TypeMetaFieldType((uint)ForyTypeId.Unknown, true); + : new TypeMetaFieldType((uint)global::Apache.Fory.TypeId.Unknown, true); TypeMetaFieldType value = Generics.Count > 1 ? Generics[1] - : new TypeMetaFieldType((uint)ForyTypeId.Unknown, true); + : new TypeMetaFieldType((uint)global::Apache.Fory.TypeId.Unknown, true); key.Write(writer, true, key.Nullable); value.Write(writer, true, value.Nullable); } @@ -195,13 +195,13 @@ internal static TypeMetaFieldType Read( resolvedTrackRef = trackRef ?? false; } - if (typeId is (uint)ForyTypeId.List or (uint)ForyTypeId.Set) + if (typeId is (uint)global::Apache.Fory.TypeId.List or (uint)global::Apache.Fory.TypeId.Set) { TypeMetaFieldType element = Read(reader, true); return new TypeMetaFieldType(typeId, resolvedNullable, resolvedTrackRef, [element]); } - if (typeId == (uint)ForyTypeId.Map) + if (typeId == (uint)global::Apache.Fory.TypeId.Map) { TypeMetaFieldType key = Read(reader, true); TypeMetaFieldType value = Read(reader, true); diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index ebf6357186..6bf129d719 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -19,7 +19,7 @@ namespace Apache.Fory; internal sealed record RegisteredTypeInfo( uint? UserTypeId, - ForyTypeId Kind, + TypeId Kind, bool RegisterByName, MetaString? NamespaceName, MetaString TypeName, @@ -36,7 +36,7 @@ internal enum DynamicRegistrationMode internal sealed class TypeReader { - public required ForyTypeId Kind { get; init; } + public required TypeId Kind { get; init; } public required Func Reader { get; init; } public required Func CompatibleReader { get; init; } } @@ -46,7 +46,7 @@ public sealed class TypeResolver private readonly Dictionary _byType = []; private readonly Dictionary _byUserTypeId = []; private readonly Dictionary _byTypeName = []; - private readonly Dictionary _registrationModeByKind = []; + private readonly Dictionary _registrationModeByKind = []; internal void Register(Type type, uint id, SerializerBinding? explicitSerializer = null) { @@ -140,16 +140,16 @@ internal RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) { uint rawTypeId = context.Reader.ReadVarUInt32(); - if (!Enum.IsDefined(typeof(ForyTypeId), rawTypeId)) + if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) { throw new ForyInvalidDataException($"unknown dynamic type id {rawTypeId}"); } - ForyTypeId wireTypeId = (ForyTypeId)rawTypeId; + TypeId wireTypeId = (TypeId)rawTypeId; switch (wireTypeId) { - case ForyTypeId.CompatibleStruct: - case ForyTypeId.NamedCompatibleStruct: + case TypeId.CompatibleStruct: + case TypeId.NamedCompatibleStruct: { TypeMeta typeMeta = context.ReadCompatibleTypeMeta(); if (typeMeta.RegisterByName) @@ -159,19 +159,19 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) return new DynamicTypeInfo(wireTypeId, typeMeta.UserTypeId, null, null, typeMeta); } - case ForyTypeId.NamedStruct: - case ForyTypeId.NamedEnum: - case ForyTypeId.NamedExt: - case ForyTypeId.NamedUnion: + case TypeId.NamedStruct: + case TypeId.NamedEnum: + case TypeId.NamedExt: + case TypeId.NamedUnion: { MetaString namespaceName = ReadMetaString(context.Reader, MetaStringDecoder.Namespace, TypeMetaEncodings.NamespaceMetaStringEncodings); MetaString typeName = ReadMetaString(context.Reader, MetaStringDecoder.TypeName, TypeMetaEncodings.TypeNameMetaStringEncodings); return new DynamicTypeInfo(wireTypeId, null, namespaceName, typeName, null); } - case ForyTypeId.Struct: - case ForyTypeId.Enum: - case ForyTypeId.Ext: - case ForyTypeId.TypedUnion: + case TypeId.Struct: + case TypeId.Enum: + case TypeId.Ext: + case TypeId.TypedUnion: { DynamicRegistrationMode mode = DynamicRegistrationModeFor(wireTypeId); if (mode == DynamicRegistrationMode.IdOnly) @@ -197,77 +197,77 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) { switch (typeInfo.WireTypeId) { - case ForyTypeId.Bool: + case TypeId.Bool: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int8: + case TypeId.Int8: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int16: + case TypeId.Int16: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int32: + case TypeId.Int32: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.VarInt32: + case TypeId.VarInt32: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int64: + case TypeId.Int64: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.VarInt64: + case TypeId.VarInt64: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.TaggedInt64: + case TypeId.TaggedInt64: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.UInt8: + case TypeId.UInt8: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.UInt16: + case TypeId.UInt16: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.UInt32: + case TypeId.UInt32: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.VarUInt32: + case TypeId.VarUInt32: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.UInt64: + case TypeId.UInt64: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.VarUInt64: + case TypeId.VarUInt64: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.TaggedUInt64: + case TypeId.TaggedUInt64: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Float32: + case TypeId.Float32: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Float64: + case TypeId.Float64: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.String: + case TypeId.String: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Binary: - case ForyTypeId.UInt8Array: + case TypeId.Binary: + case TypeId.UInt8Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.BoolArray: + case TypeId.BoolArray: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int8Array: + case TypeId.Int8Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int16Array: + case TypeId.Int16Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int32Array: + case TypeId.Int32Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Int64Array: + case TypeId.Int64Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.UInt16Array: + case TypeId.UInt16Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.UInt32Array: + case TypeId.UInt32Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.UInt64Array: + case TypeId.UInt64Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Float32Array: + case TypeId.Float32Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Float64Array: + case TypeId.Float64Array: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.List: + case TypeId.List: return DynamicContainerCodec.ReadListPayload(ref context); - case ForyTypeId.Set: + case TypeId.Set: return DynamicContainerCodec.ReadSetPayload(ref context); - case ForyTypeId.Map: + case TypeId.Map: return DynamicContainerCodec.ReadMapPayload(ref context); - case ForyTypeId.Union: + case TypeId.Union: return SerializerRegistry.Get().Read(ref context, RefMode.None, false); - case ForyTypeId.Struct: - case ForyTypeId.Enum: - case ForyTypeId.Ext: - case ForyTypeId.TypedUnion: + case TypeId.Struct: + case TypeId.Enum: + case TypeId.Ext: + case TypeId.TypedUnion: { if (typeInfo.UserTypeId.HasValue) { @@ -281,10 +281,10 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) throw new ForyInvalidDataException($"missing dynamic registration info for {typeInfo.WireTypeId}"); } - case ForyTypeId.NamedStruct: - case ForyTypeId.NamedEnum: - case ForyTypeId.NamedExt: - case ForyTypeId.NamedUnion: + case TypeId.NamedStruct: + case TypeId.NamedEnum: + case TypeId.NamedExt: + case TypeId.NamedUnion: { if (!typeInfo.NamespaceName.HasValue || !typeInfo.TypeName.HasValue) { @@ -293,8 +293,8 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) return ReadByTypeName(typeInfo.NamespaceName.Value.Value, typeInfo.TypeName.Value.Value, ref context); } - case ForyTypeId.CompatibleStruct: - case ForyTypeId.NamedCompatibleStruct: + case TypeId.CompatibleStruct: + case TypeId.NamedCompatibleStruct: { if (typeInfo.CompatibleTypeMeta is null) { @@ -318,14 +318,14 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) return ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value, ref context, compatibleTypeMeta); } - case ForyTypeId.None: + case TypeId.None: return null; default: throw new ForyInvalidDataException($"unsupported dynamic type id {typeInfo.WireTypeId}"); } } - private void MarkRegistrationMode(ForyTypeId kind, bool registerByName) + private void MarkRegistrationMode(TypeId kind, bool registerByName) { DynamicRegistrationMode mode = registerByName ? DynamicRegistrationMode.NameOnly : DynamicRegistrationMode.IdOnly; if (!_registrationModeByKind.TryGetValue(kind, out DynamicRegistrationMode existing)) @@ -340,7 +340,7 @@ private void MarkRegistrationMode(ForyTypeId kind, bool registerByName) } } - private DynamicRegistrationMode DynamicRegistrationModeFor(ForyTypeId kind) + private DynamicRegistrationMode DynamicRegistrationModeFor(TypeId kind) { if (_registrationModeByKind.TryGetValue(kind, out DynamicRegistrationMode mode)) { diff --git a/csharp/src/Fory/TypedSerializerBinding.cs b/csharp/src/Fory/TypedSerializerBinding.cs index 096e3bf5b5..a9470acb38 100644 --- a/csharp/src/Fory/TypedSerializerBinding.cs +++ b/csharp/src/Fory/TypedSerializerBinding.cs @@ -68,7 +68,7 @@ internal delegate void UntypedWriteDelegate( internal sealed class TypedSerializerBinding { public TypedSerializerBinding( - ForyTypeId staticTypeId, + TypeId staticTypeId, bool isNullableType, bool isReferenceTrackableType, T defaultValue, @@ -95,7 +95,7 @@ public TypedSerializerBinding( _compatibleTypeMetaFields = compatibleTypeMetaFields; } - public ForyTypeId StaticTypeId { get; } + public TypeId StaticTypeId { get; } public bool IsNullableType { get; } @@ -157,7 +157,7 @@ internal sealed class SerializerBinding { public SerializerBinding( Type type, - ForyTypeId staticTypeId, + TypeId staticTypeId, bool isNullableType, bool isReferenceTrackableType, object? defaultObject, @@ -189,7 +189,7 @@ public SerializerBinding( public Type Type { get; } - public ForyTypeId StaticTypeId { get; } + public TypeId StaticTypeId { get; } public bool IsNullableType { get; } diff --git a/csharp/src/Fory/Union.cs b/csharp/src/Fory/Union.cs index 1ab18c38b6..1db9a9ec27 100644 --- a/csharp/src/Fory/Union.cs +++ b/csharp/src/Fory/Union.cs @@ -20,7 +20,7 @@ namespace Apache.Fory; public class Union : IEquatable { public Union(int index, object? value) - : this(index, value, (int)ForyTypeId.Unknown) + : this(index, value, (int)TypeId.Unknown) { } @@ -74,7 +74,7 @@ public override string ToString() public sealed class Union2 : Union { private Union2(int index, object? value) - : this(index, value, (int)ForyTypeId.Unknown) + : this(index, value, (int)TypeId.Unknown) { } diff --git a/csharp/src/Fory/UnionSerializer.cs b/csharp/src/Fory/UnionSerializer.cs index 35904e3b81..adef58e9a9 100644 --- a/csharp/src/Fory/UnionSerializer.cs +++ b/csharp/src/Fory/UnionSerializer.cs @@ -25,7 +25,7 @@ namespace Apache.Fory; { private static readonly Func Factory = BuildFactory(); - public static ForyTypeId StaticTypeId => ForyTypeId.TypedUnion; + public static TypeId StaticTypeId => TypeId.TypedUnion; public static bool IsNullableType => true; diff --git a/csharp/tests/Fory.XlangPeer/Program.cs b/csharp/tests/Fory.XlangPeer/Program.cs index 8b4db14ca6..dd7e31a047 100644 --- a/csharp/tests/Fory.XlangPeer/Program.cs +++ b/csharp/tests/Fory.XlangPeer/Program.cs @@ -1027,7 +1027,7 @@ public sealed class MyExt public readonly struct MyExtSerializer : IStaticSerializer { - public static ForyTypeId StaticTypeId => ForyTypeId.Ext; + public static TypeId StaticTypeId => TypeId.Ext; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; public static MyExt DefaultValue => null!; From cd5ac2e7c0c46a1eb5a16f91bdd554cee9fc6f82 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 11:13:12 +0800 Subject: [PATCH 08/16] refactor(csharp): rename map APIs to dictionary variants --- .../src/Fory.Generator/ForyObjectGenerator.cs | 2 +- csharp/src/Fory/CollectionSerializers.cs | 6 +- ...erializers.cs => DictionarySerializers.cs} | 48 +-- .../{ForyMap.cs => NullableKeyDictionary.cs} | 385 +++++++++++++++--- csharp/src/Fory/SerializerRegistry.cs | 6 +- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 35 +- csharp/tests/Fory.XlangPeer/Program.cs | 14 +- 7 files changed, 403 insertions(+), 93 deletions(-) rename csharp/src/Fory/{MapSerializers.cs => DictionarySerializers.cs} (90%) rename csharp/src/Fory/{ForyMap.cs => NullableKeyDictionary.cs} (57%) diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index d085a76ed2..cafb6a4cef 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -1114,7 +1114,7 @@ private static bool TryGetMapTypeArguments(ITypeSymbol type, out ITypeSymbol? ke "System.Collections.Generic.Dictionary" or "System.Collections.Generic.IDictionary" or "System.Collections.Generic.IReadOnlyDictionary" or - "Apache.Fory.ForyMap") + "Apache.Fory.NullableKeyDictionary") { keyType = named.TypeArguments[0]; valueType = named.TypeArguments[1]; diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index 61b9cc4567..c81b27fa95 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -303,13 +303,13 @@ public static bool TryWritePayload(object value, ref WriteContext context, bool { if (value is IDictionary dictionary) { - ForyMap map = new(); + NullableKeyDictionary map = new(); foreach (DictionaryEntry entry in dictionary) { map.Add(entry.Key, entry.Value); } - SerializerRegistry.Get>().WriteData(ref context, map, false); + SerializerRegistry.Get>().WriteData(ref context, map, false); return true; } @@ -353,7 +353,7 @@ public static bool TryWritePayload(object value, ref WriteContext context, bool public static object ReadMapPayload(ref ReadContext context) { - ForyMap map = SerializerRegistry.Get>().ReadData(ref context); + NullableKeyDictionary map = SerializerRegistry.Get>().ReadData(ref context); if (map.HasNullKey) { return map; diff --git a/csharp/src/Fory/MapSerializers.cs b/csharp/src/Fory/DictionarySerializers.cs similarity index 90% rename from csharp/src/Fory/MapSerializers.cs rename to csharp/src/Fory/DictionarySerializers.cs index 3d981469e6..c85bf3b32d 100644 --- a/csharp/src/Fory/MapSerializers.cs +++ b/csharp/src/Fory/DictionarySerializers.cs @@ -17,7 +17,7 @@ namespace Apache.Fory; -internal static class MapBits +internal static class DictionaryBits { public const byte TrackingKeyRef = 0b0000_0001; public const byte KeyNull = 0b0000_0010; @@ -27,7 +27,7 @@ internal static class MapBits public const byte DeclaredValueType = 0b0010_0000; } -public readonly struct MapSerializer : IStaticSerializer, Dictionary> +public readonly struct DictionarySerializer : IStaticSerializer, Dictionary> where TKey : notnull { private static Serializer KeySerializer => SerializerRegistry.Get(); @@ -86,32 +86,32 @@ public static void WriteData(ref WriteContext context, in Dictionary ReadData(ref ReadContext context) while (readCount < totalLength) { byte header = context.Reader.ReadUInt8(); - bool trackKeyRef = (header & MapBits.TrackingKeyRef) != 0; - bool keyNull = (header & MapBits.KeyNull) != 0; - bool keyDeclared = (header & MapBits.DeclaredKeyType) != 0; - bool trackValueRef = (header & MapBits.TrackingValueRef) != 0; - bool valueNull = (header & MapBits.ValueNull) != 0; - bool valueDeclared = (header & MapBits.DeclaredValueType) != 0; + bool trackKeyRef = (header & DictionaryBits.TrackingKeyRef) != 0; + bool keyNull = (header & DictionaryBits.KeyNull) != 0; + bool keyDeclared = (header & DictionaryBits.DeclaredKeyType) != 0; + bool trackValueRef = (header & DictionaryBits.TrackingValueRef) != 0; + bool valueNull = (header & DictionaryBits.ValueNull) != 0; + bool valueDeclared = (header & DictionaryBits.DeclaredValueType) != 0; if (keyNull && valueNull) { @@ -370,30 +370,30 @@ private static void WriteDynamicMapPairs( byte header = 0; if (trackKeyRef) { - header |= MapBits.TrackingKeyRef; + header |= DictionaryBits.TrackingKeyRef; } if (trackValueRef) { - header |= MapBits.TrackingValueRef; + header |= DictionaryBits.TrackingValueRef; } if (keyIsNull) { - header |= MapBits.KeyNull; + header |= DictionaryBits.KeyNull; } else if (!keyDynamicType && keyDeclared) { - header |= MapBits.DeclaredKeyType; + header |= DictionaryBits.DeclaredKeyType; } if (valueIsNull) { - header |= MapBits.ValueNull; + header |= DictionaryBits.ValueNull; } else if (!valueDynamicType && valueDeclared) { - header |= MapBits.DeclaredValueType; + header |= DictionaryBits.DeclaredValueType; } context.Writer.WriteUInt8(header); diff --git a/csharp/src/Fory/ForyMap.cs b/csharp/src/Fory/NullableKeyDictionary.cs similarity index 57% rename from csharp/src/Fory/ForyMap.cs rename to csharp/src/Fory/NullableKeyDictionary.cs index 6ce283ef8f..0c4a603a98 100644 --- a/csharp/src/Fory/ForyMap.cs +++ b/csharp/src/Fory/NullableKeyDictionary.cs @@ -20,22 +20,49 @@ namespace Apache.Fory; #pragma warning disable CS8714 -public sealed class ForyMap : IEnumerable> +public sealed class NullableKeyDictionary : IDictionary, IReadOnlyDictionary { private readonly Dictionary _nonNullEntries; private bool _hasNullKey; private TValue _nullValue = default!; + private KeyCollection? _keys; + private ValueCollection? _values; - public ForyMap() - : this(null) + public NullableKeyDictionary() + : this((IEqualityComparer?)null) { } - public ForyMap(IEqualityComparer? comparer) + public NullableKeyDictionary(int capacity) + : this(capacity, null) + { + } + + public NullableKeyDictionary(IEqualityComparer? comparer) + : this(0, comparer) + { + } + + public NullableKeyDictionary(int capacity, IEqualityComparer? comparer) { _nonNullEntries = comparer is null - ? new Dictionary() - : new Dictionary(comparer); + ? new Dictionary(capacity) + : new Dictionary(capacity, comparer); + } + + public NullableKeyDictionary(IDictionary dictionary) + : this(dictionary, null) + { + } + + public NullableKeyDictionary(IDictionary dictionary, IEqualityComparer? comparer) + : this(dictionary?.Count ?? 0, comparer) + { + ArgumentNullException.ThrowIfNull(dictionary); + foreach (KeyValuePair entry in dictionary) + { + this[entry.Key] = entry.Value; + } } public int Count => _nonNullEntries.Count + (_hasNullKey ? 1 : 0); @@ -44,21 +71,78 @@ public ForyMap(IEqualityComparer? comparer) public TValue NullKeyValue => _nullValue; + public IEqualityComparer Comparer => _nonNullEntries.Comparer; + public IEnumerable> NonNullEntries => _nonNullEntries; - public void Add(TKey? key, TValue value) + public ICollection Keys => _keys ??= new KeyCollection(this); + + IEnumerable IReadOnlyDictionary.Keys => Keys; + + public ICollection Values => _values ??= new ValueCollection(this); + + IEnumerable IReadOnlyDictionary.Values => Values; + + public TValue this[TKey key] + { + get + { + if (TryGetValue(key, out TValue value)) + { + return value; + } + + throw new KeyNotFoundException(); + } + set => SetValue(key, value); + } + + public bool IsReadOnly => false; + + public void Add(TKey key, TValue value) { if (key is null) { - _hasNullKey = true; - _nullValue = value; + if (_hasNullKey) + { + throw new ArgumentException("An item with the same key has already been added.", nameof(key)); + } + + SetNullKeyValue(value); return; } - _nonNullEntries[key] = value; + _nonNullEntries.Add(key, value); + } + + public bool ContainsKey(TKey key) + { + if (key is null) + { + return _hasNullKey; + } + + return _nonNullEntries.ContainsKey(key); + } + + public bool Remove(TKey key) + { + if (key is null) + { + if (!_hasNullKey) + { + return false; + } + + _hasNullKey = false; + _nullValue = default!; + return true; + } + + return _nonNullEntries.Remove(key); } - public bool TryGetValue(TKey? key, out TValue value) + public bool TryGetValue(TKey key, out TValue value) { if (key is null) { @@ -75,6 +159,51 @@ public bool TryGetValue(TKey? key, out TValue value) return _nonNullEntries.TryGetValue(key, out value!); } + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public bool Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out TValue value) && + EqualityComparer.Default.Equals(value, item.Value); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException("The destination array is too small.", nameof(array)); + } + + if (_hasNullKey) + { + array[arrayIndex++] = new KeyValuePair(default!, _nullValue); + } + + foreach (KeyValuePair entry in _nonNullEntries) + { + array[arrayIndex++] = entry; + } + } + + public bool Remove(KeyValuePair item) + { + if (!Contains(item)) + { + return false; + } + + return Remove(item.Key); + } + public void Clear() { _nonNullEntries.Clear(); @@ -82,16 +211,33 @@ public void Clear() _nullValue = default!; } - public IEnumerator> GetEnumerator() + internal void SetNullKeyValue(TValue value) + { + _hasNullKey = true; + _nullValue = value; + } + + private void SetValue(TKey key, TValue value) + { + if (key is null) + { + SetNullKeyValue(value); + return; + } + + _nonNullEntries[key] = value; + } + + public IEnumerator> GetEnumerator() { if (_hasNullKey) { - yield return new KeyValuePair(default, _nullValue); + yield return new KeyValuePair(default!, _nullValue); } foreach (KeyValuePair entry in _nonNullEntries) { - yield return new KeyValuePair(entry.Key, entry.Value); + yield return entry; } } @@ -99,9 +245,150 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + private sealed class KeyCollection(NullableKeyDictionary map) : ICollection + { + private readonly NullableKeyDictionary _map = map; + + public int Count => _map.Count; + + public bool IsReadOnly => true; + + public void Add(TKey item) + { + throw new NotSupportedException("Collection is read-only."); + } + + public void Clear() + { + throw new NotSupportedException("Collection is read-only."); + } + + public bool Contains(TKey item) + { + return _map.ContainsKey(item); + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException("The destination array is too small.", nameof(array)); + } + + if (_map._hasNullKey) + { + array[arrayIndex++] = default!; + } + + _map._nonNullEntries.Keys.CopyTo(array, arrayIndex); + } + + public bool Remove(TKey item) + { + throw new NotSupportedException("Collection is read-only."); + } + + public IEnumerator GetEnumerator() + { + if (_map._hasNullKey) + { + yield return default!; + } + + foreach (TKey key in _map._nonNullEntries.Keys) + { + yield return key; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private sealed class ValueCollection(NullableKeyDictionary map) : ICollection + { + private readonly NullableKeyDictionary _map = map; + + public int Count => _map.Count; + + public bool IsReadOnly => true; + + public void Add(TValue item) + { + throw new NotSupportedException("Collection is read-only."); + } + + public void Clear() + { + throw new NotSupportedException("Collection is read-only."); + } + + public bool Contains(TValue item) + { + if (_map._hasNullKey && EqualityComparer.Default.Equals(_map._nullValue, item)) + { + return true; + } + + return _map._nonNullEntries.Values.Contains(item); + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException("The destination array is too small.", nameof(array)); + } + + if (_map._hasNullKey) + { + array[arrayIndex++] = _map._nullValue; + } + + _map._nonNullEntries.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(TValue item) + { + throw new NotSupportedException("Collection is read-only."); + } + + public IEnumerator GetEnumerator() + { + if (_map._hasNullKey) + { + yield return _map._nullValue; + } + + foreach (TValue value in _map._nonNullEntries.Values) + { + yield return value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } } -public readonly struct ForyMapSerializer : IStaticSerializer, ForyMap> +public readonly struct NullableKeyDictionarySerializer : IStaticSerializer, NullableKeyDictionary> { private static Serializer KeySerializer => SerializerRegistry.Get(); private static Serializer ValueSerializer => SerializerRegistry.Get(); @@ -109,14 +396,14 @@ IEnumerator IEnumerable.GetEnumerator() public static TypeId StaticTypeId => TypeId.Map; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; - public static ForyMap DefaultValue => null!; - public static bool IsNone(in ForyMap value) => value is null; + public static NullableKeyDictionary DefaultValue => null!; + public static bool IsNone(in NullableKeyDictionary value) => value is null; - public static void WriteData(ref WriteContext context, in ForyMap value, bool hasGenerics) + public static void WriteData(ref WriteContext context, in NullableKeyDictionary value, bool hasGenerics) { Serializer keySerializer = KeySerializer; Serializer valueSerializer = ValueSerializer; - ForyMap map = value ?? new ForyMap(); + NullableKeyDictionary map = value ?? new NullableKeyDictionary(); context.Writer.WriteVarUInt32((uint)map.Count); if (map.Count == 0) { @@ -129,7 +416,7 @@ public static void WriteData(ref WriteContext context, in ForyMap bool valueDeclared = hasGenerics && !valueSerializer.StaticTypeId.NeedsTypeInfoForField(); bool keyDynamicType = keySerializer.StaticTypeId == TypeId.Unknown; bool valueDynamicType = valueSerializer.StaticTypeId == TypeId.Unknown; - KeyValuePair[] pairs = [.. map]; + KeyValuePair[] pairs = [.. map]; if (keyDynamicType || valueDynamicType) { WriteDynamicMapPairs( @@ -147,37 +434,37 @@ public static void WriteData(ref WriteContext context, in ForyMap return; } - foreach (KeyValuePair entry in pairs) + foreach (KeyValuePair entry in pairs) { bool keyIsNull = entry.Key is null || keySerializer.IsNoneObject(entry.Key); bool valueIsNull = valueSerializer.IsNoneObject(entry.Value); byte header = 0; if (trackKeyRef) { - header |= MapBits.TrackingKeyRef; + header |= DictionaryBits.TrackingKeyRef; } if (trackValueRef) { - header |= MapBits.TrackingValueRef; + header |= DictionaryBits.TrackingValueRef; } if (keyIsNull) { - header |= MapBits.KeyNull; + header |= DictionaryBits.KeyNull; } else if (keyDeclared) { - header |= MapBits.DeclaredKeyType; + header |= DictionaryBits.DeclaredKeyType; } if (valueIsNull) { - header |= MapBits.ValueNull; + header |= DictionaryBits.ValueNull; } else if (valueDeclared) { - header |= MapBits.DeclaredValueType; + header |= DictionaryBits.DeclaredValueType; } context.Writer.WriteUInt8(header); @@ -244,17 +531,17 @@ public static void WriteData(ref WriteContext context, in ForyMap } } - public static ForyMap ReadData(ref ReadContext context) + public static NullableKeyDictionary ReadData(ref ReadContext context) { Serializer keySerializer = KeySerializer; Serializer valueSerializer = ValueSerializer; int totalLength = checked((int)context.Reader.ReadVarUInt32()); if (totalLength == 0) { - return new ForyMap(); + return new NullableKeyDictionary(); } - ForyMap map = new(); + NullableKeyDictionary map = new(); bool keyDynamicType = keySerializer.StaticTypeId == TypeId.Unknown; bool valueDynamicType = valueSerializer.StaticTypeId == TypeId.Unknown; bool canonicalizeValues = context.TrackRef && valueSerializer.IsReferenceTrackableType; @@ -263,16 +550,16 @@ public static ForyMap ReadData(ref ReadContext context) while (readCount < totalLength) { byte header = context.Reader.ReadUInt8(); - bool trackKeyRef = (header & MapBits.TrackingKeyRef) != 0; - bool keyNull = (header & MapBits.KeyNull) != 0; - bool keyDeclared = (header & MapBits.DeclaredKeyType) != 0; - bool trackValueRef = (header & MapBits.TrackingValueRef) != 0; - bool valueNull = (header & MapBits.ValueNull) != 0; - bool valueDeclared = (header & MapBits.DeclaredValueType) != 0; + bool trackKeyRef = (header & DictionaryBits.TrackingKeyRef) != 0; + bool keyNull = (header & DictionaryBits.KeyNull) != 0; + bool keyDeclared = (header & DictionaryBits.DeclaredKeyType) != 0; + bool trackValueRef = (header & DictionaryBits.TrackingValueRef) != 0; + bool valueNull = (header & DictionaryBits.ValueNull) != 0; + bool valueDeclared = (header & DictionaryBits.DeclaredValueType) != 0; if (keyNull && valueNull) { - map.Add(default, (TValue)valueSerializer.DefaultObject!); + map.SetNullKeyValue((TValue)valueSerializer.DefaultObject!); readCount += 1; continue; } @@ -286,7 +573,7 @@ public static ForyMap ReadData(ref ReadContext context) canonicalizeValues, valueSerializer); - map.Add(default, valueRead); + map.SetNullKeyValue(valueRead); readCount += 1; continue; } @@ -298,7 +585,7 @@ public static ForyMap ReadData(ref ReadContext context) trackKeyRef ? RefMode.Tracking : RefMode.None, !keyDeclared); - map.Add(key, (TValue)valueSerializer.DefaultObject!); + map[key] = (TValue)valueSerializer.DefaultObject!; readCount += 1; continue; } @@ -362,7 +649,7 @@ public static ForyMap ReadData(ref ReadContext context) context.ClearDynamicTypeInfo(typeof(TValue)); } - map.Add(key, valueRead); + map[key] = valueRead; } readCount += chunkSize; @@ -383,7 +670,7 @@ public static ForyMap ReadData(ref ReadContext context) { TKey key = keySerializer.Read(ref context, trackKeyRef ? RefMode.Tracking : RefMode.None, false); TValue valueRead = ReadValueElement(ref context, trackValueRef, false, canonicalizeValues, valueSerializer); - map.Add(key, valueRead); + map[key] = valueRead; } if (!keyDeclared) @@ -403,7 +690,7 @@ public static ForyMap ReadData(ref ReadContext context) } private static void WriteDynamicMapPairs( - KeyValuePair[] pairs, + KeyValuePair[] pairs, ref WriteContext context, bool hasGenerics, bool trackKeyRef, @@ -415,37 +702,37 @@ private static void WriteDynamicMapPairs( Serializer keySerializer, Serializer valueSerializer) { - foreach (KeyValuePair pair in pairs) + foreach (KeyValuePair pair in pairs) { bool keyIsNull = pair.Key is null || keySerializer.IsNoneObject(pair.Key); bool valueIsNull = valueSerializer.IsNoneObject(pair.Value); byte header = 0; if (trackKeyRef) { - header |= MapBits.TrackingKeyRef; + header |= DictionaryBits.TrackingKeyRef; } if (trackValueRef) { - header |= MapBits.TrackingValueRef; + header |= DictionaryBits.TrackingValueRef; } if (keyIsNull) { - header |= MapBits.KeyNull; + header |= DictionaryBits.KeyNull; } else if (!keyDynamicType && keyDeclared) { - header |= MapBits.DeclaredKeyType; + header |= DictionaryBits.DeclaredKeyType; } if (valueIsNull) { - header |= MapBits.ValueNull; + header |= DictionaryBits.ValueNull; } else if (!valueDynamicType && valueDeclared) { - header |= MapBits.DeclaredValueType; + header |= DictionaryBits.DeclaredValueType; } context.Writer.WriteUInt8(header); diff --git a/csharp/src/Fory/SerializerRegistry.cs b/csharp/src/Fory/SerializerRegistry.cs index 7bde12d43f..d0248b0cb4 100644 --- a/csharp/src/Fory/SerializerRegistry.cs +++ b/csharp/src/Fory/SerializerRegistry.cs @@ -235,13 +235,13 @@ private static SerializerBinding Create(Type type) if (genericType == typeof(Dictionary<,>)) { - Type serializerType = typeof(MapSerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); + Type serializerType = typeof(DictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); return StaticSerializerBindingFactory.Create(type, serializerType); } - if (genericType == typeof(ForyMap<,>)) + if (genericType == typeof(NullableKeyDictionary<,>)) { - Type serializerType = typeof(ForyMapSerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); + Type serializerType = typeof(NullableKeyDictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); return StaticSerializerBindingFactory.Create(type, serializerType); } } diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index bc28bc211a..e0548caf0c 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -99,7 +99,7 @@ public sealed class StructWithEnum [ForyObject] public sealed class StructWithNullableMap { - public ForyMap Data { get; set; } = new(); + public NullableKeyDictionary Data { get; set; } = new(); } [ForyObject] @@ -262,17 +262,17 @@ public void MacroClassReferenceTracking() } [Fact] - public void ForyMapSupportsNullKeyRoundTrip() + public void NullableKeyDictionarySupportsNullKeyRoundTrip() { ForyRuntime fory = ForyRuntime.Builder().Compatible(true).Build(); - ForyMap map = new(); + NullableKeyDictionary map = new(); map.Add("k1", "v1"); - map.Add(null, "v2"); + map.Add((string)null!, "v2"); map.Add("k3", null); map.Add("k4", "v4"); - ForyMap decoded = fory.Deserialize>(fory.Serialize(map)); + NullableKeyDictionary decoded = fory.Deserialize>(fory.Serialize(map)); Assert.True(decoded.HasNullKey); Assert.Equal("v2", decoded.NullKeyValue); Assert.True(decoded.TryGetValue("k1", out string? v1)); @@ -281,6 +281,29 @@ public void ForyMapSupportsNullKeyRoundTrip() Assert.Null(v3); } + [Fact] + public void NullableKeyDictionarySupportsDropInDictionaryBehavior() + { + IDictionary map = new NullableKeyDictionary(); + map.Add("k1", "v1"); + map.Add(null!, "v2"); + + Assert.Throws(() => map.Add("k1", "dup")); + Assert.Throws(() => map.Add(null!, "dup")); + + map["k1"] = "v1-updated"; + map[null!] = "v2-updated"; + + Assert.True(map.ContainsKey("k1")); + Assert.True(map.ContainsKey(null!)); + Assert.Equal("v1-updated", map["k1"]); + Assert.Equal("v2-updated", map[null!]); + Assert.True(map.TryGetValue(null!, out string? nullValue)); + Assert.Equal("v2-updated", nullValue); + Assert.True(map.Remove(null!)); + Assert.False(map.ContainsKey(null!)); + } + [Fact] public void StructWithNullableMapRoundTrip() { @@ -289,7 +312,7 @@ public void StructWithNullableMapRoundTrip() StructWithNullableMap value = new(); value.Data.Add("key1", "value1"); - value.Data.Add(null, "value2"); + value.Data.Add((string)null!, "value2"); value.Data.Add("key3", null); StructWithNullableMap decoded = fory.Deserialize(fory.Serialize(value)); diff --git a/csharp/tests/Fory.XlangPeer/Program.cs b/csharp/tests/Fory.XlangPeer/Program.cs index dd7e31a047..526b1fb3b8 100644 --- a/csharp/tests/Fory.XlangPeer/Program.cs +++ b/csharp/tests/Fory.XlangPeer/Program.cs @@ -457,8 +457,8 @@ private static byte[] CaseMap(byte[] input) ForyRuntime fory = BuildFory(compatible: true); fory.Register(102); ReadOnlySequence sequence = new(input); - ForyMap strMap = fory.Deserialize>(ref sequence); - ForyMap itemMap = fory.Deserialize>(ref sequence); + NullableKeyDictionary strMap = fory.Deserialize>(ref sequence); + NullableKeyDictionary itemMap = fory.Deserialize>(ref sequence); EnsureConsumed(sequence, nameof(CaseMap)); List output = []; @@ -1004,7 +1004,7 @@ public sealed class StructWithList [ForyObject] public sealed class StructWithMap { - public ForyMap Data { get; set; } = new(); + public NullableKeyDictionary Data { get; set; } = new(); } [ForyObject] @@ -1146,7 +1146,7 @@ public sealed class NullableComprehensiveSchemaConsistent public string StringField { get; set; } = string.Empty; public List ListField { get; set; } = []; public HashSet SetField { get; set; } = []; - public ForyMap MapField { get; set; } = new(); + public NullableKeyDictionary MapField { get; set; } = new(); public int? NullableInt { get; set; } public long? NullableLong { get; set; } @@ -1156,7 +1156,7 @@ public sealed class NullableComprehensiveSchemaConsistent public string? NullableString { get; set; } public List? NullableList { get; set; } public HashSet? NullableSet { get; set; } - public ForyMap? NullableMap { get; set; } + public NullableKeyDictionary? NullableMap { get; set; } } [ForyObject] @@ -1179,7 +1179,7 @@ public sealed class NullableComprehensiveCompatible public string StringField { get; set; } = string.Empty; public List ListField { get; set; } = []; public HashSet SetField { get; set; } = []; - public ForyMap MapField { get; set; } = new(); + public NullableKeyDictionary MapField { get; set; } = new(); public int NullableInt1 { get; set; } public long NullableLong1 { get; set; } @@ -1190,7 +1190,7 @@ public sealed class NullableComprehensiveCompatible public string NullableString2 { get; set; } = string.Empty; public List NullableList2 { get; set; } = []; public HashSet NullableSet2 { get; set; } = []; - public ForyMap NullableMap2 { get; set; } = new(); + public NullableKeyDictionary NullableMap2 { get; set; } = new(); } [ForyObject] From ada056eed7b7903782e031a9cf8824bec7b3bb0e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 11:17:06 +0800 Subject: [PATCH 09/16] fix(csharp): skip null-key entries in dictionary deserialization --- csharp/src/Fory/DictionarySerializers.cs | 6 ++++-- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/csharp/src/Fory/DictionarySerializers.cs b/csharp/src/Fory/DictionarySerializers.cs index c85bf3b32d..9637e70c13 100644 --- a/csharp/src/Fory/DictionarySerializers.cs +++ b/csharp/src/Fory/DictionarySerializers.cs @@ -220,7 +220,8 @@ public static Dictionary ReadData(ref ReadContext context) if (keyNull && valueNull) { - map[(TKey)keySerializer.DefaultObject!] = (TValue)valueSerializer.DefaultObject!; + // Dictionary cannot represent a null key. + // Drop this entry instead of mapping it to default(TKey), which would corrupt key semantics. readCount += 1; continue; } @@ -234,7 +235,8 @@ public static Dictionary ReadData(ref ReadContext context) canonicalizeValues, valueSerializer); - map[(TKey)keySerializer.DefaultObject!] = value; + // Preserve stream/reference state by reading value payload, then skip null-key entry. + // This avoids injecting a fake default(TKey) key into Dictionary. readCount += 1; continue; } diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index e0548caf0c..c6a5cdf152 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -304,6 +304,22 @@ public void NullableKeyDictionarySupportsDropInDictionaryBehavior() Assert.False(map.ContainsKey(null!)); } + [Fact] + public void DictionarySerializerSkipsNullKeyEntries() + { + ForyRuntime fory = ForyRuntime.Builder().Compatible(true).Build(); + + NullableKeyDictionary source = new(); + source.Add("k1", "v1"); + source.Add((string)null!, "v-null"); + source.Add("k2", "v2"); + + Dictionary decoded = fory.Deserialize>(fory.Serialize(source)); + Assert.Equal(2, decoded.Count); + Assert.Equal("v1", decoded["k1"]); + Assert.Equal("v2", decoded["k2"]); + } + [Fact] public void StructWithNullableMapRoundTrip() { From 473ef530c462843d75110a3c78314be3b3aef12c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 11:18:24 +0800 Subject: [PATCH 10/16] refactor(csharp): remove fory prefix from derived exceptions --- .../src/Fory.Generator/ForyObjectGenerator.cs | 2 +- csharp/src/Fory/AnySerializer.cs | 10 ++-- csharp/src/Fory/ByteBuffer.cs | 8 ++-- csharp/src/Fory/CollectionSerializers.cs | 18 +++---- csharp/src/Fory/Context.cs | 8 ++-- csharp/src/Fory/EnumSerializer.cs | 2 +- csharp/src/Fory/FieldSkipper.cs | 14 +++--- csharp/src/Fory/Fory.cs | 6 +-- csharp/src/Fory/ForyException.cs | 24 +++++----- csharp/src/Fory/MetaString.cs | 22 ++++----- csharp/src/Fory/OptionalSerializer.cs | 6 +-- csharp/src/Fory/PrimitiveSerializers.cs | 4 +- csharp/src/Fory/RefResolver.cs | 6 +-- csharp/src/Fory/Serializer.cs | 48 +++++++++---------- csharp/src/Fory/SerializerRegistry.cs | 2 +- csharp/src/Fory/TypeMeta.cs | 26 +++++----- csharp/src/Fory/TypeResolver.cs | 24 +++++----- csharp/src/Fory/TypedSerializerBinding.cs | 4 +- csharp/src/Fory/UnionSerializer.cs | 6 +-- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 2 +- 20 files changed, 121 insertions(+), 121 deletions(-) diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index cafb6a4cef..64c8dd95ed 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -272,7 +272,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" uint expectedHash = __ForySchemaHash(context.TrackRef);"); sb.AppendLine(" if (schemaHash != expectedHash)"); sb.AppendLine(" {"); - sb.AppendLine(" throw new global::Apache.Fory.ForyInvalidDataException($\"class version hash mismatch: expected {expectedHash}, got {schemaHash}\");"); + sb.AppendLine(" throw new global::Apache.Fory.InvalidDataException($\"class version hash mismatch: expected {expectedHash}, got {schemaHash}\");"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine($" {model.TypeName} valueSchema = new {model.TypeName}();"); diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index a0f977c41e..8763fc17f0 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -40,7 +40,7 @@ public static void WriteData(ref WriteContext context, in object? value, bool ha DynamicTypeInfo? dynamicTypeInfo = context.DynamicTypeInfo(typeof(object)); if (dynamicTypeInfo is null) { - throw new ForyInvalidDataException("dynamic Any value requires type info"); + throw new InvalidDataException("dynamic Any value requires type info"); } return context.TypeResolver.ReadDynamicValue(dynamicTypeInfo, ref context); @@ -48,7 +48,7 @@ public static void WriteData(ref WriteContext context, in object? value, bool ha public static void WriteTypeInfo(ref WriteContext context) { - throw new ForyInvalidDataException("dynamic Any value type info is runtime-only"); + throw new InvalidDataException("dynamic Any value type info is runtime-only"); } public static void ReadTypeInfo(ref ReadContext context) @@ -129,7 +129,7 @@ public static void Write(ref WriteContext context, in object? value, RefMode ref case RefFlag.NotNullValue: break; default: - throw new ForyRefException($"invalid ref flag {rawFlag}"); + throw new RefException($"invalid ref flag {rawFlag}"); } } @@ -183,7 +183,7 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) return null; } - throw new ForyInvalidDataException($"cannot cast null dynamic Any value to non-nullable {targetType}"); + throw new InvalidDataException($"cannot cast null dynamic Any value to non-nullable {targetType}"); } if (targetType.IsInstanceOfType(value)) @@ -191,7 +191,7 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) return value; } - throw new ForyInvalidDataException($"cannot cast dynamic Any value to {targetType}"); + throw new InvalidDataException($"cannot cast dynamic Any value to {targetType}"); } public static void WriteAny(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo = true, bool hasGenerics = false) diff --git a/csharp/src/Fory/ByteBuffer.cs b/csharp/src/Fory/ByteBuffer.cs index 3f32406045..4d7e83e14a 100644 --- a/csharp/src/Fory/ByteBuffer.cs +++ b/csharp/src/Fory/ByteBuffer.cs @@ -117,7 +117,7 @@ public void WriteVarUInt36Small(ulong value) { if (value >= (1UL << 36)) { - throw new ForyEncodingException("varuint36small overflow"); + throw new EncodingException("varuint36small overflow"); } WriteVarUInt64(value); @@ -238,7 +238,7 @@ public void CheckBound(int need) { if (_cursor + need > _storage.Length) { - throw new ForyOutOfBoundsException(_cursor, need, _storage.Length); + throw new OutOfBoundsException(_cursor, need, _storage.Length); } } @@ -310,7 +310,7 @@ public uint ReadVarUInt32() shift += 7; if (shift > 28) { - throw new ForyEncodingException("varuint32 overflow"); + throw new EncodingException("varuint32 overflow"); } } } @@ -341,7 +341,7 @@ public ulong ReadVarUInt36Small() ulong value = ReadVarUInt64(); if (value >= (1UL << 36)) { - throw new ForyEncodingException("varuint36small overflow"); + throw new EncodingException("varuint36small overflow"); } return value; diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index c81b27fa95..31b2c7dd95 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -172,7 +172,7 @@ public static List ReadCollectionData(Serializer elementSerializer, ref } else { - throw new ForyRefException($"invalid nullability flag {refFlag}"); + throw new RefException($"invalid nullability flag {refFlag}"); } } } @@ -613,7 +613,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) { if ((payloadSize & 1) != 0) { - throw new ForyInvalidDataException("int16 array payload size mismatch"); + throw new InvalidDataException("int16 array payload size mismatch"); } short[] outValues = new short[payloadSize / 2]; @@ -629,7 +629,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) { if ((payloadSize & 3) != 0) { - throw new ForyInvalidDataException("int32 array payload size mismatch"); + throw new InvalidDataException("int32 array payload size mismatch"); } int[] outValues = new int[payloadSize / 4]; @@ -645,7 +645,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) { if ((payloadSize & 3) != 0) { - throw new ForyInvalidDataException("uint32 array payload size mismatch"); + throw new InvalidDataException("uint32 array payload size mismatch"); } uint[] outValues = new uint[payloadSize / 4]; @@ -661,7 +661,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) { if ((payloadSize & 7) != 0) { - throw new ForyInvalidDataException("int64 array payload size mismatch"); + throw new InvalidDataException("int64 array payload size mismatch"); } long[] outValues = new long[payloadSize / 8]; @@ -677,7 +677,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) { if ((payloadSize & 7) != 0) { - throw new ForyInvalidDataException("uint64 array payload size mismatch"); + throw new InvalidDataException("uint64 array payload size mismatch"); } ulong[] outValues = new ulong[payloadSize / 8]; @@ -693,7 +693,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) { if ((payloadSize & 1) != 0) { - throw new ForyInvalidDataException("uint16 array payload size mismatch"); + throw new InvalidDataException("uint16 array payload size mismatch"); } ushort[] outValues = new ushort[payloadSize / 2]; @@ -709,7 +709,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) { if ((payloadSize & 3) != 0) { - throw new ForyInvalidDataException("float32 array payload size mismatch"); + throw new InvalidDataException("float32 array payload size mismatch"); } float[] outValues = new float[payloadSize / 4]; @@ -723,7 +723,7 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) if ((payloadSize & 7) != 0) { - throw new ForyInvalidDataException("float64 array payload size mismatch"); + throw new InvalidDataException("float64 array payload size mismatch"); } double[] doubles = new double[payloadSize / 8]; diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs index 9fa9deaeb6..9dafa1c1d2 100644 --- a/csharp/src/Fory/Context.cs +++ b/csharp/src/Fory/Context.cs @@ -60,7 +60,7 @@ public void StoreTypeMeta(TypeMeta typeMeta, int index) { if (index < 0) { - throw new ForyInvalidDataException("negative compatible type definition index"); + throw new InvalidDataException("negative compatible type definition index"); } if (index == _typeMetas.Count) @@ -75,7 +75,7 @@ public void StoreTypeMeta(TypeMeta typeMeta, int index) return; } - throw new ForyInvalidDataException( + throw new InvalidDataException( $"compatible type definition index gap: index={index}, count={_typeMetas.Count}"); } @@ -304,7 +304,7 @@ public TypeMeta ReadCompatibleTypeMeta() TypeMeta? cached = CompatibleTypeDefState.TypeMetaAt(index); if (cached is null) { - throw new ForyInvalidDataException($"unknown compatible type definition ref index {index}"); + throw new InvalidDataException($"unknown compatible type definition ref index {index}"); } return cached; @@ -324,7 +324,7 @@ public TypeMeta ConsumeCompatibleTypeMeta(Type type) { if (!_pendingCompatibleTypeMeta.TryGetValue(type, out List? stack) || stack.Count == 0) { - throw new ForyInvalidDataException($"missing compatible type metadata for {type}"); + throw new InvalidDataException($"missing compatible type metadata for {type}"); } return stack[^1]; diff --git a/csharp/src/Fory/EnumSerializer.cs b/csharp/src/Fory/EnumSerializer.cs index 4b17c5ac1f..111e5fb46c 100644 --- a/csharp/src/Fory/EnumSerializer.cs +++ b/csharp/src/Fory/EnumSerializer.cs @@ -35,7 +35,7 @@ public static TEnum ReadData(ref ReadContext context) TEnum value = (TEnum)Enum.ToObject(typeof(TEnum), ordinal); if (!Enum.IsDefined(typeof(TEnum), value)) { - throw new ForyInvalidDataException($"unknown enum ordinal {ordinal}"); + throw new InvalidDataException($"unknown enum ordinal {ordinal}"); } return value; diff --git a/csharp/src/Fory/FieldSkipper.cs b/csharp/src/Fory/FieldSkipper.cs index 75cd05be1e..e66e6e7a69 100644 --- a/csharp/src/Fory/FieldSkipper.cs +++ b/csharp/src/Fory/FieldSkipper.cs @@ -30,8 +30,8 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie { RefMode.None => context.Reader.ReadVarUInt32(), RefMode.NullOnly => ReadNullableEnumOrdinal(ref context), - RefMode.Tracking => throw new ForyInvalidDataException("enum tracking ref mode is not supported"), - _ => throw new ForyInvalidDataException($"unsupported ref mode {refMode}"), + RefMode.Tracking => throw new InvalidDataException("enum tracking ref mode is not supported"), + _ => throw new InvalidDataException($"unsupported ref mode {refMode}"), }; } @@ -45,7 +45,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie if (flag != (sbyte)RefFlag.NotNullValue) { - throw new ForyInvalidDataException($"unexpected enum nullOnly flag {flag}"); + throw new InvalidDataException($"unexpected enum nullOnly flag {flag}"); } return context.Reader.ReadVarUInt32(); @@ -76,7 +76,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie { if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)TypeId.String) { - throw new ForyInvalidDataException("unsupported compatible list element type"); + throw new InvalidDataException("unsupported compatible list element type"); } return SerializerRegistry.Get>().Read(ref context, refMode, false); @@ -85,7 +85,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie { if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)TypeId.String) { - throw new ForyInvalidDataException("unsupported compatible set element type"); + throw new InvalidDataException("unsupported compatible set element type"); } return SerializerRegistry.Get>().Read(ref context, refMode, false); @@ -96,7 +96,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie fieldType.Generics[0].TypeId != (uint)TypeId.String || fieldType.Generics[1].TypeId != (uint)TypeId.String) { - throw new ForyInvalidDataException("unsupported compatible map key/value type"); + throw new InvalidDataException("unsupported compatible map key/value type"); } return SerializerRegistry.Get>().Read(ref context, refMode, false); @@ -108,7 +108,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie case (uint)TypeId.NamedUnion: return SerializerRegistry.Get().Read(ref context, refMode, false); default: - throw new ForyInvalidDataException($"unsupported compatible field type id {fieldType.TypeId}"); + throw new InvalidDataException($"unsupported compatible field type id {fieldType.TypeId}"); } } } diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs index 8ebdcc122d..1ee87a3370 100644 --- a/csharp/src/Fory/Fory.cs +++ b/csharp/src/Fory/Fory.cs @@ -105,7 +105,7 @@ public T Deserialize(ReadOnlySpan payload) T value = DeserializeFromReader(reader); if (reader.Remaining != 0) { - throw new ForyInvalidDataException($"unexpected trailing bytes after deserializing {typeof(T)}"); + throw new InvalidDataException($"unexpected trailing bytes after deserializing {typeof(T)}"); } return value; @@ -154,7 +154,7 @@ public void SerializeObject(IBufferWriter output, object? value) object? value = DeserializeObjectFromReader(reader); if (reader.Remaining != 0) { - throw new ForyInvalidDataException("unexpected trailing bytes after deserializing dynamic object"); + throw new InvalidDataException("unexpected trailing bytes after deserializing dynamic object"); } return value; @@ -191,7 +191,7 @@ public bool ReadHead(ByteReader reader) bool peerIsXlang = (bitmap & ForyHeaderFlag.IsXlang) != 0; if (peerIsXlang != Config.Xlang) { - throw new ForyInvalidDataException("xlang bitmap mismatch"); + throw new InvalidDataException("xlang bitmap mismatch"); } return (bitmap & ForyHeaderFlag.IsNull) != 0; diff --git a/csharp/src/Fory/ForyException.cs b/csharp/src/Fory/ForyException.cs index 3d0199688f..d3c4689ff2 100644 --- a/csharp/src/Fory/ForyException.cs +++ b/csharp/src/Fory/ForyException.cs @@ -24,45 +24,45 @@ public ForyException(string message) : base(message) } } -public sealed class ForyInvalidDataException : ForyException +public sealed class InvalidDataException : ForyException { - public ForyInvalidDataException(string message) : base($"Invalid data: {message}") + public InvalidDataException(string message) : base($"Invalid data: {message}") { } } -public sealed class ForyTypeMismatchException : ForyException +public sealed class TypeMismatchException : ForyException { - public ForyTypeMismatchException(uint expected, uint actual) + public TypeMismatchException(uint expected, uint actual) : base($"Type mismatch: expected {expected}, got {actual}") { } } -public sealed class ForyTypeNotRegisteredException : ForyException +public sealed class TypeNotRegisteredException : ForyException { - public ForyTypeNotRegisteredException(string message) : base($"Type not registered: {message}") + public TypeNotRegisteredException(string message) : base($"Type not registered: {message}") { } } -public sealed class ForyRefException : ForyException +public sealed class RefException : ForyException { - public ForyRefException(string message) : base($"Reference error: {message}") + public RefException(string message) : base($"Reference error: {message}") { } } -public sealed class ForyEncodingException : ForyException +public sealed class EncodingException : ForyException { - public ForyEncodingException(string message) : base($"Encoding error: {message}") + public EncodingException(string message) : base($"Encoding error: {message}") { } } -public sealed class ForyOutOfBoundsException : ForyException +public sealed class OutOfBoundsException : ForyException { - public ForyOutOfBoundsException(int cursor, int need, int length) + public OutOfBoundsException(int cursor, int need, int length) : base($"Buffer out of bounds: cursor={cursor}, need={need}, length={length}") { } diff --git a/csharp/src/Fory/MetaString.cs b/csharp/src/Fory/MetaString.cs index a863f590bf..f67152e2d2 100644 --- a/csharp/src/Fory/MetaString.cs +++ b/csharp/src/Fory/MetaString.cs @@ -41,12 +41,12 @@ public MetaString( { if (value.Length >= MaxMetaStringLength) { - throw new ForyEncodingException("meta string too long"); + throw new EncodingException("meta string too long"); } if (encoding != MetaStringEncoding.Utf8 && bytes.Length == 0) { - throw new ForyEncodingException("encoded meta string cannot be empty"); + throw new EncodingException("encoded meta string cannot be empty"); } Value = value; @@ -138,7 +138,7 @@ public MetaString Encode(string input, MetaStringEncoding encoding) { if (input.Length >= MaxMetaStringLength) { - throw new ForyEncodingException("meta string too long"); + throw new EncodingException("meta string too long"); } if (input.Length == 0) @@ -148,7 +148,7 @@ public MetaString Encode(string input, MetaStringEncoding encoding) if (encoding != MetaStringEncoding.Utf8 && !IsLatin(input)) { - throw new ForyEncodingException("non-ASCII characters are not allowed for packed meta string"); + throw new EncodingException("non-ASCII characters are not allowed for packed meta string"); } return encoding switch @@ -183,7 +183,7 @@ public MetaString Encode(string input, MetaStringEncoding encoding) SpecialChar1, SpecialChar2, EncodeGeneric(EscapeAllUpper(input), 5, MapLowerSpecial)), - _ => throw new ForyEncodingException($"unsupported meta string encoding: {encoding}"), + _ => throw new EncodingException($"unsupported meta string encoding: {encoding}"), }; } @@ -191,7 +191,7 @@ private MetaString EncodeAuto(string input, IReadOnlyList? a { if (input.Length >= MaxMetaStringLength) { - throw new ForyEncodingException("meta string too long"); + throw new EncodingException("meta string too long"); } if (input.Length == 0) @@ -331,7 +331,7 @@ private static byte MapLowerSpecial(char c) '_' => 27, '$' => 28, '|' => 29, - _ => throw new ForyEncodingException("unsupported character in LOWER_SPECIAL"), + _ => throw new EncodingException("unsupported character in LOWER_SPECIAL"), }; } @@ -362,7 +362,7 @@ private byte MapLowerUpperDigitSpecial(char c) return 63; } - throw new ForyEncodingException("unsupported character in LOWER_UPPER_DIGIT_SPECIAL"); + throw new EncodingException("unsupported character in LOWER_UPPER_DIGIT_SPECIAL"); } private static string LowerFirstAscii(string input) @@ -437,7 +437,7 @@ public MetaString Decode(byte[] bytes, MetaStringEncoding encoding) DecodeFirstToLowerSpecial(bytes), MetaStringEncoding.AllToLowerSpecial => UnescapeAllUpper(DecodeGeneric(bytes, 5, UnmapLowerSpecial)), - _ => throw new ForyEncodingException($"unsupported meta string encoding: {encoding}"), + _ => throw new EncodingException($"unsupported meta string encoding: {encoding}"), }; return new MetaString(value, encoding, SpecialChar1, SpecialChar2, bytes); @@ -493,7 +493,7 @@ private static char UnmapLowerSpecial(byte value) 27 => '_', 28 => '$', 29 => '|', - _ => throw new ForyEncodingException("invalid LOWER_SPECIAL value"), + _ => throw new EncodingException("invalid LOWER_SPECIAL value"), }; } @@ -506,7 +506,7 @@ private char UnmapLowerUpperDigitSpecial(byte value) <= 61 => (char)('0' + value - 52), 62 => SpecialChar1, 63 => SpecialChar2, - _ => throw new ForyEncodingException("invalid LOWER_UPPER_DIGIT_SPECIAL value"), + _ => throw new EncodingException("invalid LOWER_UPPER_DIGIT_SPECIAL value"), }; } diff --git a/csharp/src/Fory/OptionalSerializer.cs b/csharp/src/Fory/OptionalSerializer.cs index e0ba4782a6..f692c4e552 100644 --- a/csharp/src/Fory/OptionalSerializer.cs +++ b/csharp/src/Fory/OptionalSerializer.cs @@ -38,7 +38,7 @@ public static void WriteData(ref WriteContext context, in T? value, bool hasGene { if (!value.HasValue) { - throw new ForyInvalidDataException("Nullable.None cannot write raw payload"); + throw new InvalidDataException("Nullable.None cannot write raw payload"); } T wrapped = value.Value; @@ -72,7 +72,7 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, case RefMode.None: if (!value.HasValue) { - throw new ForyInvalidDataException("Nullable.None with RefMode.None"); + throw new InvalidDataException("Nullable.None with RefMode.None"); } T wrapped = value.Value; @@ -128,7 +128,7 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, return WrappedSerializer.Read(ref context, RefMode.Tracking, readTypeInfo); } default: - throw new ForyInvalidDataException($"unsupported ref mode {refMode}"); + throw new InvalidDataException($"unsupported ref mode {refMode}"); } } } diff --git a/csharp/src/Fory/PrimitiveSerializers.cs b/csharp/src/Fory/PrimitiveSerializers.cs index 957626b179..a1c732b1dd 100644 --- a/csharp/src/Fory/PrimitiveSerializers.cs +++ b/csharp/src/Fory/PrimitiveSerializers.cs @@ -272,7 +272,7 @@ public static string ReadData(ref ReadContext context) (ulong)ForyStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes), (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes), (ulong)ForyStringEncoding.Utf16 => DecodeUtf16(bytes), - _ => throw new ForyEncodingException($"unsupported string encoding {encoding}"), + _ => throw new EncodingException($"unsupported string encoding {encoding}"), }; } @@ -291,7 +291,7 @@ private static string DecodeUtf16(byte[] bytes) { if ((bytes.Length & 1) != 0) { - throw new ForyEncodingException("utf16 byte length is not even"); + throw new EncodingException("utf16 byte length is not even"); } return Encoding.Unicode.GetString(bytes); diff --git a/csharp/src/Fory/RefResolver.cs b/csharp/src/Fory/RefResolver.cs index 183bbfce86..98c12baa74 100644 --- a/csharp/src/Fory/RefResolver.cs +++ b/csharp/src/Fory/RefResolver.cs @@ -73,7 +73,7 @@ public T ReadRef(uint refId) int index = checked((int)refId); if (index < 0 || index >= _refs.Count) { - throw new ForyRefException($"ref_id out of range: {refId}"); + throw new RefException($"ref_id out of range: {refId}"); } if (_refs[index] is T typed) @@ -81,7 +81,7 @@ public T ReadRef(uint refId) return typed; } - throw new ForyRefException($"ref_id {refId} has unexpected runtime type"); + throw new RefException($"ref_id {refId} has unexpected runtime type"); } public object? ReadRefValue(uint refId) @@ -89,7 +89,7 @@ public T ReadRef(uint refId) int index = checked((int)refId); if (index < 0 || index >= _refs.Count) { - throw new ForyRefException($"ref_id out of range: {refId}"); + throw new RefException($"ref_id out of range: {refId}"); } return _refs[index]; diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs index 23439470e6..099b46291c 100644 --- a/csharp/src/Fory/Serializer.cs +++ b/csharp/src/Fory/Serializer.cs @@ -121,7 +121,7 @@ static virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInf case RefFlag.NotNullValue: break; default: - throw new ForyRefException($"invalid ref flag {rawFlag}"); + throw new RefException($"invalid ref flag {rawFlag}"); } } @@ -149,7 +149,7 @@ private TypedSerializerBinding Binding { if (_binding is null) { - throw new ForyInvalidDataException($"serializer handle for {typeof(T)} is not initialized"); + throw new InvalidDataException($"serializer handle for {typeof(T)} is not initialized"); } return _binding; @@ -258,7 +258,7 @@ public static void WriteTypeInfo(ref WriteContext context) { if (info.NamespaceName is null) { - throw new ForyInvalidDataException("missing namespace metadata for name-registered type"); + throw new InvalidDataException("missing namespace metadata for name-registered type"); } WriteMetaString( @@ -280,7 +280,7 @@ public static void WriteTypeInfo(ref WriteContext context) { if (!info.UserTypeId.HasValue) { - throw new ForyInvalidDataException("missing user type id for id-registered type"); + throw new InvalidDataException("missing user type id for id-registered type"); } context.Writer.WriteVarUInt32(info.UserTypeId.Value); @@ -296,7 +296,7 @@ public static void ReadTypeInfo(ref ReadContext context) uint rawTypeId = context.Reader.ReadVarUInt32(); if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) { - throw new ForyInvalidDataException($"unknown type id {rawTypeId}"); + throw new InvalidDataException($"unknown type id {rawTypeId}"); } TypeId typeId = (TypeId)rawTypeId; @@ -305,7 +305,7 @@ public static void ReadTypeInfo(ref ReadContext context) { if (typeId != staticTypeId) { - throw new ForyTypeMismatchException((uint)staticTypeId, rawTypeId); + throw new TypeMismatchException((uint)staticTypeId, rawTypeId); } return; @@ -317,7 +317,7 @@ public static void ReadTypeInfo(ref ReadContext context) if (!allowed.Contains(typeId)) { uint expected = allowed.Count > 0 ? (uint)allowed.First() : 0; - throw new ForyTypeMismatchException(expected, rawTypeId); + throw new TypeMismatchException(expected, rawTypeId); } switch (typeId) @@ -356,12 +356,12 @@ public static void ReadTypeInfo(ref ReadContext context) TypeMetaEncodings.TypeNameMetaStringEncodings); if (!info.RegisterByName || info.NamespaceName is null) { - throw new ForyInvalidDataException("received name-registered type info for id-registered local type"); + throw new InvalidDataException("received name-registered type info for id-registered local type"); } if (namespaceName.Value != info.NamespaceName.Value.Value || typeName.Value != info.TypeName.Value) { - throw new ForyInvalidDataException( + throw new InvalidDataException( $"type name mismatch: expected {info.NamespaceName.Value.Value}::{info.TypeName.Value}, got {namespaceName.Value}::{typeName.Value}"); } } @@ -373,13 +373,13 @@ public static void ReadTypeInfo(ref ReadContext context) { if (!info.UserTypeId.HasValue) { - throw new ForyInvalidDataException("missing user type id for id-registered local type"); + throw new InvalidDataException("missing user type id for id-registered local type"); } uint remoteUserTypeId = context.Reader.ReadVarUInt32(); if (remoteUserTypeId != info.UserTypeId.Value) { - throw new ForyTypeMismatchException(info.UserTypeId.Value, remoteUserTypeId); + throw new TypeMismatchException(info.UserTypeId.Value, remoteUserTypeId); } } @@ -464,7 +464,7 @@ private static TypeMeta BuildCompatibleTypeMeta( { if (info.NamespaceName is null) { - throw new ForyInvalidDataException("missing namespace metadata for name-registered type"); + throw new InvalidDataException("missing namespace metadata for name-registered type"); } return new TypeMeta( @@ -479,7 +479,7 @@ private static TypeMeta BuildCompatibleTypeMeta( if (!info.UserTypeId.HasValue) { - throw new ForyInvalidDataException("missing user type id metadata for id-registered type"); + throw new InvalidDataException("missing user type id metadata for id-registered type"); } return new TypeMeta( @@ -502,19 +502,19 @@ private static void ValidateCompatibleTypeMeta( { if (!localInfo.RegisterByName || localInfo.NamespaceName is null) { - throw new ForyInvalidDataException( + throw new InvalidDataException( "received name-registered compatible metadata for id-registered local type"); } if (remoteTypeMeta.NamespaceName.Value != localInfo.NamespaceName.Value.Value) { - throw new ForyInvalidDataException( + throw new InvalidDataException( $"namespace mismatch: expected {localInfo.NamespaceName.Value.Value}, got {remoteTypeMeta.NamespaceName.Value}"); } if (remoteTypeMeta.TypeName.Value != localInfo.TypeName.Value) { - throw new ForyInvalidDataException( + throw new InvalidDataException( $"type name mismatch: expected {localInfo.TypeName.Value}, got {remoteTypeMeta.TypeName.Value}"); } } @@ -522,23 +522,23 @@ private static void ValidateCompatibleTypeMeta( { if (localInfo.RegisterByName) { - throw new ForyInvalidDataException( + throw new InvalidDataException( "received id-registered compatible metadata for name-registered local type"); } if (!remoteTypeMeta.UserTypeId.HasValue) { - throw new ForyInvalidDataException("missing user type id in compatible type metadata"); + throw new InvalidDataException("missing user type id in compatible type metadata"); } if (!localInfo.UserTypeId.HasValue) { - throw new ForyInvalidDataException("missing local user type id metadata for id-registered type"); + throw new InvalidDataException("missing local user type id metadata for id-registered type"); } if (remoteTypeMeta.UserTypeId.Value != localInfo.UserTypeId.Value) { - throw new ForyTypeMismatchException(localInfo.UserTypeId.Value, remoteTypeMeta.UserTypeId.Value); + throw new TypeMismatchException(localInfo.UserTypeId.Value, remoteTypeMeta.UserTypeId.Value); } } @@ -548,7 +548,7 @@ private static void ValidateCompatibleTypeMeta( TypeId remoteWireTypeId = (TypeId)remoteTypeMeta.TypeId.Value; if (!expectedWireTypes.Contains(remoteWireTypeId)) { - throw new ForyTypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value); + throw new TypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value); } } } @@ -564,7 +564,7 @@ private static void WriteMetaString( : encoder.Encode(value.Value, encodings); if (!encodings.Contains(normalized.Encoding)) { - throw new ForyEncodingException("failed to normalize meta string encoding"); + throw new EncodingException("failed to normalize meta string encoding"); } byte[] bytes = normalized.Bytes; @@ -603,7 +603,7 @@ private static MetaString ReadMetaString( MetaString? cached = context.MetaStringReadState.ValueAt(index); if (cached is null) { - throw new ForyInvalidDataException($"unknown meta string ref index {index}"); + throw new InvalidDataException($"unknown meta string ref index {index}"); } return cached.Value; @@ -630,7 +630,7 @@ private static MetaString ReadMetaString( if (!encodings.Contains(encoding)) { - throw new ForyInvalidDataException($"meta string encoding {encoding} not allowed in this context"); + throw new InvalidDataException($"meta string encoding {encoding} not allowed in this context"); } byte[] bytes = context.Reader.ReadBytes(length); diff --git a/csharp/src/Fory/SerializerRegistry.cs b/csharp/src/Fory/SerializerRegistry.cs index d0248b0cb4..0b1c2702c3 100644 --- a/csharp/src/Fory/SerializerRegistry.cs +++ b/csharp/src/Fory/SerializerRegistry.cs @@ -251,7 +251,7 @@ private static SerializerBinding Create(Type type) return generatedSerializer!; } - throw new ForyTypeNotRegisteredException($"No serializer available for {type}"); + throw new TypeNotRegisteredException($"No serializer available for {type}"); } private static void RegisterBuiltins() diff --git a/csharp/src/Fory/TypeMeta.cs b/csharp/src/Fory/TypeMeta.cs index b9465f2e56..d51b7a2e02 100644 --- a/csharp/src/Fory/TypeMeta.cs +++ b/csharp/src/Fory/TypeMeta.cs @@ -277,7 +277,7 @@ internal void Write(ByteWriter writer) short fieldId = FieldId.Value; if (fieldId < 0) { - throw new ForyEncodingException("negative field id is invalid"); + throw new EncodingException("negative field id is invalid"); } int size = fieldId; @@ -303,7 +303,7 @@ internal void Write(ByteWriter writer) int encodingIndex = Array.IndexOf(TypeMetaEncodings.FieldNameMetaStringEncodings, encoded.Encoding); if (encodingIndex < 0) { - throw new ForyEncodingException("unsupported field name encoding"); + throw new EncodingException("unsupported field name encoding"); } int encodedSize = encoded.Bytes.Length - 1; @@ -348,7 +348,7 @@ internal static TypeMetaFieldInfo Read(ByteReader reader) if (encodingFlags >= TypeMetaEncodings.FieldNameMetaStringEncodings.Length) { - throw new ForyInvalidDataException("invalid field name encoding id"); + throw new InvalidDataException("invalid field name encoding id"); } byte[] nameBytes = reader.ReadBytes(size); @@ -398,19 +398,19 @@ public TypeMeta( { if (typeName.Value.Length == 0) { - throw new ForyEncodingException("type name is required in register-by-name mode"); + throw new EncodingException("type name is required in register-by-name mode"); } } else { if (!typeId.HasValue) { - throw new ForyEncodingException("type id is required in register-by-id mode"); + throw new EncodingException("type id is required in register-by-id mode"); } if (!userTypeId.HasValue || userTypeId.Value == TypeMetaConstants.NoUserTypeId) { - throw new ForyEncodingException("user type id is required in register-by-id mode"); + throw new EncodingException("user type id is required in register-by-id mode"); } } @@ -447,7 +447,7 @@ public byte[] Encode() { if (Compressed) { - throw new ForyEncodingException("compressed TypeMeta is not supported yet"); + throw new EncodingException("compressed TypeMeta is not supported yet"); } byte[] body = EncodeBody(); @@ -498,7 +498,7 @@ public static TypeMeta Decode(ByteReader reader) byte[] encodedBody = reader.ReadBytes(metaSize); if (compressed) { - throw new ForyEncodingException("compressed TypeMeta is not supported yet"); + throw new EncodingException("compressed TypeMeta is not supported yet"); } ByteReader bodyReader = new(encodedBody); @@ -537,7 +537,7 @@ public static TypeMeta Decode(ByteReader reader) if (bodyReader.Remaining != 0) { - throw new ForyInvalidDataException("unexpected trailing bytes in TypeMeta body"); + throw new InvalidDataException("unexpected trailing bytes in TypeMeta body"); } return new TypeMeta( @@ -576,12 +576,12 @@ private byte[] EncodeBody() { if (!TypeId.HasValue) { - throw new ForyEncodingException("type id is required in register-by-id mode"); + throw new EncodingException("type id is required in register-by-id mode"); } if (!UserTypeId.HasValue || UserTypeId == TypeMetaConstants.NoUserTypeId) { - throw new ForyEncodingException("user type id is required in register-by-id mode"); + throw new EncodingException("user type id is required in register-by-id mode"); } writer.WriteUInt8(unchecked((byte)TypeId.Value)); @@ -609,7 +609,7 @@ private static void WriteName(ByteWriter writer, MetaString name, IReadOnlyList< int encodingIndex = TypeMetaUtils.EncodingIndexOf(encodings, normalized.Encoding); if (encodingIndex < 0) { - throw new ForyEncodingException("failed to normalize meta string encoding"); + throw new EncodingException("failed to normalize meta string encoding"); } byte[] bytes = normalized.Bytes; @@ -632,7 +632,7 @@ private static MetaString ReadName(ByteReader reader, MetaStringDecoder decoder, int encodingIndex = header & 0b11; if (encodingIndex >= encodings.Count) { - throw new ForyInvalidDataException("invalid meta string encoding index"); + throw new InvalidDataException("invalid meta string encoding index"); } int length = header >> 2; diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index 6bf129d719..25f133cd7c 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -110,14 +110,14 @@ internal RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) return info; } - throw new ForyTypeNotRegisteredException($"{type} is not registered"); + throw new TypeNotRegisteredException($"{type} is not registered"); } public object? ReadByUserTypeId(uint userTypeId, ref ReadContext context, TypeMeta? compatibleTypeMeta = null) { if (!_byUserTypeId.TryGetValue(userTypeId, out TypeReader? entry)) { - throw new ForyTypeNotRegisteredException($"user_type_id={userTypeId}"); + throw new TypeNotRegisteredException($"user_type_id={userTypeId}"); } return compatibleTypeMeta is null @@ -129,7 +129,7 @@ internal RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) { if (!_byTypeName.TryGetValue(new TypeNameKey(namespaceName, typeName), out TypeReader? entry)) { - throw new ForyTypeNotRegisteredException($"namespace={namespaceName}, type={typeName}"); + throw new TypeNotRegisteredException($"namespace={namespaceName}, type={typeName}"); } return compatibleTypeMeta is null @@ -142,7 +142,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) uint rawTypeId = context.Reader.ReadVarUInt32(); if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) { - throw new ForyInvalidDataException($"unknown dynamic type id {rawTypeId}"); + throw new InvalidDataException($"unknown dynamic type id {rawTypeId}"); } TypeId wireTypeId = (TypeId)rawTypeId; @@ -186,7 +186,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) return new DynamicTypeInfo(wireTypeId, null, namespaceName, typeName, null); } - throw new ForyInvalidDataException($"ambiguous dynamic type registration mode for {wireTypeId}"); + throw new InvalidDataException($"ambiguous dynamic type registration mode for {wireTypeId}"); } default: return new DynamicTypeInfo(wireTypeId, null, null, null, null); @@ -279,7 +279,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) return ReadByTypeName(typeInfo.NamespaceName.Value.Value, typeInfo.TypeName.Value.Value, ref context); } - throw new ForyInvalidDataException($"missing dynamic registration info for {typeInfo.WireTypeId}"); + throw new InvalidDataException($"missing dynamic registration info for {typeInfo.WireTypeId}"); } case TypeId.NamedStruct: case TypeId.NamedEnum: @@ -288,7 +288,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) { if (!typeInfo.NamespaceName.HasValue || !typeInfo.TypeName.HasValue) { - throw new ForyInvalidDataException($"missing dynamic type name for {typeInfo.WireTypeId}"); + throw new InvalidDataException($"missing dynamic type name for {typeInfo.WireTypeId}"); } return ReadByTypeName(typeInfo.NamespaceName.Value.Value, typeInfo.TypeName.Value.Value, ref context); @@ -298,7 +298,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) { if (typeInfo.CompatibleTypeMeta is null) { - throw new ForyInvalidDataException($"missing compatible type meta for {typeInfo.WireTypeId}"); + throw new InvalidDataException($"missing compatible type meta for {typeInfo.WireTypeId}"); } TypeMeta compatibleTypeMeta = typeInfo.CompatibleTypeMeta; @@ -313,7 +313,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) if (!compatibleTypeMeta.UserTypeId.HasValue) { - throw new ForyInvalidDataException("missing user type id in compatible dynamic type meta"); + throw new InvalidDataException("missing user type id in compatible dynamic type meta"); } return ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value, ref context, compatibleTypeMeta); @@ -321,7 +321,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) case TypeId.None: return null; default: - throw new ForyInvalidDataException($"unsupported dynamic type id {typeInfo.WireTypeId}"); + throw new InvalidDataException($"unsupported dynamic type id {typeInfo.WireTypeId}"); } } @@ -347,7 +347,7 @@ private DynamicRegistrationMode DynamicRegistrationModeFor(TypeId kind) return mode; } - throw new ForyTypeNotRegisteredException($"no dynamic registration mode for kind {kind}"); + throw new TypeNotRegisteredException($"no dynamic registration mode for kind {kind}"); } private static MetaString ReadMetaString(ByteReader reader, MetaStringDecoder decoder, IReadOnlyList encodings) @@ -356,7 +356,7 @@ private static MetaString ReadMetaString(ByteReader reader, MetaStringDecoder de int encodingIndex = header & 0b11; if (encodingIndex >= encodings.Count) { - throw new ForyInvalidDataException("invalid meta string encoding index"); + throw new InvalidDataException("invalid meta string encoding index"); } int length = header >> 2; diff --git a/csharp/src/Fory/TypedSerializerBinding.cs b/csharp/src/Fory/TypedSerializerBinding.cs index a9470acb38..48a1aad111 100644 --- a/csharp/src/Fory/TypedSerializerBinding.cs +++ b/csharp/src/Fory/TypedSerializerBinding.cs @@ -254,7 +254,7 @@ public TypedSerializerBinding RequireTypedBinding() return typed; } - throw new ForyInvalidDataException($"serializer type mismatch for {typeof(T)}"); + throw new InvalidDataException($"serializer type mismatch for {typeof(T)}"); } } @@ -359,7 +359,7 @@ private static T CoerceValue(object? value) return TSerializer.DefaultValue; } - throw new ForyInvalidDataException( + throw new InvalidDataException( $"serializer {typeof(TSerializer).Name} expected value of type {typeof(T)}, got {value?.GetType()}"); } } diff --git a/csharp/src/Fory/UnionSerializer.cs b/csharp/src/Fory/UnionSerializer.cs index adef58e9a9..d884ef6efe 100644 --- a/csharp/src/Fory/UnionSerializer.cs +++ b/csharp/src/Fory/UnionSerializer.cs @@ -43,7 +43,7 @@ public static void WriteData(ref WriteContext context, in TUnion value, bool has _ = hasGenerics; if (value is null) { - throw new ForyInvalidDataException("union value is null"); + throw new InvalidDataException("union value is null"); } context.Writer.WriteVarUInt32((uint)value.Index); @@ -55,7 +55,7 @@ public static TUnion ReadData(ref ReadContext context) uint rawCaseId = context.Reader.ReadVarUInt32(); if (rawCaseId > int.MaxValue) { - throw new ForyInvalidDataException($"union case id out of range: {rawCaseId}"); + throw new InvalidDataException($"union case id out of range: {rawCaseId}"); } object? caseValue = DynamicAnyCodec.ReadAny(ref context, RefMode.Tracking, true); @@ -93,7 +93,7 @@ public static TUnion ReadData(ref ReadContext context) return (index, value) => (TUnion)ofFactory.Invoke(null, [index, value])!; } - throw new ForyInvalidDataException( + throw new InvalidDataException( $"union type {typeof(TUnion)} must define (int, object) constructor or static Of(int, object)"); } } diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index c6a5cdf152..38da28f0e9 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -412,7 +412,7 @@ public void SchemaVersionMismatchThrows() reader.Register(200); byte[] payload = writer.Serialize(new OneStringField { F1 = "hello" }); - Assert.Throws(() => { _ = reader.Deserialize(payload); }); + Assert.Throws(() => { _ = reader.Deserialize(payload); }); } [Fact] From 0a7b66fac8f35c140605f9748f2ea844b28ecb1f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 21 Feb 2026 14:13:58 +0800 Subject: [PATCH 11/16] refactor(csharp): move serializer dispatch into type resolver --- .../src/Fory.Generator/ForyObjectGenerator.cs | 30 +- csharp/src/Fory/AnySerializer.cs | 15 +- csharp/src/Fory/CollectionSerializers.cs | 30 +- csharp/src/Fory/DictionarySerializers.cs | 11 +- csharp/src/Fory/FieldSkipper.cs | 24 +- csharp/src/Fory/Fory.cs | 12 +- csharp/src/Fory/NullableKeyDictionary.cs | 11 +- csharp/src/Fory/OptionalSerializer.cs | 34 +- csharp/src/Fory/SerializerRegistry.cs | 290 ---------------- csharp/src/Fory/TypeResolver.cs | 316 ++++++++++++++++-- csharp/src/Fory/TypedSerializerBinding.cs | 33 -- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 2 +- 12 files changed, 364 insertions(+), 444 deletions(-) delete mode 100644 csharp/src/Fory/SerializerRegistry.cs diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index 64c8dd95ed..ca354622ec 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -118,12 +118,12 @@ private static void Emit(SourceProductionContext context, ImmutableArray>();"); + $" global::Apache.Fory.TypeResolver.RegisterGenerated<{model.TypeName}, global::Apache.Fory.EnumSerializer<{model.TypeName}>>();"); } else { sb.AppendLine( - $" global::Apache.Fory.SerializerRegistry.RegisterGenerated<{model.TypeName}, {model.SerializerName}>();"); + $" global::Apache.Fory.TypeResolver.RegisterGenerated<{model.TypeName}, {model.SerializerName}>();"); } } @@ -162,7 +162,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" };"); sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" private static uint __ForySchemaHash(bool trackRef)"); + sb.AppendLine(" private static uint __ForySchemaHash(bool trackRef, global::Apache.Fory.TypeResolver typeResolver)"); sb.AppendLine(" {"); sb.Append(" return global::Apache.Fory.SchemaHash.StructHash32("); sb.Append(BuildSchemaFingerprintExpression(model.Members)); @@ -226,7 +226,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" context.Writer.WriteInt32(unchecked((int)__ForySchemaHash(context.TrackRef)));"); + sb.AppendLine(" context.Writer.WriteInt32(unchecked((int)__ForySchemaHash(context.TrackRef, context.TypeResolver)));"); foreach (MemberModel member in model.SortedMembers) { EmitWriteMember(sb, member, false); @@ -269,7 +269,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" uint schemaHash = unchecked((uint)context.Reader.ReadInt32());"); - sb.AppendLine(" uint expectedHash = __ForySchemaHash(context.TrackRef);"); + sb.AppendLine(" uint expectedHash = __ForySchemaHash(context.TrackRef, context.TypeResolver);"); sb.AppendLine(" if (schemaHash != expectedHash)"); sb.AppendLine(" {"); sb.AppendLine(" throw new global::Apache.Fory.InvalidDataException($\"class version hash mismatch: expected {expectedHash}, got {schemaHash}\");"); @@ -297,7 +297,7 @@ private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool c string memberAccess = $"value.{member.Name}"; string hasGenerics = member.IsCollection ? "true" : "false"; string writeTypeInfo = compatibleMode - ? $"__ForyNeedsTypeInfoForField(global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().StaticTypeId)" + ? $"__ForyNeedsTypeInfoForField(context.TypeResolver.GetSerializer<{member.TypeName}>().StaticTypeId)" : "false"; switch (member.DynamicAnyKind) @@ -322,19 +322,19 @@ private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool c sb.AppendLine( $" {member.CustomWrapperTypeName}? {wrappedVarName} = {memberAccess}.HasValue ? new {member.CustomWrapperTypeName}({memberAccess}.Value) : null;"); sb.AppendLine( - $" global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}?>().Write(ref context, {wrappedVarName}, {refModeExpr}, false, false);"); + $" context.TypeResolver.GetSerializer<{member.CustomWrapperTypeName}?>().Write(ref context, {wrappedVarName}, {refModeExpr}, false, false);"); } else { sb.AppendLine( - $" global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}>().Write(ref context, new {member.CustomWrapperTypeName}({memberAccess}), {refModeExpr}, false, false);"); + $" context.TypeResolver.GetSerializer<{member.CustomWrapperTypeName}>().Write(ref context, new {member.CustomWrapperTypeName}({memberAccess}), {refModeExpr}, false, false);"); } return; } sb.AppendLine( - $" global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().Write(ref context, {memberAccess}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});"); + $" context.TypeResolver.GetSerializer<{member.TypeName}>().Write(ref context, {memberAccess}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});"); } private static void EmitReadMemberAssignment( @@ -367,21 +367,21 @@ private static void EmitReadMemberAssignment( if (member.IsNullableValueType) { sb.AppendLine( - $"{indent}{member.CustomWrapperTypeName}? {wrappedVarName} = global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}?>().Read(ref context, {refModeExpr}, false);"); + $"{indent}{member.CustomWrapperTypeName}? {wrappedVarName} = context.TypeResolver.GetSerializer<{member.CustomWrapperTypeName}?>().Read(ref context, {refModeExpr}, false);"); sb.AppendLine( $"{indent}{assignmentTarget} = {wrappedVarName}.HasValue ? {wrappedVarName}.Value.RawValue : ({member.TypeName})null!;"); } else { sb.AppendLine( - $"{indent}{assignmentTarget} = global::Apache.Fory.SerializerRegistry.Get<{member.CustomWrapperTypeName}>().Read(ref context, {refModeExpr}, false).RawValue;"); + $"{indent}{assignmentTarget} = context.TypeResolver.GetSerializer<{member.CustomWrapperTypeName}>().Read(ref context, {refModeExpr}, false).RawValue;"); } return; } sb.AppendLine( - $"{indent}{assignmentTarget} = global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().Read(ref context, {refModeExpr}, {readTypeInfoExpr});"); + $"{indent}{assignmentTarget} = context.TypeResolver.GetSerializer<{member.TypeName}>().Read(ref context, {refModeExpr}, {readTypeInfoExpr});"); } private static string StripNullableForTypeOf(string typeName) @@ -412,7 +412,7 @@ private static string BuildSchemaFingerprintExpression(ImmutableArray "(trackRef ? 1 : 0)", _ => member.Classification.IsBuiltIn ? "0" - : $"((trackRef && global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)", + : $"((trackRef && typeResolver.GetSerializer<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)", }; string nullable = member.IsNullable ? "1" : "0"; string piece = $"\"{EscapeString(member.FieldIdentifier)},{fingerprintTypeId},\" + {trackRefExpr} + \",{nullable};\""; @@ -450,7 +450,7 @@ private static string BuildWriteRefModeExpression(MemberModel member) DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)", _ => member.Classification.IsBuiltIn ? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)" - : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && global::Apache.Fory.SerializerRegistry.Get<{member.TypeName}>().IsReferenceTrackableType)", + : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && context.TypeResolver.GetSerializer<{member.TypeName}>().IsReferenceTrackableType)", }; } @@ -732,7 +732,7 @@ private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel( string typeName = memberType.ToDisplayString(FullNameFormat); TypeClassification classification = ClassifyType(unwrapped); return new TypeMetaFieldTypeModel( - $"(uint)global::Apache.Fory.SerializerRegistry.Get<{typeName}>().StaticTypeId", + $"(uint)global::Apache.Fory.TypeResolver.StaticTypeIdOf<{typeName}>()", nullable, !classification.IsBuiltIn && unwrapped.TypeKind != TypeKind.Enum, ImmutableArray.Empty); diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index 8763fc17f0..d5e7144542 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -68,7 +68,7 @@ public static void Write(ref WriteContext context, in object? value, RefMode ref } bool wroteTrackingRefFlag = false; - if (refMode == RefMode.Tracking && AnyValueIsReferenceTrackable(value!)) + if (refMode == RefMode.Tracking && AnyValueIsReferenceTrackable(value!, context.TypeResolver)) { if (context.RefWriter.TryWriteReference(context.Writer, value!)) { @@ -147,12 +147,11 @@ public static void Write(ref WriteContext context, in object? value, RefMode ref return result; } - private static bool AnyValueIsReferenceTrackable(object value) + private static bool AnyValueIsReferenceTrackable(object value, TypeResolver typeResolver) { - SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); + SerializerBinding serializer = typeResolver.GetBinding(value.GetType()); return serializer.IsReferenceTrackableType; } - } public static class DynamicAnyCodec @@ -165,7 +164,7 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) return; } - SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); + SerializerBinding serializer = context.TypeResolver.GetBinding(value.GetType()); serializer.WriteTypeInfo(ref context); } @@ -196,12 +195,12 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) public static void WriteAny(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo = true, bool hasGenerics = false) { - SerializerRegistry.Get().Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + context.TypeResolver.GetSerializer().Write(ref context, value, refMode, writeTypeInfo, hasGenerics); } public static object? ReadAny(ref ReadContext context, RefMode refMode, bool readTypeInfo = true) { - return SerializerRegistry.Get().Read(ref context, refMode, readTypeInfo); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, readTypeInfo); } public static void WriteAnyPayload(object value, ref WriteContext context, bool hasGenerics) @@ -211,7 +210,7 @@ public static void WriteAnyPayload(object value, ref WriteContext context, bool return; } - SerializerBinding serializer = SerializerRegistry.GetBinding(value.GetType()); + SerializerBinding serializer = context.TypeResolver.GetBinding(value.GetType()); serializer.WriteData(ref context, value, hasGenerics); } } diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index 31b2c7dd95..67396f6c2f 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -309,7 +309,7 @@ public static bool TryWritePayload(object value, ref WriteContext context, bool map.Add(entry.Key, entry.Value); } - SerializerRegistry.Get>().WriteData(ref context, map, false); + context.TypeResolver.GetSerializer>().WriteData(ref context, map, false); return true; } @@ -322,7 +322,7 @@ public static bool TryWritePayload(object value, ref WriteContext context, bool values.Add(list[i]); } - SerializerRegistry.Get>().WriteData(ref context, values, hasGenerics); + context.TypeResolver.GetSerializer>().WriteData(ref context, values, hasGenerics); return true; } @@ -337,23 +337,23 @@ public static bool TryWritePayload(object value, ref WriteContext context, bool set.Add(item); } - SerializerRegistry.Get>().WriteData(ref context, set, hasGenerics); + context.TypeResolver.GetSerializer>().WriteData(ref context, set, hasGenerics); return true; } public static List ReadListPayload(ref ReadContext context) { - return SerializerRegistry.Get>().ReadData(ref context); + return context.TypeResolver.GetSerializer>().ReadData(ref context); } public static HashSet ReadSetPayload(ref ReadContext context) { - return SerializerRegistry.Get>().ReadData(ref context); + return context.TypeResolver.GetSerializer>().ReadData(ref context); } public static object ReadMapPayload(ref ReadContext context) { - NullableKeyDictionary map = SerializerRegistry.Get>().ReadData(ref context); + NullableKeyDictionary map = context.TypeResolver.GetSerializer>().ReadData(ref context); if (map.HasNullKey) { return map; @@ -738,7 +738,6 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) public readonly struct ArraySerializer : IStaticSerializer, T[]> { - private static Serializer ElementSerializer => SerializerRegistry.Get(); private static readonly TypeId? PrimitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); public static TypeId StaticTypeId => PrimitiveArrayTypeId ?? TypeId.List; @@ -758,7 +757,7 @@ public static void WriteData(ref WriteContext context, in T[] value, bool hasGen CollectionCodec.WriteCollectionData( safe, - ElementSerializer, + context.TypeResolver.GetSerializer(), ref context, hasGenerics); } @@ -770,15 +769,13 @@ public static T[] ReadData(ref ReadContext context) return PrimitiveArrayCodec.ReadPrimitiveArray(ref context); } - List values = CollectionCodec.ReadCollectionData(ElementSerializer, ref context); + List values = CollectionCodec.ReadCollectionData(context.TypeResolver.GetSerializer(), ref context); return values.ToArray(); } } public readonly struct ListSerializer : IStaticSerializer, List> { - private static Serializer ElementSerializer => SerializerRegistry.Get(); - public static TypeId StaticTypeId => TypeId.List; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; @@ -788,19 +785,17 @@ public static T[] ReadData(ref ReadContext context) public static void WriteData(ref WriteContext context, in List value, bool hasGenerics) { List safe = value ?? []; - CollectionCodec.WriteCollectionData(safe, ElementSerializer, ref context, hasGenerics); + CollectionCodec.WriteCollectionData(safe, context.TypeResolver.GetSerializer(), ref context, hasGenerics); } public static List ReadData(ref ReadContext context) { - return CollectionCodec.ReadCollectionData(ElementSerializer, ref context); + return CollectionCodec.ReadCollectionData(context.TypeResolver.GetSerializer(), ref context); } } public readonly struct SetSerializer : IStaticSerializer, HashSet> where T : notnull { - private static Serializer> ListSerializer => SerializerRegistry.Get>(); - public static TypeId StaticTypeId => TypeId.Set; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; @@ -810,12 +805,11 @@ public static List ReadData(ref ReadContext context) public static void WriteData(ref WriteContext context, in HashSet value, bool hasGenerics) { List list = value is null ? [] : [.. value]; - ListSerializer.WriteData(ref context, list, hasGenerics); + context.TypeResolver.GetSerializer>().WriteData(ref context, list, hasGenerics); } public static HashSet ReadData(ref ReadContext context) { - return [.. ListSerializer.ReadData(ref context)]; + return [.. context.TypeResolver.GetSerializer>().ReadData(ref context)]; } } - diff --git a/csharp/src/Fory/DictionarySerializers.cs b/csharp/src/Fory/DictionarySerializers.cs index 9637e70c13..fa421d69b0 100644 --- a/csharp/src/Fory/DictionarySerializers.cs +++ b/csharp/src/Fory/DictionarySerializers.cs @@ -30,9 +30,6 @@ internal static class DictionaryBits public readonly struct DictionarySerializer : IStaticSerializer, Dictionary> where TKey : notnull { - private static Serializer KeySerializer => SerializerRegistry.Get(); - private static Serializer ValueSerializer => SerializerRegistry.Get(); - public static TypeId StaticTypeId => TypeId.Map; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; @@ -41,8 +38,8 @@ internal static class DictionaryBits public static void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) { - Serializer keySerializer = KeySerializer; - Serializer valueSerializer = ValueSerializer; + Serializer keySerializer = context.TypeResolver.GetSerializer(); + Serializer valueSerializer = context.TypeResolver.GetSerializer(); Dictionary map = value ?? []; context.Writer.WriteVarUInt32((uint)map.Count); if (map.Count == 0) @@ -194,8 +191,8 @@ public static void WriteData(ref WriteContext context, in Dictionary ReadData(ref ReadContext context) { - Serializer keySerializer = KeySerializer; - Serializer valueSerializer = ValueSerializer; + Serializer keySerializer = context.TypeResolver.GetSerializer(); + Serializer valueSerializer = context.TypeResolver.GetSerializer(); int totalLength = checked((int)context.Reader.ReadVarUInt32()); if (totalLength == 0) { diff --git a/csharp/src/Fory/FieldSkipper.cs b/csharp/src/Fory/FieldSkipper.cs index e66e6e7a69..22121372cd 100644 --- a/csharp/src/Fory/FieldSkipper.cs +++ b/csharp/src/Fory/FieldSkipper.cs @@ -57,21 +57,21 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie switch (fieldType.TypeId) { case (uint)TypeId.Bool: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.Int8: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.Int16: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.VarInt32: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.VarInt64: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.Float32: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.Float64: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.String: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); case (uint)TypeId.List: { if (fieldType.Generics.Count != 1 || fieldType.Generics[0].TypeId != (uint)TypeId.String) @@ -79,7 +79,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie throw new InvalidDataException("unsupported compatible list element type"); } - return SerializerRegistry.Get>().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer>().Read(ref context, refMode, false); } case (uint)TypeId.Set: { @@ -88,7 +88,7 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie throw new InvalidDataException("unsupported compatible set element type"); } - return SerializerRegistry.Get>().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer>().Read(ref context, refMode, false); } case (uint)TypeId.Map: { @@ -99,14 +99,14 @@ public static void SkipFieldValue(ref ReadContext context, TypeMetaFieldType fie throw new InvalidDataException("unsupported compatible map key/value type"); } - return SerializerRegistry.Get>().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer>().Read(ref context, refMode, false); } case (uint)TypeId.Enum: return ReadEnumOrdinal(ref context, refMode); case (uint)TypeId.Union: case (uint)TypeId.TypedUnion: case (uint)TypeId.NamedUnion: - return SerializerRegistry.Get().Read(ref context, refMode, false); + return context.TypeResolver.GetSerializer().Read(ref context, refMode, false); default: throw new InvalidDataException($"unsupported compatible field type id {fieldType.TypeId}"); } diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs index 1ee87a3370..6765b5f942 100644 --- a/csharp/src/Fory/Fory.cs +++ b/csharp/src/Fory/Fory.cs @@ -57,23 +57,23 @@ public Fory Register(string typeNamespace, string typeName) public Fory Register(uint typeId) where TSerializer : IStaticSerializer { - SerializerRegistry.RegisterCustom(); - _typeResolver.Register(typeof(T), typeId); + SerializerBinding serializerBinding = _typeResolver.RegisterCustom(); + _typeResolver.Register(typeof(T), typeId, serializerBinding); return this; } public Fory Register(string typeNamespace, string typeName) where TSerializer : IStaticSerializer { - SerializerRegistry.RegisterCustom(); - _typeResolver.Register(typeof(T), typeNamespace, typeName); + SerializerBinding serializerBinding = _typeResolver.RegisterCustom(); + _typeResolver.Register(typeof(T), typeNamespace, typeName, serializerBinding); return this; } public byte[] Serialize(in T value) { ByteWriter writer = new(); - Serializer binding = TypedSerializerBindingCache.Get(); + Serializer binding = _typeResolver.GetSerializer(); bool isNone = binding.IsNone(value); WriteHead(writer, isNone); if (!isNone) @@ -200,7 +200,7 @@ public bool ReadHead(ByteReader reader) private T DeserializeFromReader(ByteReader reader) { bool isNone = ReadHead(reader); - Serializer binding = TypedSerializerBindingCache.Get(); + Serializer binding = _typeResolver.GetSerializer(); if (isNone) { return binding.DefaultValue; diff --git a/csharp/src/Fory/NullableKeyDictionary.cs b/csharp/src/Fory/NullableKeyDictionary.cs index 0c4a603a98..4a8b879f55 100644 --- a/csharp/src/Fory/NullableKeyDictionary.cs +++ b/csharp/src/Fory/NullableKeyDictionary.cs @@ -390,9 +390,6 @@ IEnumerator IEnumerable.GetEnumerator() public readonly struct NullableKeyDictionarySerializer : IStaticSerializer, NullableKeyDictionary> { - private static Serializer KeySerializer => SerializerRegistry.Get(); - private static Serializer ValueSerializer => SerializerRegistry.Get(); - public static TypeId StaticTypeId => TypeId.Map; public static bool IsNullableType => true; public static bool IsReferenceTrackableType => true; @@ -401,8 +398,8 @@ IEnumerator IEnumerable.GetEnumerator() public static void WriteData(ref WriteContext context, in NullableKeyDictionary value, bool hasGenerics) { - Serializer keySerializer = KeySerializer; - Serializer valueSerializer = ValueSerializer; + Serializer keySerializer = context.TypeResolver.GetSerializer(); + Serializer valueSerializer = context.TypeResolver.GetSerializer(); NullableKeyDictionary map = value ?? new NullableKeyDictionary(); context.Writer.WriteVarUInt32((uint)map.Count); if (map.Count == 0) @@ -533,8 +530,8 @@ public static void WriteData(ref WriteContext context, in NullableKeyDictionary< public static NullableKeyDictionary ReadData(ref ReadContext context) { - Serializer keySerializer = KeySerializer; - Serializer valueSerializer = ValueSerializer; + Serializer keySerializer = context.TypeResolver.GetSerializer(); + Serializer valueSerializer = context.TypeResolver.GetSerializer(); int totalLength = checked((int)context.Reader.ReadVarUInt32()); if (totalLength == 0) { diff --git a/csharp/src/Fory/OptionalSerializer.cs b/csharp/src/Fory/OptionalSerializer.cs index f692c4e552..ad650c59f1 100644 --- a/csharp/src/Fory/OptionalSerializer.cs +++ b/csharp/src/Fory/OptionalSerializer.cs @@ -19,13 +19,11 @@ namespace Apache.Fory; public readonly struct NullableSerializer : IStaticSerializer, T?> where T : struct { - private static Serializer WrappedSerializer => SerializerRegistry.Get(); - - public static TypeId StaticTypeId => WrappedSerializer.StaticTypeId; + public static TypeId StaticTypeId => TypeResolver.StaticTypeIdOf(); public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => WrappedSerializer.IsReferenceTrackableType; + public static bool IsReferenceTrackableType => TypeResolver.IsReferenceTrackableTypeOf(); public static T? DefaultValue => null; @@ -42,31 +40,36 @@ public static void WriteData(ref WriteContext context, in T? value, bool hasGene } T wrapped = value.Value; - WrappedSerializer.WriteData(ref context, wrapped, hasGenerics); + Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); + wrappedSerializer.WriteData(ref context, wrapped, hasGenerics); } public static T? ReadData(ref ReadContext context) { - return WrappedSerializer.ReadData(ref context); + Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); + return wrappedSerializer.ReadData(ref context); } public static void WriteTypeInfo(ref WriteContext context) { - WrappedSerializer.WriteTypeInfo(ref context); + Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); + wrappedSerializer.WriteTypeInfo(ref context); } public static void ReadTypeInfo(ref ReadContext context) { - WrappedSerializer.ReadTypeInfo(ref context); + Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); + wrappedSerializer.ReadTypeInfo(ref context); } public static IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { - return WrappedSerializer.CompatibleTypeMetaFields(trackRef); + return TypeResolver.CompatibleTypeMetaFieldsOf(trackRef); } public static void Write(ref WriteContext context, in T? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { + Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); switch (refMode) { case RefMode.None: @@ -76,7 +79,7 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, } T wrapped = value.Value; - WrappedSerializer.Write(ref context, wrapped, RefMode.None, writeTypeInfo, hasGenerics); + wrappedSerializer.Write(ref context, wrapped, RefMode.None, writeTypeInfo, hasGenerics); break; case RefMode.NullOnly: if (!value.HasValue) @@ -86,7 +89,7 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, } context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); - WrappedSerializer.Write(ref context, value.Value, RefMode.None, writeTypeInfo, hasGenerics); + wrappedSerializer.Write(ref context, value.Value, RefMode.None, writeTypeInfo, hasGenerics); break; case RefMode.Tracking: if (!value.HasValue) @@ -95,17 +98,18 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, return; } - WrappedSerializer.Write(ref context, value.Value, RefMode.Tracking, writeTypeInfo, hasGenerics); + wrappedSerializer.Write(ref context, value.Value, RefMode.Tracking, writeTypeInfo, hasGenerics); break; } } public static T? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { + Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); switch (refMode) { case RefMode.None: - return WrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); + return wrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); case RefMode.NullOnly: { sbyte refFlag = context.Reader.ReadInt8(); @@ -114,7 +118,7 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, return null; } - return WrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); + return wrappedSerializer.Read(ref context, RefMode.None, readTypeInfo); } case RefMode.Tracking: { @@ -125,7 +129,7 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, } context.Reader.MoveBack(1); - return WrappedSerializer.Read(ref context, RefMode.Tracking, readTypeInfo); + return wrappedSerializer.Read(ref context, RefMode.Tracking, readTypeInfo); } default: throw new InvalidDataException($"unsupported ref mode {refMode}"); diff --git a/csharp/src/Fory/SerializerRegistry.cs b/csharp/src/Fory/SerializerRegistry.cs deleted file mode 100644 index 0b1c2702c3..0000000000 --- a/csharp/src/Fory/SerializerRegistry.cs +++ /dev/null @@ -1,290 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -using System.Collections.Concurrent; -using System.Threading; - -namespace Apache.Fory; - -public static class SerializerRegistry -{ - private static readonly ConcurrentDictionary Cache = new(); - private static readonly ConcurrentDictionary> GeneratedFactories = new(); - private static int _version; - - static SerializerRegistry() - { - RegisterBuiltins(); - GeneratedSerializerRegistry.Register(GeneratedFactories); - } - - public static void RegisterGenerated() - where TSerializer : IStaticSerializer - { - GeneratedFactories[typeof(T)] = StaticSerializerBindingFactory.Create; - Cache.TryRemove(typeof(T), out _); - Interlocked.Increment(ref _version); - } - - public static void RegisterCustom() - where TSerializer : IStaticSerializer - { - Cache[typeof(T)] = StaticSerializerBindingFactory.Create(); - Interlocked.Increment(ref _version); - } - - internal static void RegisterCustom(Type type, SerializerBinding serializerBinding) - { - Cache[type] = serializerBinding; - Interlocked.Increment(ref _version); - } - - internal static int Version => Volatile.Read(ref _version); - - public static Serializer Get() - { - return TypedSerializerBindingCache.Get(); - } - - internal static SerializerBinding GetBinding(Type type) - { - return Cache.GetOrAdd(type, Create); - } - - private static SerializerBinding Create(Type type) - { - if (GeneratedFactories.TryGetValue(type, out Func? generatedFactory)) - { - return generatedFactory(); - } - - if (type == typeof(bool)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(sbyte)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(short)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(int)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(long)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(byte)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ushort)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(uint)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ulong)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(float)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(double)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(string)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(byte[])) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(DateOnly)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(DateTimeOffset)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(DateTime)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(TimeSpan)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ForyInt32Fixed)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ForyInt64Fixed)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ForyInt64Tagged)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ForyUInt32Fixed)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ForyUInt64Fixed)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(ForyUInt64Tagged)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (type == typeof(object)) - { - return StaticSerializerBindingFactory.Create(); - } - - if (typeof(Union).IsAssignableFrom(type)) - { - Type serializerType = typeof(UnionSerializer<>).MakeGenericType(type); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - - if (type.IsEnum) - { - Type serializerType = typeof(EnumSerializer<>).MakeGenericType(type); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - - if (type.IsArray) - { - Type elementType = type.GetElementType()!; - Type serializerType = typeof(ArraySerializer<>).MakeGenericType(elementType); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - - if (type.IsGenericType) - { - Type genericType = type.GetGenericTypeDefinition(); - Type[] genericArgs = type.GetGenericArguments(); - if (genericType == typeof(Nullable<>)) - { - Type serializerType = typeof(NullableSerializer<>).MakeGenericType(genericArgs[0]); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - - if (genericType == typeof(List<>)) - { - Type serializerType = typeof(ListSerializer<>).MakeGenericType(genericArgs[0]); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - - if (genericType == typeof(HashSet<>)) - { - Type serializerType = typeof(SetSerializer<>).MakeGenericType(genericArgs[0]); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - - if (genericType == typeof(Dictionary<,>)) - { - Type serializerType = typeof(DictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - - if (genericType == typeof(NullableKeyDictionary<,>)) - { - Type serializerType = typeof(NullableKeyDictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return StaticSerializerBindingFactory.Create(type, serializerType); - } - } - - if (GeneratedSerializerRegistry.TryCreate(type, out SerializerBinding? generatedSerializer)) - { - return generatedSerializer!; - } - - throw new TypeNotRegisteredException($"No serializer available for {type}"); - } - - private static void RegisterBuiltins() - { - Cache[typeof(bool)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(sbyte)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(short)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(int)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(long)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(byte)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(ushort)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(uint)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(ulong)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(float)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(double)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(string)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(byte[])] = StaticSerializerBindingFactory.Create(); - Cache[typeof(object)] = StaticSerializerBindingFactory.Create(); - Cache[typeof(Union)] = StaticSerializerBindingFactory.Create>(); - } -} - -internal static partial class GeneratedSerializerRegistry -{ - public static void Register(ConcurrentDictionary> factories) - { - _ = factories; - } - - public static bool TryCreate(Type type, out SerializerBinding? serializer) - { - _ = type; - serializer = null; - return false; - } -} diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index 25f133cd7c..2db261386e 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +using System.Collections.Concurrent; + namespace Apache.Fory; internal sealed record RegisteredTypeInfo( @@ -43,14 +45,75 @@ internal sealed class TypeReader public sealed class TypeResolver { + private static readonly ConcurrentDictionary> GeneratedFactories = new(); + private static readonly ConcurrentDictionary SharedStaticBindings = new(); + private readonly Dictionary _byType = []; private readonly Dictionary _byUserTypeId = []; private readonly Dictionary _byTypeName = []; private readonly Dictionary _registrationModeByKind = []; + private readonly ConcurrentDictionary _serializerBindings = new(); + private readonly ConcurrentDictionary _typedBindings = new(); + + public static void RegisterGenerated() + where TSerializer : IStaticSerializer + { + Type type = typeof(T); + GeneratedFactories[type] = StaticSerializerBindingFactory.Create; + SharedStaticBindings.TryRemove(type, out _); + } + + public static TypeId StaticTypeIdOf() + { + return GetSharedStaticBinding(typeof(T)).RequireTypedBinding().StaticTypeId; + } + + public static bool IsReferenceTrackableTypeOf() + { + return GetSharedStaticBinding(typeof(T)).RequireTypedBinding().IsReferenceTrackableType; + } + + public static IReadOnlyList CompatibleTypeMetaFieldsOf(bool trackRef) + { + return GetSharedStaticBinding(typeof(T)).RequireTypedBinding().CompatibleTypeMetaFields(trackRef); + } + + public Serializer GetSerializer() + { + Type type = typeof(T); + if (_typedBindings.TryGetValue(type, out object? cachedTypedBinding)) + { + return new Serializer((TypedSerializerBinding)cachedTypedBinding); + } + + TypedSerializerBinding typedBinding = GetBinding(type).RequireTypedBinding(); + object typedObject = _typedBindings.GetOrAdd(type, typedBinding); + return new Serializer((TypedSerializerBinding)typedObject); + } + + internal SerializerBinding GetBinding(Type type) + { + return _serializerBindings.GetOrAdd(type, CreateBindingCore); + } + + internal SerializerBinding RegisterCustom() + where TSerializer : IStaticSerializer + { + SerializerBinding serializerBinding = StaticSerializerBindingFactory.Create(); + RegisterCustom(typeof(T), serializerBinding); + return serializerBinding; + } + + internal void RegisterCustom(Type type, SerializerBinding serializerBinding) + { + _serializerBindings[type] = serializerBinding; + _typedBindings.TryRemove(type, out _); + } + internal void Register(Type type, uint id, SerializerBinding? explicitSerializer = null) { - SerializerBinding serializer = explicitSerializer ?? SerializerRegistry.GetBinding(type); + SerializerBinding serializer = explicitSerializer ?? GetBinding(type); RegisteredTypeInfo info = new( id, serializer.StaticTypeId, @@ -74,7 +137,7 @@ internal void Register(Type type, uint id, SerializerBinding? explicitSerializer internal void Register(Type type, string namespaceName, string typeName, SerializerBinding? explicitSerializer = null) { - SerializerBinding serializer = explicitSerializer ?? SerializerRegistry.GetBinding(type); + SerializerBinding serializer = explicitSerializer ?? GetBinding(type); MetaString namespaceMeta = MetaStringEncoder.Namespace.Encode(namespaceName, TypeMetaEncodings.NamespaceMetaStringEncodings); MetaString typeNameMeta = MetaStringEncoder.TypeName.Encode(typeName, TypeMetaEncodings.TypeNameMetaStringEncodings); RegisteredTypeInfo info = new( @@ -198,64 +261,64 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) switch (typeInfo.WireTypeId) { case TypeId.Bool: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int8: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int16: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int32: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.VarInt32: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int64: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.VarInt64: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.TaggedInt64: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.UInt8: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.UInt16: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.UInt32: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.VarUInt32: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.UInt64: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.VarUInt64: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.TaggedUInt64: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Float32: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Float64: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.String: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Binary: case TypeId.UInt8Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.BoolArray: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int8Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int16Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int32Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Int64Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.UInt16Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.UInt32Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.UInt64Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Float32Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Float64Array: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.List: return DynamicContainerCodec.ReadListPayload(ref context); case TypeId.Set: @@ -263,7 +326,7 @@ public DynamicTypeInfo ReadDynamicTypeInfo(ref ReadContext context) case TypeId.Map: return DynamicContainerCodec.ReadMapPayload(ref context); case TypeId.Union: - return SerializerRegistry.Get().Read(ref context, RefMode.None, false); + return GetSerializer().Read(ref context, RefMode.None, false); case TypeId.Struct: case TypeId.Enum: case TypeId.Ext: @@ -350,6 +413,195 @@ private DynamicRegistrationMode DynamicRegistrationModeFor(TypeId kind) throw new TypeNotRegisteredException($"no dynamic registration mode for kind {kind}"); } + private static SerializerBinding GetSharedStaticBinding(Type type) + { + return SharedStaticBindings.GetOrAdd(type, CreateBindingCore); + } + + private static SerializerBinding CreateBindingCore(Type type) + { + if (GeneratedFactories.TryGetValue(type, out Func? generatedFactory)) + { + return generatedFactory(); + } + + if (type == typeof(bool)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(sbyte)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(short)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(int)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(long)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(byte)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ushort)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(uint)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ulong)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(float)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(double)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(string)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(byte[])) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(DateOnly)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(DateTimeOffset)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(DateTime)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(TimeSpan)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ForyInt32Fixed)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ForyInt64Fixed)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ForyInt64Tagged)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ForyUInt32Fixed)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ForyUInt64Fixed)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(ForyUInt64Tagged)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (type == typeof(object)) + { + return StaticSerializerBindingFactory.Create(); + } + + if (typeof(Union).IsAssignableFrom(type)) + { + Type serializerType = typeof(UnionSerializer<>).MakeGenericType(type); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + + if (type.IsEnum) + { + Type serializerType = typeof(EnumSerializer<>).MakeGenericType(type); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + + if (type.IsArray) + { + Type elementType = type.GetElementType()!; + Type serializerType = typeof(ArraySerializer<>).MakeGenericType(elementType); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + Type[] genericArgs = type.GetGenericArguments(); + if (genericType == typeof(Nullable<>)) + { + Type serializerType = typeof(NullableSerializer<>).MakeGenericType(genericArgs[0]); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + + if (genericType == typeof(List<>)) + { + Type serializerType = typeof(ListSerializer<>).MakeGenericType(genericArgs[0]); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + + if (genericType == typeof(HashSet<>)) + { + Type serializerType = typeof(SetSerializer<>).MakeGenericType(genericArgs[0]); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + + if (genericType == typeof(Dictionary<,>)) + { + Type serializerType = typeof(DictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + + if (genericType == typeof(NullableKeyDictionary<,>)) + { + Type serializerType = typeof(NullableKeyDictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); + return StaticSerializerBindingFactory.Create(type, serializerType); + } + } + + throw new TypeNotRegisteredException($"No serializer available for {type}"); + } + private static MetaString ReadMetaString(ByteReader reader, MetaStringDecoder decoder, IReadOnlyList encodings) { byte header = reader.ReadUInt8(); diff --git a/csharp/src/Fory/TypedSerializerBinding.cs b/csharp/src/Fory/TypedSerializerBinding.cs index 48a1aad111..efe584dca5 100644 --- a/csharp/src/Fory/TypedSerializerBinding.cs +++ b/csharp/src/Fory/TypedSerializerBinding.cs @@ -16,7 +16,6 @@ // under the License. using System.Reflection; -using System.Threading; namespace Apache.Fory; @@ -412,35 +411,3 @@ private static SerializerBinding CreateGeneric() return Create(); } } - -internal static class TypedSerializerBindingCache -{ - private static readonly object Gate = new(); - private static TypedSerializerBinding? _binding; - private static int _bindingVersion = -1; - - public static Serializer Get() - { - int currentVersion = SerializerRegistry.Version; - TypedSerializerBinding? binding = Volatile.Read(ref _binding); - if (binding is not null && Volatile.Read(ref _bindingVersion) == currentVersion) - { - return new Serializer(binding); - } - - lock (Gate) - { - binding = _binding; - if (binding is not null && _bindingVersion == currentVersion) - { - return new Serializer(binding); - } - - SerializerBinding untyped = SerializerRegistry.GetBinding(typeof(T)); - binding = untyped.RequireTypedBinding(); - _binding = binding; - Volatile.Write(ref _bindingVersion, currentVersion); - return new Serializer(binding); - } - } -} diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index 38da28f0e9..f497249f7c 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -360,7 +360,7 @@ public void MacroFieldOrderFollowsForyRules() long second = reader.ReadVarInt64(); int third = reader.ReadVarInt32(); ReadContext tailContext = new(reader, new TypeResolver(), false, false); - string fourth = SerializerRegistry.Get().ReadData(ref tailContext); + string fourth = tailContext.TypeResolver.GetSerializer().ReadData(ref tailContext); Assert.Equal(value.B, first); Assert.Equal(value.A, second); From adef8d767737886fac7989612b713800812624fa Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 22 Feb 2026 13:00:55 +0800 Subject: [PATCH 12/16] refactor(csharp): migrate serialization to virtual dispatch --- .../src/Fory.Generator/ForyObjectGenerator.cs | 161 +++- csharp/src/Fory/AnySerializer.cs | 26 +- csharp/src/Fory/CollectionSerializers.cs | 48 +- csharp/src/Fory/DictionarySerializers.cs | 16 +- csharp/src/Fory/EnumSerializer.cs | 10 +- csharp/src/Fory/Fory.cs | 4 +- csharp/src/Fory/NullableKeyDictionary.cs | 16 +- csharp/src/Fory/OptionalSerializer.cs | 28 +- .../Fory/PrimitiveCollectionSerializers.cs | 719 ++++++++++++++++++ csharp/src/Fory/PrimitiveSerializers.cs | 316 ++++---- csharp/src/Fory/Serializer.cs | 176 +---- csharp/src/Fory/StringSerializer.cs | 87 +++ csharp/src/Fory/TypeInfo.cs | 43 ++ csharp/src/Fory/TypeResolver.cs | 260 +++++-- csharp/src/Fory/TypedSerializerBinding.cs | 448 ++++------- csharp/src/Fory/UnionSerializer.cs | 16 +- csharp/tests/Fory.XlangPeer/Program.cs | 16 +- 17 files changed, 1631 insertions(+), 759 deletions(-) create mode 100644 csharp/src/Fory/PrimitiveCollectionSerializers.cs create mode 100644 csharp/src/Fory/StringSerializer.cs create mode 100644 csharp/src/Fory/TypeInfo.cs diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index ca354622ec..bbda5d6c71 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -135,7 +135,7 @@ private static void Emit(SourceProductionContext context, ImmutableArray"); + sb.AppendLine($"file sealed class {model.SerializerName} : global::Apache.Fory.TypedSerializer<{model.TypeName}>"); sb.AppendLine("{"); sb.AppendLine(" private static global::Apache.Fory.RefMode __ForyRefMode(bool nullable, bool trackRef)"); sb.AppendLine(" {"); @@ -169,21 +169,21 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(");"); sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" public static global::Apache.Fory.TypeId StaticTypeId => global::Apache.Fory.TypeId.Struct;"); + sb.AppendLine(" public override global::Apache.Fory.TypeId StaticTypeId => global::Apache.Fory.TypeId.Struct;"); if (model.Kind == DeclKind.Class) { - sb.AppendLine(" public static bool IsNullableType => true;"); - sb.AppendLine(" public static bool IsReferenceTrackableType => true;"); - sb.AppendLine($" public static {model.TypeName} DefaultValue => null!;"); - sb.AppendLine($" public static bool IsNone(in {model.TypeName} value) => value is null;"); + sb.AppendLine(" public override bool IsNullableType => true;"); + sb.AppendLine(" public override bool IsReferenceTrackableType => true;"); + sb.AppendLine($" public override {model.TypeName} DefaultValue => null!;"); + sb.AppendLine($" public override bool IsNone(in {model.TypeName} value) => value is null;"); } else { - sb.AppendLine($" public static {model.TypeName} DefaultValue => new {model.TypeName}();"); + sb.AppendLine($" public override {model.TypeName} DefaultValue => new {model.TypeName}();"); } sb.AppendLine(); - sb.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList CompatibleTypeMetaFields(bool trackRef)"); + sb.AppendLine(" public override global::System.Collections.Generic.IReadOnlyList CompatibleTypeMetaFields(bool trackRef)"); sb.AppendLine(" {"); if (model.SortedMembers.Length == 0) { @@ -205,7 +205,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine( - $" public static void WriteData(ref global::Apache.Fory.WriteContext context, in {model.TypeName} value, bool hasGenerics)"); + $" public override void WriteData(ref global::Apache.Fory.WriteContext context, in {model.TypeName} value, bool hasGenerics)"); sb.AppendLine(" {"); sb.AppendLine(" _ = hasGenerics;"); sb.AppendLine(" if (context.Compatible)"); @@ -234,7 +234,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine($" public static {model.TypeName} ReadData(ref global::Apache.Fory.ReadContext context)"); + sb.AppendLine($" public override {model.TypeName} ReadData(ref global::Apache.Fory.ReadContext context)"); sb.AppendLine(" {"); sb.AppendLine(" if (context.Compatible)"); sb.AppendLine(" {"); @@ -255,7 +255,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) { sb.AppendLine($" case \"{EscapeString(member.FieldIdentifier)}\":"); sb.AppendLine(" {"); - EmitReadMemberAssignment(sb, member, "remoteRefMode", "remoteReadTypeInfo", "value", "Compat", 7); + EmitReadMemberAssignment(sb, member, "remoteRefMode", "remoteReadTypeInfo", "value", "Compat", 7, false); sb.AppendLine(" break;"); sb.AppendLine(" }"); } @@ -283,7 +283,7 @@ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model) foreach (MemberModel member in model.SortedMembers) { - EmitReadMemberAssignment(sb, member, BuildWriteRefModeExpression(member), "false", "valueSchema", "Schema", 2); + EmitReadMemberAssignment(sb, member, BuildWriteRefModeExpression(member), "false", "valueSchema", "Schema", 2, true); } sb.AppendLine(" return valueSchema;"); @@ -297,7 +297,7 @@ private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool c string memberAccess = $"value.{member.Name}"; string hasGenerics = member.IsCollection ? "true" : "false"; string writeTypeInfo = compatibleMode - ? $"__ForyNeedsTypeInfoForField(context.TypeResolver.GetSerializer<{member.TypeName}>().StaticTypeId)" + ? $"__ForyNeedsTypeInfoForField(context.TypeResolver.GetTypeInfo<{member.TypeName}>().StaticTypeId)" : "false"; switch (member.DynamicAnyKind) @@ -333,6 +333,12 @@ private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool c return; } + if (!member.IsNullable && TryBuildDirectFieldWrite(member, memberAccess, out string? directWriteCode)) + { + sb.AppendLine($" {directWriteCode}"); + return; + } + sb.AppendLine( $" context.TypeResolver.GetSerializer<{member.TypeName}>().Write(ref context, {memberAccess}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});"); } @@ -344,7 +350,8 @@ private static void EmitReadMemberAssignment( string readTypeInfoExpr, string valueVar, string variableSuffix, - int indentLevel) + int indentLevel, + bool allowDirectRead) { string indent = new(' ', indentLevel * 2); string assignmentTarget = $"{valueVar}.{member.Name}"; @@ -380,6 +387,12 @@ private static void EmitReadMemberAssignment( return; } + if (allowDirectRead && !member.IsNullable && TryBuildDirectFieldRead(member, out string? directReadExpr)) + { + sb.AppendLine($"{indent}{assignmentTarget} = {directReadExpr};"); + return; + } + sb.AppendLine( $"{indent}{assignmentTarget} = context.TypeResolver.GetSerializer<{member.TypeName}>().Read(ref context, {refModeExpr}, {readTypeInfoExpr});"); } @@ -389,6 +402,122 @@ private static string StripNullableForTypeOf(string typeName) return typeName.Replace("?", string.Empty); } + private static bool TryBuildDirectFieldWrite(MemberModel member, string memberAccess, out string? writeCode) + { + writeCode = null; + if (!CanUseDirectBuiltInFieldAccess(member)) + { + return false; + } + + switch (member.Classification.TypeId) + { + case 1: + writeCode = $"context.Writer.WriteUInt8({memberAccess} ? (byte)1 : (byte)0);"; + return true; + case 2: + writeCode = $"context.Writer.WriteInt8({memberAccess});"; + return true; + case 3: + writeCode = $"context.Writer.WriteInt16({memberAccess});"; + return true; + case 5: + writeCode = $"context.Writer.WriteVarInt32({memberAccess});"; + return true; + case 7: + writeCode = $"context.Writer.WriteVarInt64({memberAccess});"; + return true; + case 9: + writeCode = $"context.Writer.WriteUInt8({memberAccess});"; + return true; + case 10: + writeCode = $"context.Writer.WriteUInt16({memberAccess});"; + return true; + case 12: + writeCode = $"context.Writer.WriteVarUInt32({memberAccess});"; + return true; + case 14: + writeCode = $"context.Writer.WriteVarUInt64({memberAccess});"; + return true; + case 19: + writeCode = $"context.Writer.WriteFloat32({memberAccess});"; + return true; + case 20: + writeCode = $"context.Writer.WriteFloat64({memberAccess});"; + return true; + case 21: + writeCode = $"global::Apache.Fory.StringSerializer.WriteString(ref context, {memberAccess});"; + return true; + default: + return false; + } + } + + private static bool TryBuildDirectFieldRead(MemberModel member, out string? readExpr) + { + readExpr = null; + if (!CanUseDirectBuiltInFieldAccess(member)) + { + return false; + } + + switch (member.Classification.TypeId) + { + case 1: + readExpr = "context.Reader.ReadUInt8() != 0"; + return true; + case 2: + readExpr = "context.Reader.ReadInt8()"; + return true; + case 3: + readExpr = "context.Reader.ReadInt16()"; + return true; + case 5: + readExpr = "context.Reader.ReadVarInt32()"; + return true; + case 7: + readExpr = "context.Reader.ReadVarInt64()"; + return true; + case 9: + readExpr = "context.Reader.ReadUInt8()"; + return true; + case 10: + readExpr = "context.Reader.ReadUInt16()"; + return true; + case 12: + readExpr = "context.Reader.ReadVarUInt32()"; + return true; + case 14: + readExpr = "context.Reader.ReadVarUInt64()"; + return true; + case 19: + readExpr = "context.Reader.ReadFloat32()"; + return true; + case 20: + readExpr = "context.Reader.ReadFloat64()"; + return true; + case 21: + readExpr = "global::Apache.Fory.StringSerializer.ReadString(ref context)"; + return true; + default: + return false; + } + } + + private static bool CanUseDirectBuiltInFieldAccess(MemberModel member) + { + if (member.IsNullable || + member.CustomWrapperTypeName is not null || + member.DynamicAnyKind != DynamicAnyKind.None || + member.IsCollection || + member.Classification.IsMap) + { + return false; + } + + return member.Classification.IsPrimitive || member.Classification.TypeId == 21; + } + private static string BuildSchemaFingerprintExpression(ImmutableArray members) { if (members.IsDefaultOrEmpty) @@ -412,7 +541,7 @@ private static string BuildSchemaFingerprintExpression(ImmutableArray "(trackRef ? 1 : 0)", _ => member.Classification.IsBuiltIn ? "0" - : $"((trackRef && typeResolver.GetSerializer<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)", + : $"((trackRef && typeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)", }; string nullable = member.IsNullable ? "1" : "0"; string piece = $"\"{EscapeString(member.FieldIdentifier)},{fingerprintTypeId},\" + {trackRefExpr} + \",{nullable};\""; @@ -450,7 +579,7 @@ private static string BuildWriteRefModeExpression(MemberModel member) DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)", _ => member.Classification.IsBuiltIn ? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)" - : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && context.TypeResolver.GetSerializer<{member.TypeName}>().IsReferenceTrackableType)", + : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && context.TypeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType)", }; } diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index d5e7144542..e8940f1f83 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -17,15 +17,15 @@ namespace Apache.Fory; -public readonly struct DynamicAnyObjectSerializer : IStaticSerializer +public sealed class DynamicAnyObjectSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Unknown; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static object? DefaultValue => null; - public static bool IsNone(in object? value) => value is null; + public override TypeId StaticTypeId => TypeId.Unknown; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override object? DefaultValue => null; + public override bool IsNone(in object? value) => value is null; - public static void WriteData(ref WriteContext context, in object? value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in object? value, bool hasGenerics) { if (IsNone(value)) { @@ -35,7 +35,7 @@ public static void WriteData(ref WriteContext context, in object? value, bool ha DynamicAnyCodec.WriteAnyPayload(value!, ref context, hasGenerics); } - public static object? ReadData(ref ReadContext context) + public override object? ReadData(ref ReadContext context) { DynamicTypeInfo? dynamicTypeInfo = context.DynamicTypeInfo(typeof(object)); if (dynamicTypeInfo is null) @@ -46,18 +46,18 @@ public static void WriteData(ref WriteContext context, in object? value, bool ha return context.TypeResolver.ReadDynamicValue(dynamicTypeInfo, ref context); } - public static void WriteTypeInfo(ref WriteContext context) + public override void WriteTypeInfo(ref WriteContext context) { throw new InvalidDataException("dynamic Any value type info is runtime-only"); } - public static void ReadTypeInfo(ref ReadContext context) + public override void ReadTypeInfo(ref ReadContext context) { DynamicTypeInfo typeInfo = context.TypeResolver.ReadDynamicTypeInfo(ref context); context.SetDynamicTypeInfo(typeof(object), typeInfo); } - public static void Write(ref WriteContext context, in object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public override void Write(ref WriteContext context, in object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { if (refMode != RefMode.None) { @@ -92,7 +92,7 @@ public static void Write(ref WriteContext context, in object? value, RefMode ref WriteData(ref context, value, hasGenerics); } - public static object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + public override object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { if (refMode != RefMode.None) { @@ -211,6 +211,6 @@ public static void WriteAnyPayload(object value, ref WriteContext context, bool } SerializerBinding serializer = context.TypeResolver.GetBinding(value.GetType()); - serializer.WriteData(ref context, value, hasGenerics); + serializer.WriteDataObject(ref context, value, hasGenerics); } } diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index 67396f6c2f..8a51540f09 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -736,17 +736,17 @@ public static T[] ReadPrimitiveArray(ref ReadContext context) } } -public readonly struct ArraySerializer : IStaticSerializer, T[]> +public sealed class ArraySerializer : TypedSerializer { private static readonly TypeId? PrimitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); - public static TypeId StaticTypeId => PrimitiveArrayTypeId ?? TypeId.List; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static T[] DefaultValue => null!; - public static bool IsNone(in T[] value) => value is null; + public override TypeId StaticTypeId => PrimitiveArrayTypeId ?? TypeId.List; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override T[] DefaultValue => null!; + public override bool IsNone(in T[] value) => value is null; - public static void WriteData(ref WriteContext context, in T[] value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in T[] value, bool hasGenerics) { T[] safe = value ?? []; if (PrimitiveArrayTypeId is not null) @@ -762,7 +762,7 @@ public static void WriteData(ref WriteContext context, in T[] value, bool hasGen hasGenerics); } - public static T[] ReadData(ref ReadContext context) + public override T[] ReadData(ref ReadContext context) { if (PrimitiveArrayTypeId is not null) { @@ -774,41 +774,41 @@ public static T[] ReadData(ref ReadContext context) } } -public readonly struct ListSerializer : IStaticSerializer, List> +public class ListSerializer : TypedSerializer> { - public static TypeId StaticTypeId => TypeId.List; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static List DefaultValue => null!; - public static bool IsNone(in List value) => value is null; + public override TypeId StaticTypeId => TypeId.List; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override List DefaultValue => null!; + public override bool IsNone(in List value) => value is null; - public static void WriteData(ref WriteContext context, in List value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) { List safe = value ?? []; CollectionCodec.WriteCollectionData(safe, context.TypeResolver.GetSerializer(), ref context, hasGenerics); } - public static List ReadData(ref ReadContext context) + public override List ReadData(ref ReadContext context) { return CollectionCodec.ReadCollectionData(context.TypeResolver.GetSerializer(), ref context); } } -public readonly struct SetSerializer : IStaticSerializer, HashSet> where T : notnull +public sealed class SetSerializer : TypedSerializer> where T : notnull { - public static TypeId StaticTypeId => TypeId.Set; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static HashSet DefaultValue => null!; - public static bool IsNone(in HashSet value) => value is null; + public override TypeId StaticTypeId => TypeId.Set; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override HashSet DefaultValue => null!; + public override bool IsNone(in HashSet value) => value is null; - public static void WriteData(ref WriteContext context, in HashSet value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in HashSet value, bool hasGenerics) { List list = value is null ? [] : [.. value]; context.TypeResolver.GetSerializer>().WriteData(ref context, list, hasGenerics); } - public static HashSet ReadData(ref ReadContext context) + public override HashSet ReadData(ref ReadContext context) { return [.. context.TypeResolver.GetSerializer>().ReadData(ref context)]; } diff --git a/csharp/src/Fory/DictionarySerializers.cs b/csharp/src/Fory/DictionarySerializers.cs index fa421d69b0..e2df3943f4 100644 --- a/csharp/src/Fory/DictionarySerializers.cs +++ b/csharp/src/Fory/DictionarySerializers.cs @@ -27,16 +27,16 @@ internal static class DictionaryBits public const byte DeclaredValueType = 0b0010_0000; } -public readonly struct DictionarySerializer : IStaticSerializer, Dictionary> +public class DictionarySerializer : TypedSerializer> where TKey : notnull { - public static TypeId StaticTypeId => TypeId.Map; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static Dictionary DefaultValue => null!; - public static bool IsNone(in Dictionary value) => value is null; + public override TypeId StaticTypeId => TypeId.Map; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override Dictionary DefaultValue => null!; + public override bool IsNone(in Dictionary value) => value is null; - public static void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) { Serializer keySerializer = context.TypeResolver.GetSerializer(); Serializer valueSerializer = context.TypeResolver.GetSerializer(); @@ -189,7 +189,7 @@ public static void WriteData(ref WriteContext context, in Dictionary ReadData(ref ReadContext context) + public override Dictionary ReadData(ref ReadContext context) { Serializer keySerializer = context.TypeResolver.GetSerializer(); Serializer valueSerializer = context.TypeResolver.GetSerializer(); diff --git a/csharp/src/Fory/EnumSerializer.cs b/csharp/src/Fory/EnumSerializer.cs index 111e5fb46c..fadb419306 100644 --- a/csharp/src/Fory/EnumSerializer.cs +++ b/csharp/src/Fory/EnumSerializer.cs @@ -17,19 +17,19 @@ namespace Apache.Fory; -public readonly struct EnumSerializer : IStaticSerializer, TEnum> where TEnum : struct, Enum +public sealed class EnumSerializer : TypedSerializer where TEnum : struct, Enum { - public static TypeId StaticTypeId => TypeId.Enum; - public static TEnum DefaultValue => default; + public override TypeId StaticTypeId => TypeId.Enum; + public override TEnum DefaultValue => default; - public static void WriteData(ref WriteContext context, in TEnum value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in TEnum value, bool hasGenerics) { _ = hasGenerics; uint ordinal = Convert.ToUInt32(value); context.Writer.WriteVarUInt32(ordinal); } - public static TEnum ReadData(ref ReadContext context) + public override TEnum ReadData(ref ReadContext context) { uint ordinal = context.Reader.ReadVarUInt32(); TEnum value = (TEnum)Enum.ToObject(typeof(TEnum), ordinal); diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs index 6765b5f942..f458c5c949 100644 --- a/csharp/src/Fory/Fory.cs +++ b/csharp/src/Fory/Fory.cs @@ -55,7 +55,7 @@ public Fory Register(string typeNamespace, string typeName) } public Fory Register(uint typeId) - where TSerializer : IStaticSerializer + where TSerializer : TypedSerializer, new() { SerializerBinding serializerBinding = _typeResolver.RegisterCustom(); _typeResolver.Register(typeof(T), typeId, serializerBinding); @@ -63,7 +63,7 @@ public Fory Register(uint typeId) } public Fory Register(string typeNamespace, string typeName) - where TSerializer : IStaticSerializer + where TSerializer : TypedSerializer, new() { SerializerBinding serializerBinding = _typeResolver.RegisterCustom(); _typeResolver.Register(typeof(T), typeNamespace, typeName, serializerBinding); diff --git a/csharp/src/Fory/NullableKeyDictionary.cs b/csharp/src/Fory/NullableKeyDictionary.cs index 4a8b879f55..fe54ab4d33 100644 --- a/csharp/src/Fory/NullableKeyDictionary.cs +++ b/csharp/src/Fory/NullableKeyDictionary.cs @@ -388,15 +388,15 @@ IEnumerator IEnumerable.GetEnumerator() } } -public readonly struct NullableKeyDictionarySerializer : IStaticSerializer, NullableKeyDictionary> +public sealed class NullableKeyDictionarySerializer : TypedSerializer> { - public static TypeId StaticTypeId => TypeId.Map; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static NullableKeyDictionary DefaultValue => null!; - public static bool IsNone(in NullableKeyDictionary value) => value is null; + public override TypeId StaticTypeId => TypeId.Map; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override NullableKeyDictionary DefaultValue => null!; + public override bool IsNone(in NullableKeyDictionary value) => value is null; - public static void WriteData(ref WriteContext context, in NullableKeyDictionary value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in NullableKeyDictionary value, bool hasGenerics) { Serializer keySerializer = context.TypeResolver.GetSerializer(); Serializer valueSerializer = context.TypeResolver.GetSerializer(); @@ -528,7 +528,7 @@ public static void WriteData(ref WriteContext context, in NullableKeyDictionary< } } - public static NullableKeyDictionary ReadData(ref ReadContext context) + public override NullableKeyDictionary ReadData(ref ReadContext context) { Serializer keySerializer = context.TypeResolver.GetSerializer(); Serializer valueSerializer = context.TypeResolver.GetSerializer(); diff --git a/csharp/src/Fory/OptionalSerializer.cs b/csharp/src/Fory/OptionalSerializer.cs index ad650c59f1..dfc60fb62a 100644 --- a/csharp/src/Fory/OptionalSerializer.cs +++ b/csharp/src/Fory/OptionalSerializer.cs @@ -17,22 +17,22 @@ namespace Apache.Fory; -public readonly struct NullableSerializer : IStaticSerializer, T?> where T : struct +public sealed class NullableSerializer : TypedSerializer where T : struct { - public static TypeId StaticTypeId => TypeResolver.StaticTypeIdOf(); + public override TypeId StaticTypeId => TypeResolver.StaticTypeIdOf(); - public static bool IsNullableType => true; + public override bool IsNullableType => true; - public static bool IsReferenceTrackableType => TypeResolver.IsReferenceTrackableTypeOf(); + public override bool IsReferenceTrackableType => TypeResolver.IsReferenceTrackableTypeOf(); - public static T? DefaultValue => null; + public override T? DefaultValue => null; - public static bool IsNone(in T? value) + public override bool IsNone(in T? value) { return !value.HasValue; } - public static void WriteData(ref WriteContext context, in T? value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in T? value, bool hasGenerics) { if (!value.HasValue) { @@ -44,30 +44,30 @@ public static void WriteData(ref WriteContext context, in T? value, bool hasGene wrappedSerializer.WriteData(ref context, wrapped, hasGenerics); } - public static T? ReadData(ref ReadContext context) + public override T? ReadData(ref ReadContext context) { Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); return wrappedSerializer.ReadData(ref context); } - public static void WriteTypeInfo(ref WriteContext context) + public override void WriteTypeInfo(ref WriteContext context) { Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); wrappedSerializer.WriteTypeInfo(ref context); } - public static void ReadTypeInfo(ref ReadContext context) + public override void ReadTypeInfo(ref ReadContext context) { Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); wrappedSerializer.ReadTypeInfo(ref context); } - public static IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + public override IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { return TypeResolver.CompatibleTypeMetaFieldsOf(trackRef); } - public static void Write(ref WriteContext context, in T? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public override void Write(ref WriteContext context, in T? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); switch (refMode) @@ -100,10 +100,12 @@ public static void Write(ref WriteContext context, in T? value, RefMode refMode, wrappedSerializer.Write(ref context, value.Value, RefMode.Tracking, writeTypeInfo, hasGenerics); break; + default: + throw new InvalidDataException($"unsupported ref mode {refMode}"); } } - public static T? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + public override T? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { Serializer wrappedSerializer = context.TypeResolver.GetSerializer(); switch (refMode) diff --git a/csharp/src/Fory/PrimitiveCollectionSerializers.cs b/csharp/src/Fory/PrimitiveCollectionSerializers.cs new file mode 100644 index 0000000000..9d5638263f --- /dev/null +++ b/csharp/src/Fory/PrimitiveCollectionSerializers.cs @@ -0,0 +1,719 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +internal static class PrimitiveCollectionHeader +{ + public static void WriteListHeader(ref WriteContext context, int count, bool hasGenerics, TypeId elementTypeId, bool hasNull) + { + context.Writer.WriteVarUInt32((uint)count); + if (count == 0) + { + return; + } + + bool declared = hasGenerics && !elementTypeId.NeedsTypeInfoForField(); + byte header = CollectionBits.SameType; + if (hasNull) + { + header |= CollectionBits.HasNull; + } + + if (declared) + { + header |= CollectionBits.DeclaredElementType; + } + + context.Writer.WriteUInt8(header); + if (!declared) + { + context.Writer.WriteUInt8((byte)elementTypeId); + } + } + + public static void WriteMapChunkTypeInfo( + ref WriteContext context, + bool keyDeclared, + bool valueDeclared, + TypeId keyTypeId, + TypeId valueTypeId) + { + if (!keyDeclared) + { + context.Writer.WriteUInt8((byte)keyTypeId); + } + + if (!valueDeclared) + { + context.Writer.WriteUInt8((byte)valueTypeId); + } + } +} + +internal sealed class ListBoolSerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.Bool, false); + for (int i = 0; i < list.Count; i++) + { + context.Writer.WriteUInt8(list[i] ? (byte)1 : (byte)0); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class ListIntSerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.VarInt32, false); + for (int i = 0; i < list.Count; i++) + { + context.Writer.WriteVarInt32(list[i]); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class ListLongSerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.VarInt64, false); + for (int i = 0; i < list.Count; i++) + { + context.Writer.WriteVarInt64(list[i]); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class ListStringSerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + bool hasNull = false; + for (int i = 0; i < list.Count; i++) + { + if (list[i] is null) + { + hasNull = true; + break; + } + } + + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.String, hasNull); + if (hasNull) + { + for (int i = 0; i < list.Count; i++) + { + string? item = list[i]; + if (item is null) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + continue; + } + + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); + StringSerializer.WriteString(ref context, item); + } + + return; + } + + for (int i = 0; i < list.Count; i++) + { + StringSerializer.WriteString(ref context, list[i]); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class ListDateOnlySerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + private static readonly DateOnly Epoch = new(1970, 1, 1); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.Date, false); + for (int i = 0; i < list.Count; i++) + { + context.Writer.WriteInt32(list[i].DayNumber - Epoch.DayNumber); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class ListDateTimeOffsetSerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.Timestamp, false); + for (int i = 0; i < list.Count; i++) + { + ForyTimestamp ts = ForyTimestamp.FromDateTimeOffset(list[i]); + context.Writer.WriteInt64(ts.Seconds); + context.Writer.WriteUInt32(ts.Nanos); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class ListDateTimeSerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.Timestamp, false); + for (int i = 0; i < list.Count; i++) + { + DateTimeOffset dto = list[i].Kind switch + { + DateTimeKind.Utc => new DateTimeOffset(list[i], TimeSpan.Zero), + DateTimeKind.Local => list[i], + _ => new DateTimeOffset(DateTime.SpecifyKind(list[i], DateTimeKind.Utc)), + }; + ForyTimestamp ts = ForyTimestamp.FromDateTimeOffset(dto); + context.Writer.WriteInt64(ts.Seconds); + context.Writer.WriteUInt32(ts.Nanos); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class ListTimeSpanSerializer : TypedSerializer> +{ + private static readonly ListSerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.List; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override List DefaultValue => null!; + + public override bool IsNone(in List value) => value is null; + + public override void WriteData(ref WriteContext context, in List value, bool hasGenerics) + { + List list = value ?? []; + PrimitiveCollectionHeader.WriteListHeader(ref context, list.Count, hasGenerics, TypeId.Duration, false); + for (int i = 0; i < list.Count; i++) + { + long seconds = list[i].Ticks / TimeSpan.TicksPerSecond; + int nanos = checked((int)((list[i].Ticks % TimeSpan.TicksPerSecond) * 100)); + context.Writer.WriteInt64(seconds); + context.Writer.WriteInt32(nanos); + } + } + + public override List ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryStringStringSerializer : TypedSerializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + if (ContainsNull(map)) + { + Fallback.WriteData(ref context, map, hasGenerics); + return; + } + + WriteMapStringString(ref context, map, hasGenerics); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } + + private static bool ContainsNull(Dictionary map) + { + foreach (KeyValuePair pair in map) + { + if (pair.Key is null || pair.Value is null) + { + return true; + } + } + + return false; + } + + private static void WriteMapStringString(ref WriteContext context, Dictionary map, bool hasGenerics) + { + KeyValuePair[] pairs = [.. map]; + context.Writer.WriteVarUInt32((uint)pairs.Length); + if (pairs.Length == 0) + { + return; + } + + bool keyDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); + int index = 0; + while (index < pairs.Length) + { + int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); + byte header = 0; + if (keyDeclared) + { + header |= DictionaryBits.DeclaredKeyType; + } + + if (valueDeclared) + { + header |= DictionaryBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + context.Writer.WriteUInt8((byte)chunkSize); + PrimitiveCollectionHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, TypeId.String, TypeId.String); + for (int i = 0; i < chunkSize; i++) + { + KeyValuePair pair = pairs[index + i]; + StringSerializer.WriteString(ref context, pair.Key); + StringSerializer.WriteString(ref context, pair.Value); + } + + index += chunkSize; + } + } +} + +internal sealed class DictionaryStringIntSerializer : TypedSerializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.VarInt32, + static (writer, valueItem) => writer.WriteVarInt32(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryStringLongSerializer : TypedSerializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.VarInt64, + static (writer, valueItem) => writer.WriteVarInt64(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryStringBoolSerializer : TypedSerializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.Bool, + static (writer, valueItem) => writer.WriteUInt8(valueItem ? (byte)1 : (byte)0)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryStringDoubleSerializer : TypedSerializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.Float64, + static (writer, valueItem) => writer.WriteFloat64(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryIntIntSerializer : TypedSerializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMap( + ref context, + map, + hasGenerics, + TypeId.VarInt32, + static (writer, key) => writer.WriteVarInt32(key), + TypeId.VarInt32, + static (writer, valueItem) => writer.WriteVarInt32(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryLongLongSerializer : TypedSerializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMap( + ref context, + map, + hasGenerics, + TypeId.VarInt64, + static (writer, key) => writer.WriteVarInt64(key), + TypeId.VarInt64, + static (writer, valueItem) => writer.WriteVarInt64(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal static class PrimitiveDictionaryWriter +{ + public static void WriteMapStringValue( + ref WriteContext context, + Dictionary map, + bool hasGenerics, + TypeId valueTypeId, + Action writeValue) + { + KeyValuePair[] pairs = [.. map]; + context.Writer.WriteVarUInt32((uint)pairs.Length); + if (pairs.Length == 0) + { + return; + } + + bool keyDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !valueTypeId.NeedsTypeInfoForField(); + int index = 0; + while (index < pairs.Length) + { + int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); + byte header = 0; + if (keyDeclared) + { + header |= DictionaryBits.DeclaredKeyType; + } + + if (valueDeclared) + { + header |= DictionaryBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + context.Writer.WriteUInt8((byte)chunkSize); + PrimitiveCollectionHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, TypeId.String, valueTypeId); + for (int i = 0; i < chunkSize; i++) + { + KeyValuePair pair = pairs[index + i]; + StringSerializer.WriteString(ref context, pair.Key); + writeValue(context.Writer, pair.Value); + } + + index += chunkSize; + } + } + + public static void WriteMap( + ref WriteContext context, + Dictionary map, + bool hasGenerics, + TypeId keyTypeId, + Action writeKey, + TypeId valueTypeId, + Action writeValue) + where TKey : notnull + { + KeyValuePair[] pairs = [.. map]; + context.Writer.WriteVarUInt32((uint)pairs.Length); + if (pairs.Length == 0) + { + return; + } + + bool keyDeclared = hasGenerics && !keyTypeId.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !valueTypeId.NeedsTypeInfoForField(); + int index = 0; + while (index < pairs.Length) + { + int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); + byte header = 0; + if (keyDeclared) + { + header |= DictionaryBits.DeclaredKeyType; + } + + if (valueDeclared) + { + header |= DictionaryBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + context.Writer.WriteUInt8((byte)chunkSize); + PrimitiveCollectionHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, keyTypeId, valueTypeId); + for (int i = 0; i < chunkSize; i++) + { + KeyValuePair pair = pairs[index + i]; + writeKey(context.Writer, pair.Key); + writeValue(context.Writer, pair.Value); + } + + index += chunkSize; + } + } +} diff --git a/csharp/src/Fory/PrimitiveSerializers.cs b/csharp/src/Fory/PrimitiveSerializers.cs index a1c732b1dd..f5f26478c0 100644 --- a/csharp/src/Fory/PrimitiveSerializers.cs +++ b/csharp/src/Fory/PrimitiveSerializers.cs @@ -15,8 +15,6 @@ // specific language governing permissions and limitations // under the License. -using System.Text; - namespace Apache.Fory; internal enum ForyStringEncoding : ulong @@ -68,244 +66,215 @@ private static ForyTimestamp Normalize(long seconds, long nanos) } } -public readonly struct BoolSerializer : IStaticSerializer +public sealed class BoolSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Bool; - public static bool DefaultValue => false; - public static void WriteData(ref WriteContext context, in bool value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Bool; + + public override bool DefaultValue => false; + + public override void WriteData(ref WriteContext context, in bool value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt8(value ? (byte)1 : (byte)0); } - public static bool ReadData(ref ReadContext context) + public override bool ReadData(ref ReadContext context) { return context.Reader.ReadUInt8() != 0; } } -public readonly struct Int8Serializer : IStaticSerializer +public sealed class Int8Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Int8; - public static sbyte DefaultValue => 0; - public static void WriteData(ref WriteContext context, in sbyte value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Int8; + + public override sbyte DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in sbyte value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt8(value); } - public static sbyte ReadData(ref ReadContext context) + public override sbyte ReadData(ref ReadContext context) { return context.Reader.ReadInt8(); } } -public readonly struct Int16Serializer : IStaticSerializer +public sealed class Int16Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Int16; - public static short DefaultValue => 0; - public static void WriteData(ref WriteContext context, in short value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Int16; + + public override short DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in short value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt16(value); } - public static short ReadData(ref ReadContext context) + public override short ReadData(ref ReadContext context) { return context.Reader.ReadInt16(); } } -public readonly struct Int32Serializer : IStaticSerializer +public sealed class Int32Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.VarInt32; - public static int DefaultValue => 0; - public static void WriteData(ref WriteContext context, in int value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.VarInt32; + + public override int DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in int value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarInt32(value); } - public static int ReadData(ref ReadContext context) + public override int ReadData(ref ReadContext context) { return context.Reader.ReadVarInt32(); } } -public readonly struct Int64Serializer : IStaticSerializer +public sealed class Int64Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.VarInt64; - public static long DefaultValue => 0; - public static void WriteData(ref WriteContext context, in long value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.VarInt64; + + public override long DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in long value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarInt64(value); } - public static long ReadData(ref ReadContext context) + public override long ReadData(ref ReadContext context) { return context.Reader.ReadVarInt64(); } } -public readonly struct UInt8Serializer : IStaticSerializer +public sealed class UInt8Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.UInt8; - public static byte DefaultValue => 0; - public static void WriteData(ref WriteContext context, in byte value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.UInt8; + + public override byte DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in byte value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt8(value); } - public static byte ReadData(ref ReadContext context) + public override byte ReadData(ref ReadContext context) { return context.Reader.ReadUInt8(); } } -public readonly struct UInt16Serializer : IStaticSerializer +public sealed class UInt16Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.UInt16; - public static ushort DefaultValue => 0; - public static void WriteData(ref WriteContext context, in ushort value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.UInt16; + + public override ushort DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in ushort value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt16(value); } - public static ushort ReadData(ref ReadContext context) + public override ushort ReadData(ref ReadContext context) { return context.Reader.ReadUInt16(); } } -public readonly struct UInt32Serializer : IStaticSerializer +public sealed class UInt32Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.VarUInt32; - public static uint DefaultValue => 0; - public static void WriteData(ref WriteContext context, in uint value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.VarUInt32; + + public override uint DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in uint value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarUInt32(value); } - public static uint ReadData(ref ReadContext context) + public override uint ReadData(ref ReadContext context) { return context.Reader.ReadVarUInt32(); } } -public readonly struct UInt64Serializer : IStaticSerializer +public sealed class UInt64Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.VarUInt64; - public static ulong DefaultValue => 0; - public static void WriteData(ref WriteContext context, in ulong value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.VarUInt64; + + public override ulong DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in ulong value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarUInt64(value); } - public static ulong ReadData(ref ReadContext context) + public override ulong ReadData(ref ReadContext context) { return context.Reader.ReadVarUInt64(); } } -public readonly struct Float32Serializer : IStaticSerializer +public sealed class Float32Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Float32; - public static float DefaultValue => 0; - public static void WriteData(ref WriteContext context, in float value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Float32; + + public override float DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in float value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteFloat32(value); } - public static float ReadData(ref ReadContext context) + public override float ReadData(ref ReadContext context) { return context.Reader.ReadFloat32(); } } -public readonly struct Float64Serializer : IStaticSerializer +public sealed class Float64Serializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Float64; - public static double DefaultValue => 0; - public static void WriteData(ref WriteContext context, in double value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Float64; + + public override double DefaultValue => 0; + + public override void WriteData(ref WriteContext context, in double value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteFloat64(value); } - public static double ReadData(ref ReadContext context) + public override double ReadData(ref ReadContext context) { return context.Reader.ReadFloat64(); } } -public readonly struct StringSerializer : IStaticSerializer +public sealed class BinarySerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.String; - public static bool IsNullableType => true; - public static string DefaultValue => null!; - public static bool IsNone(in string value) => value is null; + public override TypeId StaticTypeId => TypeId.Binary; - public static void WriteData(ref WriteContext context, in string value, bool hasGenerics) - { - _ = hasGenerics; - string safe = value ?? string.Empty; - byte[] utf8 = Encoding.UTF8.GetBytes(safe); - ulong header = ((ulong)utf8.Length << 2) | (ulong)ForyStringEncoding.Utf8; - context.Writer.WriteVarUInt36Small(header); - context.Writer.WriteBytes(utf8); - } + public override bool IsNullableType => true; - public static string ReadData(ref ReadContext context) - { - ulong header = context.Reader.ReadVarUInt36Small(); - ulong encoding = header & 0x03; - int byteLength = checked((int)(header >> 2)); - byte[] bytes = context.Reader.ReadBytes(byteLength); - return encoding switch - { - (ulong)ForyStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes), - (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes), - (ulong)ForyStringEncoding.Utf16 => DecodeUtf16(bytes), - _ => throw new EncodingException($"unsupported string encoding {encoding}"), - }; - } + public override byte[] DefaultValue => null!; - private static string DecodeLatin1(byte[] bytes) - { - return string.Create(bytes.Length, bytes, static (span, b) => - { - for (int i = 0; i < b.Length; i++) - { - span[i] = (char)b[i]; - } - }); - } + public override bool IsNone(in byte[] value) => value is null; - private static string DecodeUtf16(byte[] bytes) - { - if ((bytes.Length & 1) != 0) - { - throw new EncodingException("utf16 byte length is not even"); - } - - return Encoding.Unicode.GetString(bytes); - } -} - -public readonly struct BinarySerializer : IStaticSerializer -{ - public static TypeId StaticTypeId => TypeId.Binary; - public static bool IsNullableType => true; - public static byte[] DefaultValue => null!; - public static bool IsNone(in byte[] value) => value is null; - - public static void WriteData(ref WriteContext context, in byte[] value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in byte[] value, bool hasGenerics) { _ = hasGenerics; byte[] safe = value ?? []; @@ -313,135 +282,150 @@ public static void WriteData(ref WriteContext context, in byte[] value, bool has context.Writer.WriteBytes(safe); } - public static byte[] ReadData(ref ReadContext context) + public override byte[] ReadData(ref ReadContext context) { uint length = context.Reader.ReadVarUInt32(); return context.Reader.ReadBytes(checked((int)length)); } } -public readonly struct ForyInt32FixedSerializer : IStaticSerializer +public sealed class ForyInt32FixedSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Int32; - public static ForyInt32Fixed DefaultValue => new(0); - public static void WriteData(ref WriteContext context, in ForyInt32Fixed value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Int32; + + public override ForyInt32Fixed DefaultValue => new(0); + + public override void WriteData(ref WriteContext context, in ForyInt32Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt32(value.RawValue); } - public static ForyInt32Fixed ReadData(ref ReadContext context) + public override ForyInt32Fixed ReadData(ref ReadContext context) { return new ForyInt32Fixed(context.Reader.ReadInt32()); } } -public readonly struct ForyInt64FixedSerializer : IStaticSerializer +public sealed class ForyInt64FixedSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Int64; - public static ForyInt64Fixed DefaultValue => new(0); - public static void WriteData(ref WriteContext context, in ForyInt64Fixed value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Int64; + + public override ForyInt64Fixed DefaultValue => new(0); + + public override void WriteData(ref WriteContext context, in ForyInt64Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteInt64(value.RawValue); } - public static ForyInt64Fixed ReadData(ref ReadContext context) + public override ForyInt64Fixed ReadData(ref ReadContext context) { return new ForyInt64Fixed(context.Reader.ReadInt64()); } } -public readonly struct ForyInt64TaggedSerializer : IStaticSerializer +public sealed class ForyInt64TaggedSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.TaggedInt64; - public static ForyInt64Tagged DefaultValue => new(0); - public static void WriteData(ref WriteContext context, in ForyInt64Tagged value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.TaggedInt64; + + public override ForyInt64Tagged DefaultValue => new(0); + + public override void WriteData(ref WriteContext context, in ForyInt64Tagged value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteTaggedInt64(value.RawValue); } - public static ForyInt64Tagged ReadData(ref ReadContext context) + public override ForyInt64Tagged ReadData(ref ReadContext context) { return new ForyInt64Tagged(context.Reader.ReadTaggedInt64()); } } -public readonly struct ForyUInt32FixedSerializer : IStaticSerializer +public sealed class ForyUInt32FixedSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.UInt32; - public static ForyUInt32Fixed DefaultValue => new(0); - public static void WriteData(ref WriteContext context, in ForyUInt32Fixed value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.UInt32; + + public override ForyUInt32Fixed DefaultValue => new(0); + + public override void WriteData(ref WriteContext context, in ForyUInt32Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt32(value.RawValue); } - public static ForyUInt32Fixed ReadData(ref ReadContext context) + public override ForyUInt32Fixed ReadData(ref ReadContext context) { return new ForyUInt32Fixed(context.Reader.ReadUInt32()); } } -public readonly struct ForyUInt64FixedSerializer : IStaticSerializer +public sealed class ForyUInt64FixedSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.UInt64; - public static ForyUInt64Fixed DefaultValue => new(0); - public static void WriteData(ref WriteContext context, in ForyUInt64Fixed value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.UInt64; + + public override ForyUInt64Fixed DefaultValue => new(0); + + public override void WriteData(ref WriteContext context, in ForyUInt64Fixed value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteUInt64(value.RawValue); } - public static ForyUInt64Fixed ReadData(ref ReadContext context) + public override ForyUInt64Fixed ReadData(ref ReadContext context) { return new ForyUInt64Fixed(context.Reader.ReadUInt64()); } } -public readonly struct ForyUInt64TaggedSerializer : IStaticSerializer +public sealed class ForyUInt64TaggedSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.TaggedUInt64; - public static ForyUInt64Tagged DefaultValue => new(0); - public static void WriteData(ref WriteContext context, in ForyUInt64Tagged value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.TaggedUInt64; + + public override ForyUInt64Tagged DefaultValue => new(0); + + public override void WriteData(ref WriteContext context, in ForyUInt64Tagged value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteTaggedUInt64(value.RawValue); } - public static ForyUInt64Tagged ReadData(ref ReadContext context) + public override ForyUInt64Tagged ReadData(ref ReadContext context) { return new ForyUInt64Tagged(context.Reader.ReadTaggedUInt64()); } } -public readonly struct DateOnlySerializer : IStaticSerializer +public sealed class DateOnlySerializer : TypedSerializer { private static readonly DateOnly Epoch = new(1970, 1, 1); - public static TypeId StaticTypeId => TypeId.Date; - public static DateOnly DefaultValue => Epoch; - public static void WriteData(ref WriteContext context, in DateOnly value, bool hasGenerics) + public override TypeId StaticTypeId => TypeId.Date; + + public override DateOnly DefaultValue => Epoch; + + public override void WriteData(ref WriteContext context, in DateOnly value, bool hasGenerics) { _ = hasGenerics; int days = value.DayNumber - Epoch.DayNumber; context.Writer.WriteInt32(days); } - public static DateOnly ReadData(ref ReadContext context) + public override DateOnly ReadData(ref ReadContext context) { int days = context.Reader.ReadInt32(); return DateOnly.FromDayNumber(Epoch.DayNumber + days); } } -public readonly struct DateTimeOffsetSerializer : IStaticSerializer +public sealed class DateTimeOffsetSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Timestamp; - public static DateTimeOffset DefaultValue => DateTimeOffset.UnixEpoch; + public override TypeId StaticTypeId => TypeId.Timestamp; - public static void WriteData(ref WriteContext context, in DateTimeOffset value, bool hasGenerics) + public override DateTimeOffset DefaultValue => DateTimeOffset.UnixEpoch; + + public override void WriteData(ref WriteContext context, in DateTimeOffset value, bool hasGenerics) { _ = hasGenerics; ForyTimestamp ts = ForyTimestamp.FromDateTimeOffset(value); @@ -449,7 +433,7 @@ public static void WriteData(ref WriteContext context, in DateTimeOffset value, context.Writer.WriteUInt32(ts.Nanos); } - public static DateTimeOffset ReadData(ref ReadContext context) + public override DateTimeOffset ReadData(ref ReadContext context) { long seconds = context.Reader.ReadInt64(); uint nanos = context.Reader.ReadUInt32(); @@ -457,12 +441,13 @@ public static DateTimeOffset ReadData(ref ReadContext context) } } -public readonly struct DateTimeSerializer : IStaticSerializer +public sealed class DateTimeSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Timestamp; - public static DateTime DefaultValue => DateTime.UnixEpoch; + public override TypeId StaticTypeId => TypeId.Timestamp; + + public override DateTime DefaultValue => DateTime.UnixEpoch; - public static void WriteData(ref WriteContext context, in DateTime value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in DateTime value, bool hasGenerics) { _ = hasGenerics; DateTimeOffset dto = value.Kind switch @@ -476,7 +461,7 @@ public static void WriteData(ref WriteContext context, in DateTime value, bool h context.Writer.WriteUInt32(ts.Nanos); } - public static DateTime ReadData(ref ReadContext context) + public override DateTime ReadData(ref ReadContext context) { long seconds = context.Reader.ReadInt64(); uint nanos = context.Reader.ReadUInt32(); @@ -484,12 +469,13 @@ public static DateTime ReadData(ref ReadContext context) } } -public readonly struct TimeSpanSerializer : IStaticSerializer +public sealed class TimeSpanSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Duration; - public static TimeSpan DefaultValue => TimeSpan.Zero; + public override TypeId StaticTypeId => TypeId.Duration; + + public override TimeSpan DefaultValue => TimeSpan.Zero; - public static void WriteData(ref WriteContext context, in TimeSpan value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in TimeSpan value, bool hasGenerics) { _ = hasGenerics; long seconds = value.Ticks / TimeSpan.TicksPerSecond; @@ -498,7 +484,7 @@ public static void WriteData(ref WriteContext context, in TimeSpan value, bool h context.Writer.WriteInt32(nanos); } - public static TimeSpan ReadData(ref ReadContext context) + public override TimeSpan ReadData(ref ReadContext context) { long seconds = context.Reader.ReadInt64(); int nanos = context.Reader.ReadInt32(); diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs index 099b46291c..7ff987f8d5 100644 --- a/csharp/src/Fory/Serializer.cs +++ b/csharp/src/Fory/Serializer.cs @@ -17,160 +17,43 @@ namespace Apache.Fory; -public interface IStaticSerializer - where TSerializer : IStaticSerializer -{ - static abstract TypeId StaticTypeId { get; } - - static virtual bool IsNullableType => false; - - static virtual bool IsReferenceTrackableType => false; - - static virtual T DefaultValue => default!; - - static virtual bool IsNone(in T value) - { - _ = value; - return false; - } - - static abstract void WriteData(ref WriteContext context, in T value, bool hasGenerics); - - static abstract T ReadData(ref ReadContext context); - - static virtual IReadOnlyList CompatibleTypeMetaFields(bool trackRef) - { - _ = trackRef; - return []; - } - - static virtual void WriteTypeInfo(ref WriteContext context) - { - SerializerTypeInfo.WriteTypeInfo(ref context); - } - - static virtual void ReadTypeInfo(ref ReadContext context) - { - SerializerTypeInfo.ReadTypeInfo(ref context); - } - - static virtual void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) - { - if (refMode != RefMode.None) - { - bool wroteTrackingRefFlag = false; - if (refMode == RefMode.Tracking && - TSerializer.IsReferenceTrackableType && - value is object obj) - { - if (context.RefWriter.TryWriteReference(context.Writer, obj)) - { - return; - } - - wroteTrackingRefFlag = true; - } - - if (!wroteTrackingRefFlag) - { - if (TSerializer.IsNullableType && TSerializer.IsNone(value)) - { - context.Writer.WriteInt8((sbyte)RefFlag.Null); - return; - } - - context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); - } - } - - if (writeTypeInfo) - { - TSerializer.WriteTypeInfo(ref context); - } - - TSerializer.WriteData(ref context, value, hasGenerics); - } - - static virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) - { - if (refMode != RefMode.None) - { - sbyte rawFlag = context.Reader.ReadInt8(); - RefFlag flag = (RefFlag)rawFlag; - switch (flag) - { - case RefFlag.Null: - return TSerializer.DefaultValue; - case RefFlag.Ref: - uint refId = context.Reader.ReadVarUInt32(); - return context.RefReader.ReadRef(refId); - case RefFlag.RefValue: - { - uint reservedRefId = context.RefReader.ReserveRefId(); - context.PushPendingReference(reservedRefId); - if (readTypeInfo) - { - TSerializer.ReadTypeInfo(ref context); - } - - T value = TSerializer.ReadData(ref context); - context.FinishPendingReferenceIfNeeded(value); - context.PopPendingReference(); - return value; - } - case RefFlag.NotNullValue: - break; - default: - throw new RefException($"invalid ref flag {rawFlag}"); - } - } - - if (readTypeInfo) - { - TSerializer.ReadTypeInfo(ref context); - } - - return TSerializer.ReadData(ref context); - } -} - public readonly struct Serializer { - private readonly TypedSerializerBinding? _binding; + private readonly ITypedSerializer? _serializer; - internal Serializer(TypedSerializerBinding binding) + internal Serializer(ITypedSerializer serializer) { - _binding = binding; + _serializer = serializer; } - private TypedSerializerBinding Binding + private ITypedSerializer SerializerImpl { get { - if (_binding is null) + if (_serializer is null) { throw new InvalidDataException($"serializer handle for {typeof(T)} is not initialized"); } - return _binding; + return _serializer; } } public Type Type => typeof(T); - public TypeId StaticTypeId => Binding.StaticTypeId; + public TypeId StaticTypeId => SerializerImpl.StaticTypeId; - public bool IsNullableType => Binding.IsNullableType; + public bool IsNullableType => SerializerImpl.IsNullableType; - public bool IsReferenceTrackableType => Binding.IsReferenceTrackableType; + public bool IsReferenceTrackableType => SerializerImpl.IsReferenceTrackableType; - public T DefaultValue => Binding.DefaultValue; + public T DefaultValue => SerializerImpl.DefaultValue; - public object? DefaultObject => Binding.DefaultValue; + public object? DefaultObject => SerializerImpl.DefaultValue; public bool IsNone(in T value) { - return Binding.IsNone(value); + return SerializerImpl.IsNone(value); } public bool IsNoneObject(object? value) @@ -185,53 +68,51 @@ public bool IsNoneObject(object? value) public void WriteData(ref WriteContext context, in T value, bool hasGenerics) { - Binding.WriteData(ref context, value, hasGenerics); + SerializerImpl.WriteData(ref context, value, hasGenerics); } public T ReadData(ref ReadContext context) { - return Binding.ReadData(ref context); + return SerializerImpl.ReadData(ref context); } public void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { - Binding.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + SerializerImpl.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); } public T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { - return Binding.Read(ref context, refMode, readTypeInfo); + return SerializerImpl.Read(ref context, refMode, readTypeInfo); } public void WriteTypeInfo(ref WriteContext context) { - Binding.WriteTypeInfo(ref context); + SerializerImpl.WriteTypeInfo(ref context); } public void ReadTypeInfo(ref ReadContext context) { - Binding.ReadTypeInfo(ref context); + SerializerImpl.ReadTypeInfo(ref context); } public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { - return Binding.CompatibleTypeMetaFields(trackRef); + return SerializerImpl.CompatibleTypeMetaFields(trackRef); } } internal static class SerializerTypeInfo { - public static void WriteTypeInfo(ref WriteContext context) - where TSerializer : IStaticSerializer + public static void WriteTypeInfo(Type type, SerializerBinding serializer, ref WriteContext context) { - TypeId staticTypeId = TSerializer.StaticTypeId; + TypeId staticTypeId = serializer.StaticTypeId; if (!staticTypeId.IsUserTypeKind()) { context.Writer.WriteUInt8((byte)staticTypeId); return; } - Type type = typeof(T); RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); TypeId wireTypeId = ResolveWireTypeId(info.Kind, info.RegisterByName, context.Compatible); context.Writer.WriteUInt8((byte)wireTypeId); @@ -240,7 +121,7 @@ public static void WriteTypeInfo(ref WriteContext context) case TypeId.CompatibleStruct: case TypeId.NamedCompatibleStruct: { - TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); + TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); context.WriteCompatibleTypeMeta(type, typeMeta); return; } @@ -251,7 +132,7 @@ public static void WriteTypeInfo(ref WriteContext context) { if (context.Compatible) { - TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); + TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); context.WriteCompatibleTypeMeta(type, typeMeta); } else @@ -290,8 +171,7 @@ public static void WriteTypeInfo(ref WriteContext context) } } - public static void ReadTypeInfo(ref ReadContext context) - where TSerializer : IStaticSerializer + public static void ReadTypeInfo(Type type, SerializerBinding serializer, ref ReadContext context) { uint rawTypeId = context.Reader.ReadVarUInt32(); if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) @@ -300,7 +180,7 @@ public static void ReadTypeInfo(ref ReadContext context) } TypeId typeId = (TypeId)rawTypeId; - TypeId staticTypeId = TSerializer.StaticTypeId; + TypeId staticTypeId = serializer.StaticTypeId; if (!staticTypeId.IsUserTypeKind()) { if (typeId != staticTypeId) @@ -311,7 +191,6 @@ public static void ReadTypeInfo(ref ReadContext context) return; } - Type type = typeof(T); RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); HashSet allowed = AllowedWireTypeIds(info.Kind, info.RegisterByName, context.Compatible); if (!allowed.Contains(typeId)) @@ -452,13 +331,12 @@ private static bool WireTypeNeedsUserTypeId(TypeId typeId) return typeId is TypeId.Enum or TypeId.Struct or TypeId.Ext or TypeId.TypedUnion; } - private static TypeMeta BuildCompatibleTypeMeta( + private static TypeMeta BuildCompatibleTypeMeta( RegisteredTypeInfo info, TypeId wireTypeId, bool trackRef) - where TSerializer : IStaticSerializer { - IReadOnlyList fields = TSerializer.CompatibleTypeMetaFields(trackRef); + IReadOnlyList fields = info.Serializer.CompatibleTypeMetaFields(trackRef); bool hasFieldsMeta = fields.Count > 0; if (info.RegisterByName) { diff --git a/csharp/src/Fory/StringSerializer.cs b/csharp/src/Fory/StringSerializer.cs new file mode 100644 index 0000000000..6ea8ad5745 --- /dev/null +++ b/csharp/src/Fory/StringSerializer.cs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System.Text; + +namespace Apache.Fory; + +public sealed class StringSerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.String; + + public override bool IsNullableType => true; + + public override string DefaultValue => null!; + + public override bool IsNone(in string value) => value is null; + + public override void WriteData(ref WriteContext context, in string value, bool hasGenerics) + { + _ = hasGenerics; + WriteString(ref context, value ?? string.Empty); + } + + public override string ReadData(ref ReadContext context) + { + return ReadString(ref context); + } + + public static void WriteString(ref WriteContext context, string value) + { + string safe = value ?? string.Empty; + byte[] utf8 = Encoding.UTF8.GetBytes(safe); + ulong header = ((ulong)utf8.Length << 2) | (ulong)ForyStringEncoding.Utf8; + context.Writer.WriteVarUInt36Small(header); + context.Writer.WriteBytes(utf8); + } + + public static string ReadString(ref ReadContext context) + { + ulong header = context.Reader.ReadVarUInt36Small(); + ulong encoding = header & 0x03; + int byteLength = checked((int)(header >> 2)); + byte[] bytes = context.Reader.ReadBytes(byteLength); + return encoding switch + { + (ulong)ForyStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes), + (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes), + (ulong)ForyStringEncoding.Utf16 => DecodeUtf16(bytes), + _ => throw new EncodingException($"unsupported string encoding {encoding}"), + }; + } + + private static string DecodeLatin1(byte[] bytes) + { + return string.Create(bytes.Length, bytes, static (span, b) => + { + for (int i = 0; i < b.Length; i++) + { + span[i] = (char)b[i]; + } + }); + } + + private static string DecodeUtf16(byte[] bytes) + { + if ((bytes.Length & 1) != 0) + { + throw new EncodingException("utf16 byte length is not even"); + } + + return Encoding.Unicode.GetString(bytes); + } +} diff --git a/csharp/src/Fory/TypeInfo.cs b/csharp/src/Fory/TypeInfo.cs new file mode 100644 index 0000000000..c5469ef644 --- /dev/null +++ b/csharp/src/Fory/TypeInfo.cs @@ -0,0 +1,43 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +public sealed class TypeInfo +{ + internal TypeInfo(Type type, SerializerBinding serializer) + { + Type = type; + Serializer = serializer; + StaticTypeId = serializer.StaticTypeId; + IsNullableType = serializer.IsNullableType; + IsReferenceTrackableType = serializer.IsReferenceTrackableType; + DefaultObject = serializer.DefaultObject; + } + + public Type Type { get; } + + internal SerializerBinding Serializer { get; } + + public TypeId StaticTypeId { get; } + + public bool IsNullableType { get; } + + public bool IsReferenceTrackableType { get; } + + public object? DefaultObject { get; } +} diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index 2db261386e..03bc7cd3c1 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -46,7 +46,9 @@ internal sealed class TypeReader public sealed class TypeResolver { private static readonly ConcurrentDictionary> GeneratedFactories = new(); - private static readonly ConcurrentDictionary SharedStaticBindings = new(); + private static readonly ConcurrentDictionary SharedBindings = new(); + private static readonly ConcurrentDictionary SharedTypeInfos = new(); + private static int _sharedCacheVersion; private readonly Dictionary _byType = []; private readonly Dictionary _byUserTypeId = []; @@ -54,42 +56,106 @@ public sealed class TypeResolver private readonly Dictionary _registrationModeByKind = []; private readonly ConcurrentDictionary _serializerBindings = new(); - private readonly ConcurrentDictionary _typedBindings = new(); + private readonly ConcurrentDictionary _typeInfos = new(); + private int _cacheVersion; + + private static class SharedTypeInfoCache + { + public static int Version = -1; + public static TypeInfo? Cached; + } + + private static class TypeInfoCache + { + public static int Version = -1; + public static TypeResolver? Resolver; + public static TypeInfo? Cached; + } + + private static class SerializerCache + { + public static int Version = -1; + public static TypeResolver? Resolver; + public static ITypedSerializer? Cached; + } public static void RegisterGenerated() - where TSerializer : IStaticSerializer + where TSerializer : TypedSerializer, new() { Type type = typeof(T); - GeneratedFactories[type] = StaticSerializerBindingFactory.Create; - SharedStaticBindings.TryRemove(type, out _); + GeneratedFactories[type] = SerializerFactory.Create; + SharedBindings.TryRemove(type, out _); + SharedTypeInfos.TryRemove(type, out _); + unchecked + { + _sharedCacheVersion += 1; + } } public static TypeId StaticTypeIdOf() { - return GetSharedStaticBinding(typeof(T)).RequireTypedBinding().StaticTypeId; + return StaticTypeInfoOf().StaticTypeId; } public static bool IsReferenceTrackableTypeOf() { - return GetSharedStaticBinding(typeof(T)).RequireTypedBinding().IsReferenceTrackableType; + return StaticTypeInfoOf().IsReferenceTrackableType; } public static IReadOnlyList CompatibleTypeMetaFieldsOf(bool trackRef) { - return GetSharedStaticBinding(typeof(T)).RequireTypedBinding().CompatibleTypeMetaFields(trackRef); + return StaticTypeInfoOf().Serializer.CompatibleTypeMetaFields(trackRef); + } + + public static TypeInfo StaticTypeInfoOf() + { + if (SharedTypeInfoCache.Version == _sharedCacheVersion && + SharedTypeInfoCache.Cached is not null) + { + return SharedTypeInfoCache.Cached; + } + + TypeInfo typeInfo = GetSharedTypeInfo(typeof(T)); + SharedTypeInfoCache.Cached = typeInfo; + SharedTypeInfoCache.Version = _sharedCacheVersion; + return typeInfo; } public Serializer GetSerializer() { - Type type = typeof(T); - if (_typedBindings.TryGetValue(type, out object? cachedTypedBinding)) + if (SerializerCache.Version == _cacheVersion && + ReferenceEquals(SerializerCache.Resolver, this) && + SerializerCache.Cached is not null) { - return new Serializer((TypedSerializerBinding)cachedTypedBinding); + return new Serializer(SerializerCache.Cached); } - TypedSerializerBinding typedBinding = GetBinding(type).RequireTypedBinding(); - object typedObject = _typedBindings.GetOrAdd(type, typedBinding); - return new Serializer((TypedSerializerBinding)typedObject); + ITypedSerializer typedSerializer = GetTypeInfo().Serializer.RequireTypedSerializer(); + SerializerCache.Resolver = this; + SerializerCache.Version = _cacheVersion; + SerializerCache.Cached = typedSerializer; + return new Serializer(typedSerializer); + } + + public TypeInfo GetTypeInfo(Type type) + { + return _typeInfos.GetOrAdd(type, t => new TypeInfo(t, GetBinding(t))); + } + + public TypeInfo GetTypeInfo() + { + if (TypeInfoCache.Version == _cacheVersion && + ReferenceEquals(TypeInfoCache.Resolver, this) && + TypeInfoCache.Cached is not null) + { + return TypeInfoCache.Cached; + } + + TypeInfo typeInfo = GetTypeInfo(typeof(T)); + TypeInfoCache.Resolver = this; + TypeInfoCache.Version = _cacheVersion; + TypeInfoCache.Cached = typeInfo; + return typeInfo; } internal SerializerBinding GetBinding(Type type) @@ -98,9 +164,9 @@ internal SerializerBinding GetBinding(Type type) } internal SerializerBinding RegisterCustom() - where TSerializer : IStaticSerializer + where TSerializer : TypedSerializer, new() { - SerializerBinding serializerBinding = StaticSerializerBindingFactory.Create(); + SerializerBinding serializerBinding = SerializerFactory.Create(); RegisterCustom(typeof(T), serializerBinding); return serializerBinding; } @@ -108,7 +174,11 @@ internal SerializerBinding RegisterCustom() internal void RegisterCustom(Type type, SerializerBinding serializerBinding) { _serializerBindings[type] = serializerBinding; - _typedBindings.TryRemove(type, out _); + _typeInfos.TryRemove(type, out _); + unchecked + { + _cacheVersion += 1; + } } internal void Register(Type type, uint id, SerializerBinding? explicitSerializer = null) @@ -126,11 +196,11 @@ internal void Register(Type type, uint id, SerializerBinding? explicitSerializer _byUserTypeId[id] = new TypeReader { Kind = serializer.StaticTypeId, - Reader = context => serializer.Read(ref context, RefMode.None, false), + Reader = context => serializer.ReadObject(ref context, RefMode.None, false), CompatibleReader = (context, typeMeta) => { context.PushCompatibleTypeMeta(type, typeMeta); - return serializer.Read(ref context, RefMode.None, false); + return serializer.ReadObject(ref context, RefMode.None, false); }, }; } @@ -152,11 +222,11 @@ internal void Register(Type type, string namespaceName, string typeName, Seriali _byTypeName[new TypeNameKey(namespaceName, typeName)] = new TypeReader { Kind = serializer.StaticTypeId, - Reader = context => serializer.Read(ref context, RefMode.None, false), + Reader = context => serializer.ReadObject(ref context, RefMode.None, false), CompatibleReader = (context, typeMeta) => { context.PushCompatibleTypeMeta(type, typeMeta); - return serializer.Read(ref context, RefMode.None, false); + return serializer.ReadObject(ref context, RefMode.None, false); }, }; } @@ -413,9 +483,14 @@ private DynamicRegistrationMode DynamicRegistrationModeFor(TypeId kind) throw new TypeNotRegisteredException($"no dynamic registration mode for kind {kind}"); } - private static SerializerBinding GetSharedStaticBinding(Type type) + private static SerializerBinding GetSharedBinding(Type type) { - return SharedStaticBindings.GetOrAdd(type, CreateBindingCore); + return SharedBindings.GetOrAdd(type, CreateBindingCore); + } + + private static TypeInfo GetSharedTypeInfo(Type type) + { + return SharedTypeInfos.GetOrAdd(type, t => new TypeInfo(t, GetSharedBinding(t))); } private static SerializerBinding CreateBindingCore(Type type) @@ -427,141 +502,216 @@ private static SerializerBinding CreateBindingCore(Type type) if (type == typeof(bool)) { - return StaticSerializerBindingFactory.Create(); + return new BoolSerializer(); } if (type == typeof(sbyte)) { - return StaticSerializerBindingFactory.Create(); + return new Int8Serializer(); } if (type == typeof(short)) { - return StaticSerializerBindingFactory.Create(); + return new Int16Serializer(); } if (type == typeof(int)) { - return StaticSerializerBindingFactory.Create(); + return new Int32Serializer(); } if (type == typeof(long)) { - return StaticSerializerBindingFactory.Create(); + return new Int64Serializer(); } if (type == typeof(byte)) { - return StaticSerializerBindingFactory.Create(); + return new UInt8Serializer(); } if (type == typeof(ushort)) { - return StaticSerializerBindingFactory.Create(); + return new UInt16Serializer(); } if (type == typeof(uint)) { - return StaticSerializerBindingFactory.Create(); + return new UInt32Serializer(); } if (type == typeof(ulong)) { - return StaticSerializerBindingFactory.Create(); + return new UInt64Serializer(); } if (type == typeof(float)) { - return StaticSerializerBindingFactory.Create(); + return new Float32Serializer(); } if (type == typeof(double)) { - return StaticSerializerBindingFactory.Create(); + return new Float64Serializer(); } if (type == typeof(string)) { - return StaticSerializerBindingFactory.Create(); + return new StringSerializer(); } if (type == typeof(byte[])) { - return StaticSerializerBindingFactory.Create(); + return new BinarySerializer(); } if (type == typeof(DateOnly)) { - return StaticSerializerBindingFactory.Create(); + return new DateOnlySerializer(); } if (type == typeof(DateTimeOffset)) { - return StaticSerializerBindingFactory.Create(); + return new DateTimeOffsetSerializer(); } if (type == typeof(DateTime)) { - return StaticSerializerBindingFactory.Create(); + return new DateTimeSerializer(); } if (type == typeof(TimeSpan)) { - return StaticSerializerBindingFactory.Create(); + return new TimeSpanSerializer(); + } + + if (type == typeof(List)) + { + return new ListBoolSerializer(); + } + + if (type == typeof(List)) + { + return new ListIntSerializer(); + } + + if (type == typeof(List)) + { + return new ListLongSerializer(); + } + + if (type == typeof(List)) + { + return new ListStringSerializer(); + } + + if (type == typeof(List)) + { + return new ListDateOnlySerializer(); + } + + if (type == typeof(List)) + { + return new ListDateTimeOffsetSerializer(); + } + + if (type == typeof(List)) + { + return new ListDateTimeSerializer(); + } + + if (type == typeof(List)) + { + return new ListTimeSpanSerializer(); + } + + if (type == typeof(Dictionary)) + { + return new DictionaryStringStringSerializer(); + } + + if (type == typeof(Dictionary)) + { + return new DictionaryStringIntSerializer(); + } + + if (type == typeof(Dictionary)) + { + return new DictionaryStringLongSerializer(); + } + + if (type == typeof(Dictionary)) + { + return new DictionaryStringBoolSerializer(); + } + + if (type == typeof(Dictionary)) + { + return new DictionaryStringDoubleSerializer(); + } + + if (type == typeof(Dictionary)) + { + return new DictionaryIntIntSerializer(); + } + + if (type == typeof(Dictionary)) + { + return new DictionaryLongLongSerializer(); } if (type == typeof(ForyInt32Fixed)) { - return StaticSerializerBindingFactory.Create(); + return new ForyInt32FixedSerializer(); } if (type == typeof(ForyInt64Fixed)) { - return StaticSerializerBindingFactory.Create(); + return new ForyInt64FixedSerializer(); } if (type == typeof(ForyInt64Tagged)) { - return StaticSerializerBindingFactory.Create(); + return new ForyInt64TaggedSerializer(); } if (type == typeof(ForyUInt32Fixed)) { - return StaticSerializerBindingFactory.Create(); + return new ForyUInt32FixedSerializer(); } if (type == typeof(ForyUInt64Fixed)) { - return StaticSerializerBindingFactory.Create(); + return new ForyUInt64FixedSerializer(); } if (type == typeof(ForyUInt64Tagged)) { - return StaticSerializerBindingFactory.Create(); + return new ForyUInt64TaggedSerializer(); } if (type == typeof(object)) { - return StaticSerializerBindingFactory.Create(); + return new DynamicAnyObjectSerializer(); } if (typeof(Union).IsAssignableFrom(type)) { Type serializerType = typeof(UnionSerializer<>).MakeGenericType(type); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } if (type.IsEnum) { Type serializerType = typeof(EnumSerializer<>).MakeGenericType(type); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } if (type.IsArray) { Type elementType = type.GetElementType()!; Type serializerType = typeof(ArraySerializer<>).MakeGenericType(elementType); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } if (type.IsGenericType) @@ -571,31 +721,31 @@ private static SerializerBinding CreateBindingCore(Type type) if (genericType == typeof(Nullable<>)) { Type serializerType = typeof(NullableSerializer<>).MakeGenericType(genericArgs[0]); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } if (genericType == typeof(List<>)) { Type serializerType = typeof(ListSerializer<>).MakeGenericType(genericArgs[0]); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } if (genericType == typeof(HashSet<>)) { Type serializerType = typeof(SetSerializer<>).MakeGenericType(genericArgs[0]); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } if (genericType == typeof(Dictionary<,>)) { Type serializerType = typeof(DictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } if (genericType == typeof(NullableKeyDictionary<,>)) { Type serializerType = typeof(NullableKeyDictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return StaticSerializerBindingFactory.Create(type, serializerType); + return SerializerFactory.Create(serializerType); } } diff --git a/csharp/src/Fory/TypedSerializerBinding.cs b/csharp/src/Fory/TypedSerializerBinding.cs index efe584dca5..4c30c158a8 100644 --- a/csharp/src/Fory/TypedSerializerBinding.cs +++ b/csharp/src/Fory/TypedSerializerBinding.cs @@ -15,399 +15,277 @@ // specific language governing permissions and limitations // under the License. -using System.Reflection; +using System.Collections.Concurrent; +using System.Linq.Expressions; namespace Apache.Fory; -internal delegate bool TypedIsNoneDelegate(in T value); - -internal delegate void TypedWriteDataDelegate( - ref WriteContext context, - in T value, - bool hasGenerics); +public interface ITypedSerializer +{ + TypeId StaticTypeId { get; } -internal delegate T TypedReadDataDelegate(ref ReadContext context); + bool IsNullableType { get; } -internal delegate void TypedWriteDelegate( - ref WriteContext context, - in T value, - RefMode refMode, - bool writeTypeInfo, - bool hasGenerics); + bool IsReferenceTrackableType { get; } -internal delegate T TypedReadDelegate( - ref ReadContext context, - RefMode refMode, - bool readTypeInfo); + T DefaultValue { get; } -internal delegate void TypeInfoWriteDelegate(ref WriteContext context); + bool IsNone(in T value); -internal delegate void TypeInfoReadDelegate(ref ReadContext context); + void WriteData(ref WriteContext context, in T value, bool hasGenerics); -internal delegate IReadOnlyList CompatibleTypeMetaFieldsDelegate(bool trackRef); + T ReadData(ref ReadContext context); -internal delegate bool UntypedIsNoneObjectDelegate(object? value); + void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics); -internal delegate void UntypedWriteDataDelegate(ref WriteContext context, object? value, bool hasGenerics); + T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo); -internal delegate object? UntypedReadDataDelegate(ref ReadContext context); + void WriteTypeInfo(ref WriteContext context); -internal delegate void UntypedWriteDelegate( - ref WriteContext context, - object? value, - RefMode refMode, - bool writeTypeInfo, - bool hasGenerics); + void ReadTypeInfo(ref ReadContext context); -internal delegate object? UntypedReadDelegate( - ref ReadContext context, - RefMode refMode, - bool readTypeInfo); + IReadOnlyList CompatibleTypeMetaFields(bool trackRef); +} -internal sealed class TypedSerializerBinding +public abstract class SerializerBinding { - public TypedSerializerBinding( - TypeId staticTypeId, - bool isNullableType, - bool isReferenceTrackableType, - T defaultValue, - TypedIsNoneDelegate isNone, - TypedWriteDataDelegate writeData, - TypedReadDataDelegate readData, - TypedWriteDelegate write, - TypedReadDelegate read, - TypeInfoWriteDelegate writeTypeInfo, - TypeInfoReadDelegate readTypeInfo, - CompatibleTypeMetaFieldsDelegate compatibleTypeMetaFields) - { - StaticTypeId = staticTypeId; - IsNullableType = isNullableType; - IsReferenceTrackableType = isReferenceTrackableType; - DefaultValue = defaultValue; - _isNone = isNone; - _writeData = writeData; - _readData = readData; - _write = write; - _read = read; - _writeTypeInfo = writeTypeInfo; - _readTypeInfo = readTypeInfo; - _compatibleTypeMetaFields = compatibleTypeMetaFields; - } + public abstract Type Type { get; } - public TypeId StaticTypeId { get; } + public abstract TypeId StaticTypeId { get; } - public bool IsNullableType { get; } + public abstract bool IsNullableType { get; } - public bool IsReferenceTrackableType { get; } + public abstract bool IsReferenceTrackableType { get; } - public T DefaultValue { get; } + public abstract object? DefaultObject { get; } - private readonly TypedIsNoneDelegate _isNone; - private readonly TypedWriteDataDelegate _writeData; - private readonly TypedReadDataDelegate _readData; - private readonly TypedWriteDelegate _write; - private readonly TypedReadDelegate _read; - private readonly TypeInfoWriteDelegate _writeTypeInfo; - private readonly TypeInfoReadDelegate _readTypeInfo; - private readonly CompatibleTypeMetaFieldsDelegate _compatibleTypeMetaFields; + public abstract bool IsNoneObject(object? value); - public bool IsNone(in T value) - { - return _isNone(value); - } + public abstract void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics); - public void WriteData(ref WriteContext context, in T value, bool hasGenerics) - { - _writeData(ref context, value, hasGenerics); - } + public abstract object? ReadDataObject(ref ReadContext context); - public T ReadData(ref ReadContext context) - { - return _readData(ref context); - } + public abstract void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics); - public void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) - { - _write(ref context, value, refMode, writeTypeInfo, hasGenerics); - } + public abstract object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo); - public T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) - { - return _read(ref context, refMode, readTypeInfo); - } + public abstract void WriteTypeInfo(ref WriteContext context); - public void WriteTypeInfo(ref WriteContext context) - { - _writeTypeInfo(ref context); - } + public abstract void ReadTypeInfo(ref ReadContext context); - public void ReadTypeInfo(ref ReadContext context) - { - _readTypeInfo(ref context); - } + public abstract IReadOnlyList CompatibleTypeMetaFields(bool trackRef); - public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) - { - return _compatibleTypeMetaFields(trackRef); - } + public abstract ITypedSerializer RequireTypedSerializer(); } -internal sealed class SerializerBinding +public abstract class TypedSerializer : SerializerBinding, ITypedSerializer { - public SerializerBinding( - Type type, - TypeId staticTypeId, - bool isNullableType, - bool isReferenceTrackableType, - object? defaultObject, - UntypedIsNoneObjectDelegate isNoneObject, - UntypedWriteDataDelegate writeData, - UntypedReadDataDelegate readData, - UntypedWriteDelegate write, - UntypedReadDelegate read, - TypeInfoWriteDelegate writeTypeInfo, - TypeInfoReadDelegate readTypeInfo, - CompatibleTypeMetaFieldsDelegate compatibleTypeMetaFields, - object typedBinding) - { - Type = type; - StaticTypeId = staticTypeId; - IsNullableType = isNullableType; - IsReferenceTrackableType = isReferenceTrackableType; - DefaultObject = defaultObject; - _isNoneObject = isNoneObject; - _writeData = writeData; - _readData = readData; - _write = write; - _read = read; - _writeTypeInfo = writeTypeInfo; - _readTypeInfo = readTypeInfo; - _compatibleTypeMetaFields = compatibleTypeMetaFields; - _typedBinding = typedBinding; - } + public override Type Type => typeof(T); - public Type Type { get; } + public abstract override TypeId StaticTypeId { get; } - public TypeId StaticTypeId { get; } + public override bool IsNullableType => false; - public bool IsNullableType { get; } + public override bool IsReferenceTrackableType => false; - public bool IsReferenceTrackableType { get; } - - public object? DefaultObject { get; } - - private readonly UntypedIsNoneObjectDelegate _isNoneObject; - private readonly UntypedWriteDataDelegate _writeData; - private readonly UntypedReadDataDelegate _readData; - private readonly UntypedWriteDelegate _write; - private readonly UntypedReadDelegate _read; - private readonly TypeInfoWriteDelegate _writeTypeInfo; - private readonly TypeInfoReadDelegate _readTypeInfo; - private readonly CompatibleTypeMetaFieldsDelegate _compatibleTypeMetaFields; - private readonly object _typedBinding; - - public bool IsNoneObject(object? value) - { - return _isNoneObject(value); - } + public virtual T DefaultValue => default!; - public void WriteData(ref WriteContext context, object? value, bool hasGenerics) - { - _writeData(ref context, value, hasGenerics); - } + public override object? DefaultObject => DefaultValue; - public object? ReadData(ref ReadContext context) + public virtual bool IsNone(in T value) { - return _readData(ref context); + _ = value; + return false; } - public void Write(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) - { - _write(ref context, value, refMode, writeTypeInfo, hasGenerics); - } + public abstract void WriteData(ref WriteContext context, in T value, bool hasGenerics); - public object? Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) - { - return _read(ref context, refMode, readTypeInfo); - } + public abstract T ReadData(ref ReadContext context); - public void WriteTypeInfo(ref WriteContext context) + public virtual void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { - _writeTypeInfo(ref context); - } - - public void ReadTypeInfo(ref ReadContext context) - { - _readTypeInfo(ref context); - } - - public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) - { - return _compatibleTypeMetaFields(trackRef); - } + if (refMode != RefMode.None) + { + bool wroteTrackingRefFlag = false; + if (refMode == RefMode.Tracking && + IsReferenceTrackableType && + value is object obj) + { + if (context.RefWriter.TryWriteReference(context.Writer, obj)) + { + return; + } + + wroteTrackingRefFlag = true; + } + + if (!wroteTrackingRefFlag) + { + if (IsNullableType && IsNone(value)) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + return; + } + + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); + } + } - public TypedSerializerBinding RequireTypedBinding() - { - if (_typedBinding is TypedSerializerBinding typed) + if (writeTypeInfo) { - return typed; + WriteTypeInfo(ref context); } - throw new InvalidDataException($"serializer type mismatch for {typeof(T)}"); + WriteData(ref context, value, hasGenerics); } -} -internal static class StaticSerializerDispatch - where TSerializer : IStaticSerializer -{ - private static readonly TypedSerializerBinding TypedBindingValue = new( - TSerializer.StaticTypeId, - TSerializer.IsNullableType, - TSerializer.IsReferenceTrackableType, - TSerializer.DefaultValue, - IsNone, - WriteData, - ReadData, - Write, - Read, - WriteTypeInfo, - ReadTypeInfo, - CompatibleTypeMetaFields); - - public static TypedSerializerBinding TypedBinding => TypedBindingValue; - - public static bool IsNone(in T value) + public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) { - return TSerializer.IsNone(value); - } + if (refMode != RefMode.None) + { + sbyte rawFlag = context.Reader.ReadInt8(); + RefFlag flag = (RefFlag)rawFlag; + switch (flag) + { + case RefFlag.Null: + return DefaultValue; + case RefFlag.Ref: + { + uint refId = context.Reader.ReadVarUInt32(); + return context.RefReader.ReadRef(refId); + } + case RefFlag.RefValue: + { + uint reservedRefId = context.RefReader.ReserveRefId(); + context.PushPendingReference(reservedRefId); + if (readTypeInfo) + { + ReadTypeInfo(ref context); + } + + T value = ReadData(ref context); + context.FinishPendingReferenceIfNeeded(value); + context.PopPendingReference(); + return value; + } + case RefFlag.NotNullValue: + break; + default: + throw new RefException($"invalid ref flag {rawFlag}"); + } + } - public static bool IsNoneObject(object? value) - { - if (value is null) + if (readTypeInfo) { - return TSerializer.IsNullableType; + ReadTypeInfo(ref context); } - return value is T typed && TSerializer.IsNone(typed); + return ReadData(ref context); } - public static void WriteData(ref WriteContext context, in T value, bool hasGenerics) + public override void WriteTypeInfo(ref WriteContext context) { - TSerializer.WriteData(ref context, value, hasGenerics); + SerializerTypeInfo.WriteTypeInfo(Type, this, ref context); } - public static T ReadData(ref ReadContext context) + public override void ReadTypeInfo(ref ReadContext context) { - return TSerializer.ReadData(ref context); + SerializerTypeInfo.ReadTypeInfo(Type, this, ref context); } - public static void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public override IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { - TSerializer.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + _ = trackRef; + return []; } - public static T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + public override bool IsNoneObject(object? value) { - return TSerializer.Read(ref context, refMode, readTypeInfo); - } + if (value is null) + { + return IsNullableType; + } - public static void WriteTypeInfo(ref WriteContext context) - { - TSerializer.WriteTypeInfo(ref context); + return value is T typed && IsNone(typed); } - public static void ReadTypeInfo(ref ReadContext context) + public override void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics) { - TSerializer.ReadTypeInfo(ref context); + WriteData(ref context, CoerceValue(value), hasGenerics); } - public static IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + public override object? ReadDataObject(ref ReadContext context) { - return TSerializer.CompatibleTypeMetaFields(trackRef); + return ReadData(ref context); } - public static void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics) + public override void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { - TSerializer.WriteData(ref context, CoerceValue(value), hasGenerics); + Write(ref context, CoerceValue(value), refMode, writeTypeInfo, hasGenerics); } - public static object? ReadDataObject(ref ReadContext context) + public override object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo) { - return TSerializer.ReadData(ref context); + return Read(ref context, refMode, readTypeInfo); } - public static void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public override ITypedSerializer RequireTypedSerializer() { - TSerializer.Write(ref context, CoerceValue(value), refMode, writeTypeInfo, hasGenerics); - } + if (typeof(TCast) == typeof(T)) + { + return (ITypedSerializer)(object)this; + } - public static object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo) - { - return TSerializer.Read(ref context, refMode, readTypeInfo); + throw new InvalidDataException($"serializer type mismatch for {typeof(TCast)}"); } - private static T CoerceValue(object? value) + protected virtual T CoerceValue(object? value) { if (value is T typed) { return typed; } - if (value is null && TSerializer.IsNullableType) + if (value is null && IsNullableType) { - return TSerializer.DefaultValue; + return DefaultValue; } throw new InvalidDataException( - $"serializer {typeof(TSerializer).Name} expected value of type {typeof(T)}, got {value?.GetType()}"); + $"serializer {GetType().Name} expected value of type {typeof(T)}, got {value?.GetType()}"); } } -internal static class StaticSerializerBindingFactory +internal static class SerializerFactory { - private static readonly MethodInfo CreateGenericMethod = - typeof(StaticSerializerBindingFactory).GetMethod( - nameof(CreateGeneric), - BindingFlags.NonPublic | BindingFlags.Static) - ?? throw new InvalidOperationException("missing static serializer binding factory method"); - - public static SerializerBinding Create() - where TSerializer : IStaticSerializer + private static readonly ConcurrentDictionary> Ctors = new(); + + public static SerializerBinding Create() + where TSerializer : SerializerBinding, new() { - TypedSerializerBinding typed = StaticSerializerDispatch.TypedBinding; - return new SerializerBinding( - typeof(T), - typed.StaticTypeId, - typed.IsNullableType, - typed.IsReferenceTrackableType, - typed.DefaultValue, - StaticSerializerDispatch.IsNoneObject, - StaticSerializerDispatch.WriteDataObject, - StaticSerializerDispatch.ReadDataObject, - StaticSerializerDispatch.WriteObject, - StaticSerializerDispatch.ReadObject, - StaticSerializerDispatch.WriteTypeInfo, - StaticSerializerDispatch.ReadTypeInfo, - StaticSerializerDispatch.CompatibleTypeMetaFields, - typed); + return new TSerializer(); } - public static SerializerBinding Create(Type valueType, Type serializerType) + public static SerializerBinding Create(Type serializerType) { - MethodInfo method = CreateGenericMethod.MakeGenericMethod(valueType, serializerType); - try + if (!typeof(SerializerBinding).IsAssignableFrom(serializerType)) { - return (SerializerBinding)method.Invoke(null, null)!; - } - catch (TargetInvocationException ex) when (ex.InnerException is not null) - { - throw ex.InnerException; + throw new InvalidDataException($"{serializerType} is not a serializer binding"); } + + return Ctors.GetOrAdd(serializerType, BuildCtor)(); } - private static SerializerBinding CreateGeneric() - where TSerializer : IStaticSerializer + private static Func BuildCtor(Type serializerType) { - return Create(); + try + { + NewExpression body = Expression.New(serializerType); + return Expression.Lambda>(body).Compile(); + } + catch (Exception ex) + { + throw new InvalidDataException($"failed to build serializer constructor for {serializerType}: {ex.Message}"); + } } } diff --git a/csharp/src/Fory/UnionSerializer.cs b/csharp/src/Fory/UnionSerializer.cs index d884ef6efe..4b965866c8 100644 --- a/csharp/src/Fory/UnionSerializer.cs +++ b/csharp/src/Fory/UnionSerializer.cs @@ -20,25 +20,25 @@ namespace Apache.Fory; -public readonly struct UnionSerializer : IStaticSerializer, TUnion> +public sealed class UnionSerializer : TypedSerializer where TUnion : Union { private static readonly Func Factory = BuildFactory(); - public static TypeId StaticTypeId => TypeId.TypedUnion; + public override TypeId StaticTypeId => TypeId.TypedUnion; - public static bool IsNullableType => true; + public override bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; + public override bool IsReferenceTrackableType => true; - public static TUnion DefaultValue => null!; + public override TUnion DefaultValue => null!; - public static bool IsNone(in TUnion value) + public override bool IsNone(in TUnion value) { return value is null; } - public static void WriteData(ref WriteContext context, in TUnion value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in TUnion value, bool hasGenerics) { _ = hasGenerics; if (value is null) @@ -50,7 +50,7 @@ public static void WriteData(ref WriteContext context, in TUnion value, bool has DynamicAnyCodec.WriteAny(ref context, value.Value, RefMode.Tracking, true, false); } - public static TUnion ReadData(ref ReadContext context) + public override TUnion ReadData(ref ReadContext context) { uint rawCaseId = context.Reader.ReadVarUInt32(); if (rawCaseId > int.MaxValue) diff --git a/csharp/tests/Fory.XlangPeer/Program.cs b/csharp/tests/Fory.XlangPeer/Program.cs index 526b1fb3b8..132adf91c6 100644 --- a/csharp/tests/Fory.XlangPeer/Program.cs +++ b/csharp/tests/Fory.XlangPeer/Program.cs @@ -1025,21 +1025,21 @@ public sealed class MyExt public int Id { get; set; } } -public readonly struct MyExtSerializer : IStaticSerializer +public sealed class MyExtSerializer : TypedSerializer { - public static TypeId StaticTypeId => TypeId.Ext; - public static bool IsNullableType => true; - public static bool IsReferenceTrackableType => true; - public static MyExt DefaultValue => null!; - public static bool IsNone(in MyExt value) => value is null; + public override TypeId StaticTypeId => TypeId.Ext; + public override bool IsNullableType => true; + public override bool IsReferenceTrackableType => true; + public override MyExt DefaultValue => null!; + public override bool IsNone(in MyExt value) => value is null; - public static void WriteData(ref WriteContext context, in MyExt value, bool hasGenerics) + public override void WriteData(ref WriteContext context, in MyExt value, bool hasGenerics) { _ = hasGenerics; context.Writer.WriteVarInt32((value ?? new MyExt()).Id); } - public static MyExt ReadData(ref ReadContext context) + public override MyExt ReadData(ref ReadContext context) { return new MyExt { Id = context.Reader.ReadVarInt32() }; } From 0f5267826b1f427e72a27adabf342f813e80a986 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 22 Feb 2026 13:22:58 +0800 Subject: [PATCH 13/16] refactor(csharp): move type info logic to resolver and split array serializers --- csharp/src/Fory/CollectionSerializers.cs | 360 +------------- csharp/src/Fory/PrimitiveArraySerializers.cs | 418 ++++++++++++++++ csharp/src/Fory/Serializer.cs | 438 ----------------- csharp/src/Fory/TypeResolver.cs | 491 +++++++++++++++++++ csharp/src/Fory/TypedSerializerBinding.cs | 4 +- 5 files changed, 912 insertions(+), 799 deletions(-) create mode 100644 csharp/src/Fory/PrimitiveArraySerializers.cs diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index 8a51540f09..b1bb50a10a 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -391,356 +391,9 @@ private static bool IsSet(Type valueType) } } -internal static class PrimitiveArrayCodec -{ - public static TypeId? PrimitiveArrayTypeId(Type elementType) - { - if (elementType == typeof(byte)) - { - return TypeId.Binary; - } - - if (elementType == typeof(bool)) - { - return TypeId.BoolArray; - } - - if (elementType == typeof(sbyte)) - { - return TypeId.Int8Array; - } - - if (elementType == typeof(short)) - { - return TypeId.Int16Array; - } - - if (elementType == typeof(int)) - { - return TypeId.Int32Array; - } - - if (elementType == typeof(long)) - { - return TypeId.Int64Array; - } - - if (elementType == typeof(ushort)) - { - return TypeId.UInt16Array; - } - - if (elementType == typeof(uint)) - { - return TypeId.UInt32Array; - } - - if (elementType == typeof(ulong)) - { - return TypeId.UInt64Array; - } - - if (elementType == typeof(float)) - { - return TypeId.Float32Array; - } - - if (elementType == typeof(double)) - { - return TypeId.Float64Array; - } - - return null; - } - - public static void WritePrimitiveArray(T[] value, ref WriteContext context) - { - if (typeof(T) == typeof(byte)) - { - byte[] bytes = (byte[])(object)value; - context.Writer.WriteVarUInt32((uint)bytes.Length); - context.Writer.WriteBytes(bytes); - return; - } - - if (typeof(T) == typeof(bool)) - { - bool[] values = (bool[])(object)value; - context.Writer.WriteVarUInt32((uint)values.Length); - foreach (bool item in values) - { - context.Writer.WriteUInt8(item ? (byte)1 : (byte)0); - } - - return; - } - - if (typeof(T) == typeof(sbyte)) - { - sbyte[] values = (sbyte[])(object)value; - context.Writer.WriteVarUInt32((uint)values.Length); - foreach (sbyte item in values) - { - context.Writer.WriteInt8(item); - } - - return; - } - - if (typeof(T) == typeof(short)) - { - short[] values = (short[])(object)value; - context.Writer.WriteVarUInt32((uint)(values.Length * 2)); - foreach (short item in values) - { - context.Writer.WriteInt16(item); - } - - return; - } - - if (typeof(T) == typeof(int)) - { - int[] values = (int[])(object)value; - context.Writer.WriteVarUInt32((uint)(values.Length * 4)); - foreach (int item in values) - { - context.Writer.WriteInt32(item); - } - - return; - } - - if (typeof(T) == typeof(uint)) - { - uint[] values = (uint[])(object)value; - context.Writer.WriteVarUInt32((uint)(values.Length * 4)); - foreach (uint item in values) - { - context.Writer.WriteUInt32(item); - } - - return; - } - - if (typeof(T) == typeof(long)) - { - long[] values = (long[])(object)value; - context.Writer.WriteVarUInt32((uint)(values.Length * 8)); - foreach (long item in values) - { - context.Writer.WriteInt64(item); - } - - return; - } - - if (typeof(T) == typeof(ulong)) - { - ulong[] values = (ulong[])(object)value; - context.Writer.WriteVarUInt32((uint)(values.Length * 8)); - foreach (ulong item in values) - { - context.Writer.WriteUInt64(item); - } - - return; - } - - if (typeof(T) == typeof(ushort)) - { - ushort[] values = (ushort[])(object)value; - context.Writer.WriteVarUInt32((uint)(values.Length * 2)); - foreach (ushort item in values) - { - context.Writer.WriteUInt16(item); - } - - return; - } - - if (typeof(T) == typeof(float)) - { - float[] values = (float[])(object)value; - context.Writer.WriteVarUInt32((uint)(values.Length * 4)); - foreach (float item in values) - { - context.Writer.WriteFloat32(item); - } - - return; - } - - double[] doubles = (double[])(object)value; - context.Writer.WriteVarUInt32((uint)(doubles.Length * 8)); - foreach (double item in doubles) - { - context.Writer.WriteFloat64(item); - } - } - - public static T[] ReadPrimitiveArray(ref ReadContext context) - { - int payloadSize = checked((int)context.Reader.ReadVarUInt32()); - if (typeof(T) == typeof(byte)) - { - return (T[])(object)context.Reader.ReadBytes(payloadSize); - } - - if (typeof(T) == typeof(bool)) - { - bool[] outValues = new bool[payloadSize]; - for (int i = 0; i < payloadSize; i++) - { - outValues[i] = context.Reader.ReadUInt8() != 0; - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(sbyte)) - { - sbyte[] outValues = new sbyte[payloadSize]; - for (int i = 0; i < payloadSize; i++) - { - outValues[i] = context.Reader.ReadInt8(); - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(short)) - { - if ((payloadSize & 1) != 0) - { - throw new InvalidDataException("int16 array payload size mismatch"); - } - - short[] outValues = new short[payloadSize / 2]; - for (int i = 0; i < outValues.Length; i++) - { - outValues[i] = context.Reader.ReadInt16(); - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(int)) - { - if ((payloadSize & 3) != 0) - { - throw new InvalidDataException("int32 array payload size mismatch"); - } - - int[] outValues = new int[payloadSize / 4]; - for (int i = 0; i < outValues.Length; i++) - { - outValues[i] = context.Reader.ReadInt32(); - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(uint)) - { - if ((payloadSize & 3) != 0) - { - throw new InvalidDataException("uint32 array payload size mismatch"); - } - - uint[] outValues = new uint[payloadSize / 4]; - for (int i = 0; i < outValues.Length; i++) - { - outValues[i] = context.Reader.ReadUInt32(); - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(long)) - { - if ((payloadSize & 7) != 0) - { - throw new InvalidDataException("int64 array payload size mismatch"); - } - - long[] outValues = new long[payloadSize / 8]; - for (int i = 0; i < outValues.Length; i++) - { - outValues[i] = context.Reader.ReadInt64(); - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(ulong)) - { - if ((payloadSize & 7) != 0) - { - throw new InvalidDataException("uint64 array payload size mismatch"); - } - - ulong[] outValues = new ulong[payloadSize / 8]; - for (int i = 0; i < outValues.Length; i++) - { - outValues[i] = context.Reader.ReadUInt64(); - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(ushort)) - { - if ((payloadSize & 1) != 0) - { - throw new InvalidDataException("uint16 array payload size mismatch"); - } - - ushort[] outValues = new ushort[payloadSize / 2]; - for (int i = 0; i < outValues.Length; i++) - { - outValues[i] = context.Reader.ReadUInt16(); - } - - return (T[])(object)outValues; - } - - if (typeof(T) == typeof(float)) - { - if ((payloadSize & 3) != 0) - { - throw new InvalidDataException("float32 array payload size mismatch"); - } - - float[] outValues = new float[payloadSize / 4]; - for (int i = 0; i < outValues.Length; i++) - { - outValues[i] = context.Reader.ReadFloat32(); - } - - return (T[])(object)outValues; - } - - if ((payloadSize & 7) != 0) - { - throw new InvalidDataException("float64 array payload size mismatch"); - } - - double[] doubles = new double[payloadSize / 8]; - for (int i = 0; i < doubles.Length; i++) - { - doubles[i] = context.Reader.ReadFloat64(); - } - - return (T[])(object)doubles; - } -} - public sealed class ArraySerializer : TypedSerializer { - private static readonly TypeId? PrimitiveArrayTypeId = PrimitiveArrayCodec.PrimitiveArrayTypeId(typeof(T)); - - public override TypeId StaticTypeId => PrimitiveArrayTypeId ?? TypeId.List; + public override TypeId StaticTypeId => TypeId.List; public override bool IsNullableType => true; public override bool IsReferenceTrackableType => true; public override T[] DefaultValue => null!; @@ -749,12 +402,6 @@ public sealed class ArraySerializer : TypedSerializer public override void WriteData(ref WriteContext context, in T[] value, bool hasGenerics) { T[] safe = value ?? []; - if (PrimitiveArrayTypeId is not null) - { - PrimitiveArrayCodec.WritePrimitiveArray(safe, ref context); - return; - } - CollectionCodec.WriteCollectionData( safe, context.TypeResolver.GetSerializer(), @@ -764,11 +411,6 @@ public override void WriteData(ref WriteContext context, in T[] value, bool hasG public override T[] ReadData(ref ReadContext context) { - if (PrimitiveArrayTypeId is not null) - { - return PrimitiveArrayCodec.ReadPrimitiveArray(ref context); - } - List values = CollectionCodec.ReadCollectionData(context.TypeResolver.GetSerializer(), ref context); return values.ToArray(); } diff --git a/csharp/src/Fory/PrimitiveArraySerializers.cs b/csharp/src/Fory/PrimitiveArraySerializers.cs new file mode 100644 index 0000000000..47ef1a398c --- /dev/null +++ b/csharp/src/Fory/PrimitiveArraySerializers.cs @@ -0,0 +1,418 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +internal sealed class BoolArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.BoolArray; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override bool[] DefaultValue => null!; + + public override bool IsNone(in bool[] value) => value is null; + + public override void WriteData(ref WriteContext context, in bool[] value, bool hasGenerics) + { + _ = hasGenerics; + bool[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)safe.Length); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteUInt8(safe[i] ? (byte)1 : (byte)0); + } + } + + public override bool[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + bool[] values = new bool[payloadSize]; + for (int i = 0; i < payloadSize; i++) + { + values[i] = context.Reader.ReadUInt8() != 0; + } + + return values; + } +} + +internal sealed class Int8ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.Int8Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override sbyte[] DefaultValue => null!; + + public override bool IsNone(in sbyte[] value) => value is null; + + public override void WriteData(ref WriteContext context, in sbyte[] value, bool hasGenerics) + { + _ = hasGenerics; + sbyte[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)safe.Length); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteInt8(safe[i]); + } + } + + public override sbyte[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + sbyte[] values = new sbyte[payloadSize]; + for (int i = 0; i < payloadSize; i++) + { + values[i] = context.Reader.ReadInt8(); + } + + return values; + } +} + +internal sealed class Int16ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.Int16Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override short[] DefaultValue => null!; + + public override bool IsNone(in short[] value) => value is null; + + public override void WriteData(ref WriteContext context, in short[] value, bool hasGenerics) + { + _ = hasGenerics; + short[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 2)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteInt16(safe[i]); + } + } + + public override short[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 1) != 0) + { + throw new InvalidDataException("int16 array payload size mismatch"); + } + + short[] values = new short[payloadSize / 2]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadInt16(); + } + + return values; + } +} + +internal sealed class Int32ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.Int32Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override int[] DefaultValue => null!; + + public override bool IsNone(in int[] value) => value is null; + + public override void WriteData(ref WriteContext context, in int[] value, bool hasGenerics) + { + _ = hasGenerics; + int[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 4)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteInt32(safe[i]); + } + } + + public override int[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 3) != 0) + { + throw new InvalidDataException("int32 array payload size mismatch"); + } + + int[] values = new int[payloadSize / 4]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadInt32(); + } + + return values; + } +} + +internal sealed class Int64ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.Int64Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override long[] DefaultValue => null!; + + public override bool IsNone(in long[] value) => value is null; + + public override void WriteData(ref WriteContext context, in long[] value, bool hasGenerics) + { + _ = hasGenerics; + long[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 8)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteInt64(safe[i]); + } + } + + public override long[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 7) != 0) + { + throw new InvalidDataException("int64 array payload size mismatch"); + } + + long[] values = new long[payloadSize / 8]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadInt64(); + } + + return values; + } +} + +internal sealed class UInt16ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.UInt16Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override ushort[] DefaultValue => null!; + + public override bool IsNone(in ushort[] value) => value is null; + + public override void WriteData(ref WriteContext context, in ushort[] value, bool hasGenerics) + { + _ = hasGenerics; + ushort[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 2)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteUInt16(safe[i]); + } + } + + public override ushort[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 1) != 0) + { + throw new InvalidDataException("uint16 array payload size mismatch"); + } + + ushort[] values = new ushort[payloadSize / 2]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadUInt16(); + } + + return values; + } +} + +internal sealed class UInt32ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.UInt32Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override uint[] DefaultValue => null!; + + public override bool IsNone(in uint[] value) => value is null; + + public override void WriteData(ref WriteContext context, in uint[] value, bool hasGenerics) + { + _ = hasGenerics; + uint[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 4)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteUInt32(safe[i]); + } + } + + public override uint[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 3) != 0) + { + throw new InvalidDataException("uint32 array payload size mismatch"); + } + + uint[] values = new uint[payloadSize / 4]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadUInt32(); + } + + return values; + } +} + +internal sealed class UInt64ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.UInt64Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override ulong[] DefaultValue => null!; + + public override bool IsNone(in ulong[] value) => value is null; + + public override void WriteData(ref WriteContext context, in ulong[] value, bool hasGenerics) + { + _ = hasGenerics; + ulong[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 8)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteUInt64(safe[i]); + } + } + + public override ulong[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 7) != 0) + { + throw new InvalidDataException("uint64 array payload size mismatch"); + } + + ulong[] values = new ulong[payloadSize / 8]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadUInt64(); + } + + return values; + } +} + +internal sealed class Float32ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.Float32Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override float[] DefaultValue => null!; + + public override bool IsNone(in float[] value) => value is null; + + public override void WriteData(ref WriteContext context, in float[] value, bool hasGenerics) + { + _ = hasGenerics; + float[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 4)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteFloat32(safe[i]); + } + } + + public override float[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 3) != 0) + { + throw new InvalidDataException("float32 array payload size mismatch"); + } + + float[] values = new float[payloadSize / 4]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadFloat32(); + } + + return values; + } +} + +internal sealed class Float64ArraySerializer : TypedSerializer +{ + public override TypeId StaticTypeId => TypeId.Float64Array; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override double[] DefaultValue => null!; + + public override bool IsNone(in double[] value) => value is null; + + public override void WriteData(ref WriteContext context, in double[] value, bool hasGenerics) + { + _ = hasGenerics; + double[] safe = value ?? []; + context.Writer.WriteVarUInt32((uint)(safe.Length * 8)); + for (int i = 0; i < safe.Length; i++) + { + context.Writer.WriteFloat64(safe[i]); + } + } + + public override double[] ReadData(ref ReadContext context) + { + int payloadSize = checked((int)context.Reader.ReadVarUInt32()); + if ((payloadSize & 7) != 0) + { + throw new InvalidDataException("float64 array payload size mismatch"); + } + + double[] values = new double[payloadSize / 8]; + for (int i = 0; i < values.Length; i++) + { + values[i] = context.Reader.ReadFloat64(); + } + + return values; + } +} diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs index 7ff987f8d5..1af1fdf6c8 100644 --- a/csharp/src/Fory/Serializer.cs +++ b/csharp/src/Fory/Serializer.cs @@ -101,441 +101,3 @@ public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) return SerializerImpl.CompatibleTypeMetaFields(trackRef); } } - -internal static class SerializerTypeInfo -{ - public static void WriteTypeInfo(Type type, SerializerBinding serializer, ref WriteContext context) - { - TypeId staticTypeId = serializer.StaticTypeId; - if (!staticTypeId.IsUserTypeKind()) - { - context.Writer.WriteUInt8((byte)staticTypeId); - return; - } - - RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); - TypeId wireTypeId = ResolveWireTypeId(info.Kind, info.RegisterByName, context.Compatible); - context.Writer.WriteUInt8((byte)wireTypeId); - switch (wireTypeId) - { - case TypeId.CompatibleStruct: - case TypeId.NamedCompatibleStruct: - { - TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); - context.WriteCompatibleTypeMeta(type, typeMeta); - return; - } - case TypeId.NamedEnum: - case TypeId.NamedStruct: - case TypeId.NamedExt: - case TypeId.NamedUnion: - { - if (context.Compatible) - { - TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); - context.WriteCompatibleTypeMeta(type, typeMeta); - } - else - { - if (info.NamespaceName is null) - { - throw new InvalidDataException("missing namespace metadata for name-registered type"); - } - - WriteMetaString( - ref context, - info.NamespaceName.Value, - TypeMetaEncodings.NamespaceMetaStringEncodings, - MetaStringEncoder.Namespace); - WriteMetaString( - ref context, - info.TypeName, - TypeMetaEncodings.TypeNameMetaStringEncodings, - MetaStringEncoder.TypeName); - } - - return; - } - default: - if (!info.RegisterByName && WireTypeNeedsUserTypeId(wireTypeId)) - { - if (!info.UserTypeId.HasValue) - { - throw new InvalidDataException("missing user type id for id-registered type"); - } - - context.Writer.WriteVarUInt32(info.UserTypeId.Value); - } - - return; - } - } - - public static void ReadTypeInfo(Type type, SerializerBinding serializer, ref ReadContext context) - { - uint rawTypeId = context.Reader.ReadVarUInt32(); - if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) - { - throw new InvalidDataException($"unknown type id {rawTypeId}"); - } - - TypeId typeId = (TypeId)rawTypeId; - TypeId staticTypeId = serializer.StaticTypeId; - if (!staticTypeId.IsUserTypeKind()) - { - if (typeId != staticTypeId) - { - throw new TypeMismatchException((uint)staticTypeId, rawTypeId); - } - - return; - } - - RegisteredTypeInfo info = context.TypeResolver.RequireRegisteredTypeInfo(type); - HashSet allowed = AllowedWireTypeIds(info.Kind, info.RegisterByName, context.Compatible); - if (!allowed.Contains(typeId)) - { - uint expected = allowed.Count > 0 ? (uint)allowed.First() : 0; - throw new TypeMismatchException(expected, rawTypeId); - } - - switch (typeId) - { - case TypeId.CompatibleStruct: - case TypeId.NamedCompatibleStruct: - { - TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); - ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); - context.PushCompatibleTypeMeta(type, remoteTypeMeta); - return; - } - case TypeId.NamedEnum: - case TypeId.NamedStruct: - case TypeId.NamedExt: - case TypeId.NamedUnion: - { - if (context.Compatible) - { - TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); - ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); - if (typeId == TypeId.NamedStruct) - { - context.PushCompatibleTypeMeta(type, remoteTypeMeta); - } - } - else - { - MetaString namespaceName = ReadMetaString( - ref context, - MetaStringDecoder.Namespace, - TypeMetaEncodings.NamespaceMetaStringEncodings); - MetaString typeName = ReadMetaString( - ref context, - MetaStringDecoder.TypeName, - TypeMetaEncodings.TypeNameMetaStringEncodings); - if (!info.RegisterByName || info.NamespaceName is null) - { - throw new InvalidDataException("received name-registered type info for id-registered local type"); - } - - if (namespaceName.Value != info.NamespaceName.Value.Value || typeName.Value != info.TypeName.Value) - { - throw new InvalidDataException( - $"type name mismatch: expected {info.NamespaceName.Value.Value}::{info.TypeName.Value}, got {namespaceName.Value}::{typeName.Value}"); - } - } - - return; - } - default: - if (!info.RegisterByName && WireTypeNeedsUserTypeId(typeId)) - { - if (!info.UserTypeId.HasValue) - { - throw new InvalidDataException("missing user type id for id-registered local type"); - } - - uint remoteUserTypeId = context.Reader.ReadVarUInt32(); - if (remoteUserTypeId != info.UserTypeId.Value) - { - throw new TypeMismatchException(info.UserTypeId.Value, remoteUserTypeId); - } - } - - return; - } - } - - public static TypeId ResolveWireTypeId(TypeId declaredKind, bool registerByName, bool compatible) - { - TypeId baseKind = NormalizeBaseKind(declaredKind); - if (registerByName) - { - return NamedKind(baseKind, compatible); - } - - return IdKind(baseKind, compatible); - } - - public static HashSet AllowedWireTypeIds(TypeId declaredKind, bool registerByName, bool compatible) - { - TypeId baseKind = NormalizeBaseKind(declaredKind); - TypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible); - HashSet allowed = [expected]; - if (baseKind == TypeId.Struct && compatible) - { - allowed.Add(TypeId.CompatibleStruct); - allowed.Add(TypeId.NamedCompatibleStruct); - allowed.Add(TypeId.Struct); - allowed.Add(TypeId.NamedStruct); - } - - return allowed; - } - - private static TypeId NormalizeBaseKind(TypeId kind) - { - return kind switch - { - TypeId.NamedEnum => TypeId.Enum, - TypeId.CompatibleStruct or TypeId.NamedCompatibleStruct or TypeId.NamedStruct => TypeId.Struct, - TypeId.NamedExt => TypeId.Ext, - TypeId.NamedUnion => TypeId.TypedUnion, - _ => kind, - }; - } - - private static TypeId NamedKind(TypeId baseKind, bool compatible) - { - return baseKind switch - { - TypeId.Struct => compatible ? TypeId.NamedCompatibleStruct : TypeId.NamedStruct, - TypeId.Enum => TypeId.NamedEnum, - TypeId.Ext => TypeId.NamedExt, - TypeId.TypedUnion => TypeId.NamedUnion, - _ => baseKind, - }; - } - - private static TypeId IdKind(TypeId baseKind, bool compatible) - { - return baseKind switch - { - TypeId.Struct => compatible ? TypeId.CompatibleStruct : TypeId.Struct, - _ => baseKind, - }; - } - - private static bool WireTypeNeedsUserTypeId(TypeId typeId) - { - return typeId is TypeId.Enum or TypeId.Struct or TypeId.Ext or TypeId.TypedUnion; - } - - private static TypeMeta BuildCompatibleTypeMeta( - RegisteredTypeInfo info, - TypeId wireTypeId, - bool trackRef) - { - IReadOnlyList fields = info.Serializer.CompatibleTypeMetaFields(trackRef); - bool hasFieldsMeta = fields.Count > 0; - if (info.RegisterByName) - { - if (info.NamespaceName is null) - { - throw new InvalidDataException("missing namespace metadata for name-registered type"); - } - - return new TypeMeta( - (uint)wireTypeId, - null, - info.NamespaceName.Value, - info.TypeName, - true, - fields, - hasFieldsMeta); - } - - if (!info.UserTypeId.HasValue) - { - throw new InvalidDataException("missing user type id metadata for id-registered type"); - } - - return new TypeMeta( - (uint)wireTypeId, - info.UserTypeId.Value, - MetaString.Empty('.', '_'), - MetaString.Empty('$', '_'), - false, - fields, - hasFieldsMeta); - } - - private static void ValidateCompatibleTypeMeta( - TypeMeta remoteTypeMeta, - RegisteredTypeInfo localInfo, - HashSet expectedWireTypes, - TypeId actualWireTypeId) - { - if (remoteTypeMeta.RegisterByName) - { - if (!localInfo.RegisterByName || localInfo.NamespaceName is null) - { - throw new InvalidDataException( - "received name-registered compatible metadata for id-registered local type"); - } - - if (remoteTypeMeta.NamespaceName.Value != localInfo.NamespaceName.Value.Value) - { - throw new InvalidDataException( - $"namespace mismatch: expected {localInfo.NamespaceName.Value.Value}, got {remoteTypeMeta.NamespaceName.Value}"); - } - - if (remoteTypeMeta.TypeName.Value != localInfo.TypeName.Value) - { - throw new InvalidDataException( - $"type name mismatch: expected {localInfo.TypeName.Value}, got {remoteTypeMeta.TypeName.Value}"); - } - } - else - { - if (localInfo.RegisterByName) - { - throw new InvalidDataException( - "received id-registered compatible metadata for name-registered local type"); - } - - if (!remoteTypeMeta.UserTypeId.HasValue) - { - throw new InvalidDataException("missing user type id in compatible type metadata"); - } - - if (!localInfo.UserTypeId.HasValue) - { - throw new InvalidDataException("missing local user type id metadata for id-registered type"); - } - - if (remoteTypeMeta.UserTypeId.Value != localInfo.UserTypeId.Value) - { - throw new TypeMismatchException(localInfo.UserTypeId.Value, remoteTypeMeta.UserTypeId.Value); - } - } - - if (remoteTypeMeta.TypeId.HasValue && - Enum.IsDefined(typeof(TypeId), remoteTypeMeta.TypeId.Value)) - { - TypeId remoteWireTypeId = (TypeId)remoteTypeMeta.TypeId.Value; - if (!expectedWireTypes.Contains(remoteWireTypeId)) - { - throw new TypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value); - } - } - } - - private static void WriteMetaString( - ref WriteContext context, - MetaString value, - IReadOnlyList encodings, - MetaStringEncoder encoder) - { - MetaString normalized = encodings.Contains(value.Encoding) - ? value - : encoder.Encode(value.Value, encodings); - if (!encodings.Contains(normalized.Encoding)) - { - throw new EncodingException("failed to normalize meta string encoding"); - } - - byte[] bytes = normalized.Bytes; - (uint index, bool isNew) = context.MetaStringWriteState.AssignIndexIfAbsent(normalized); - if (isNew) - { - context.Writer.WriteVarUInt32((uint)(bytes.Length << 1)); - if (bytes.Length > 16) - { - context.Writer.WriteInt64(unchecked((long)JavaMetaStringHash(normalized))); - } - else if (bytes.Length > 0) - { - context.Writer.WriteUInt8((byte)normalized.Encoding); - } - - context.Writer.WriteBytes(bytes); - } - else - { - context.Writer.WriteVarUInt32(((index + 1) << 1) | 1); - } - } - - private static MetaString ReadMetaString( - ref ReadContext context, - MetaStringDecoder decoder, - IReadOnlyList encodings) - { - uint header = context.Reader.ReadVarUInt32(); - int length = checked((int)(header >> 1)); - bool isRef = (header & 1) == 1; - if (isRef) - { - int index = length - 1; - MetaString? cached = context.MetaStringReadState.ValueAt(index); - if (cached is null) - { - throw new InvalidDataException($"unknown meta string ref index {index}"); - } - - return cached.Value; - } - - MetaString value; - if (length == 0) - { - value = MetaString.Empty(decoder.SpecialChar1, decoder.SpecialChar2); - } - else - { - MetaStringEncoding encoding; - if (length > 16) - { - long hash = context.Reader.ReadInt64(); - byte rawEncoding = unchecked((byte)(hash & 0xFF)); - encoding = (MetaStringEncoding)rawEncoding; - } - else - { - encoding = (MetaStringEncoding)context.Reader.ReadUInt8(); - } - - if (!encodings.Contains(encoding)) - { - throw new InvalidDataException($"meta string encoding {encoding} not allowed in this context"); - } - - byte[] bytes = context.Reader.ReadBytes(length); - value = decoder.Decode(bytes, encoding); - } - - context.MetaStringReadState.Append(value); - return value; - } - - private static ulong JavaMetaStringHash(MetaString metaString) - { - (ulong h1, _) = MurmurHash3.X64_128(metaString.Bytes, 47); - long hash = unchecked((long)h1); - if (hash != long.MinValue) - { - hash = Math.Abs(hash); - } - - ulong result = unchecked((ulong)hash); - if (result == 0) - { - result += 256; - } - - result &= 0xffffffffffffff00; - result |= (byte)metaString.Encoding; - return result; - } -} diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index 03bc7cd3c1..ff9d6143f6 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -246,6 +246,201 @@ internal RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) throw new TypeNotRegisteredException($"{type} is not registered"); } + internal void WriteTypeInfo(Type type, SerializerBinding serializer, ref WriteContext context) + { + TypeId staticTypeId = serializer.StaticTypeId; + if (!staticTypeId.IsUserTypeKind()) + { + context.Writer.WriteUInt8((byte)staticTypeId); + return; + } + + RegisteredTypeInfo info = RequireRegisteredTypeInfo(type); + TypeId wireTypeId = ResolveWireTypeId(info.Kind, info.RegisterByName, context.Compatible); + context.Writer.WriteUInt8((byte)wireTypeId); + switch (wireTypeId) + { + case TypeId.CompatibleStruct: + case TypeId.NamedCompatibleStruct: + { + TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); + context.WriteCompatibleTypeMeta(type, typeMeta); + return; + } + case TypeId.NamedEnum: + case TypeId.NamedStruct: + case TypeId.NamedExt: + case TypeId.NamedUnion: + { + if (context.Compatible) + { + TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef); + context.WriteCompatibleTypeMeta(type, typeMeta); + } + else + { + if (info.NamespaceName is null) + { + throw new InvalidDataException("missing namespace metadata for name-registered type"); + } + + WriteMetaString( + ref context, + info.NamespaceName.Value, + TypeMetaEncodings.NamespaceMetaStringEncodings, + MetaStringEncoder.Namespace); + WriteMetaString( + ref context, + info.TypeName, + TypeMetaEncodings.TypeNameMetaStringEncodings, + MetaStringEncoder.TypeName); + } + + return; + } + default: + if (!info.RegisterByName && WireTypeNeedsUserTypeId(wireTypeId)) + { + if (!info.UserTypeId.HasValue) + { + throw new InvalidDataException("missing user type id for id-registered type"); + } + + context.Writer.WriteVarUInt32(info.UserTypeId.Value); + } + + return; + } + } + + internal void ReadTypeInfo(Type type, SerializerBinding serializer, ref ReadContext context) + { + uint rawTypeId = context.Reader.ReadVarUInt32(); + if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) + { + throw new InvalidDataException($"unknown type id {rawTypeId}"); + } + + TypeId typeId = (TypeId)rawTypeId; + TypeId staticTypeId = serializer.StaticTypeId; + if (!staticTypeId.IsUserTypeKind()) + { + if (typeId != staticTypeId) + { + throw new TypeMismatchException((uint)staticTypeId, rawTypeId); + } + + return; + } + + RegisteredTypeInfo info = RequireRegisteredTypeInfo(type); + HashSet allowed = AllowedWireTypeIds(info.Kind, info.RegisterByName, context.Compatible); + if (!allowed.Contains(typeId)) + { + uint expected = 0; + foreach (TypeId allowedType in allowed) + { + expected = (uint)allowedType; + break; + } + + throw new TypeMismatchException(expected, rawTypeId); + } + + switch (typeId) + { + case TypeId.CompatibleStruct: + case TypeId.NamedCompatibleStruct: + { + TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); + ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); + context.PushCompatibleTypeMeta(type, remoteTypeMeta); + return; + } + case TypeId.NamedEnum: + case TypeId.NamedStruct: + case TypeId.NamedExt: + case TypeId.NamedUnion: + { + if (context.Compatible) + { + TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta(); + ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId); + if (typeId == TypeId.NamedStruct) + { + context.PushCompatibleTypeMeta(type, remoteTypeMeta); + } + } + else + { + MetaString namespaceName = ReadMetaString( + ref context, + MetaStringDecoder.Namespace, + TypeMetaEncodings.NamespaceMetaStringEncodings); + MetaString typeName = ReadMetaString( + ref context, + MetaStringDecoder.TypeName, + TypeMetaEncodings.TypeNameMetaStringEncodings); + if (!info.RegisterByName || info.NamespaceName is null) + { + throw new InvalidDataException("received name-registered type info for id-registered local type"); + } + + if (namespaceName.Value != info.NamespaceName.Value.Value || typeName.Value != info.TypeName.Value) + { + throw new InvalidDataException( + $"type name mismatch: expected {info.NamespaceName.Value.Value}::{info.TypeName.Value}, got {namespaceName.Value}::{typeName.Value}"); + } + } + + return; + } + default: + if (!info.RegisterByName && WireTypeNeedsUserTypeId(typeId)) + { + if (!info.UserTypeId.HasValue) + { + throw new InvalidDataException("missing user type id for id-registered local type"); + } + + uint remoteUserTypeId = context.Reader.ReadVarUInt32(); + if (remoteUserTypeId != info.UserTypeId.Value) + { + throw new TypeMismatchException(info.UserTypeId.Value, remoteUserTypeId); + } + } + + return; + } + } + + internal static TypeId ResolveWireTypeId(TypeId declaredKind, bool registerByName, bool compatible) + { + TypeId baseKind = NormalizeBaseKind(declaredKind); + if (registerByName) + { + return NamedKind(baseKind, compatible); + } + + return IdKind(baseKind, compatible); + } + + internal static HashSet AllowedWireTypeIds(TypeId declaredKind, bool registerByName, bool compatible) + { + TypeId baseKind = NormalizeBaseKind(declaredKind); + TypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible); + HashSet allowed = [expected]; + if (baseKind == TypeId.Struct && compatible) + { + allowed.Add(TypeId.CompatibleStruct); + allowed.Add(TypeId.NamedCompatibleStruct); + allowed.Add(TypeId.Struct); + allowed.Add(TypeId.NamedStruct); + } + + return allowed; + } + public object? ReadByUserTypeId(uint userTypeId, ref ReadContext context, TypeMeta? compatibleTypeMeta = null) { if (!_byUserTypeId.TryGetValue(userTypeId, out TypeReader? entry)) @@ -483,6 +678,252 @@ private DynamicRegistrationMode DynamicRegistrationModeFor(TypeId kind) throw new TypeNotRegisteredException($"no dynamic registration mode for kind {kind}"); } + private static TypeId NormalizeBaseKind(TypeId kind) + { + return kind switch + { + TypeId.NamedEnum => TypeId.Enum, + TypeId.CompatibleStruct or TypeId.NamedCompatibleStruct or TypeId.NamedStruct => TypeId.Struct, + TypeId.NamedExt => TypeId.Ext, + TypeId.NamedUnion => TypeId.TypedUnion, + _ => kind, + }; + } + + private static TypeId NamedKind(TypeId baseKind, bool compatible) + { + return baseKind switch + { + TypeId.Struct => compatible ? TypeId.NamedCompatibleStruct : TypeId.NamedStruct, + TypeId.Enum => TypeId.NamedEnum, + TypeId.Ext => TypeId.NamedExt, + TypeId.TypedUnion => TypeId.NamedUnion, + _ => baseKind, + }; + } + + private static TypeId IdKind(TypeId baseKind, bool compatible) + { + return baseKind switch + { + TypeId.Struct => compatible ? TypeId.CompatibleStruct : TypeId.Struct, + _ => baseKind, + }; + } + + private static bool WireTypeNeedsUserTypeId(TypeId typeId) + { + return typeId is TypeId.Enum or TypeId.Struct or TypeId.Ext or TypeId.TypedUnion; + } + + private static TypeMeta BuildCompatibleTypeMeta( + RegisteredTypeInfo info, + TypeId wireTypeId, + bool trackRef) + { + IReadOnlyList fields = info.Serializer.CompatibleTypeMetaFields(trackRef); + bool hasFieldsMeta = fields.Count > 0; + if (info.RegisterByName) + { + if (info.NamespaceName is null) + { + throw new InvalidDataException("missing namespace metadata for name-registered type"); + } + + return new TypeMeta( + (uint)wireTypeId, + null, + info.NamespaceName.Value, + info.TypeName, + true, + fields, + hasFieldsMeta); + } + + if (!info.UserTypeId.HasValue) + { + throw new InvalidDataException("missing user type id metadata for id-registered type"); + } + + return new TypeMeta( + (uint)wireTypeId, + info.UserTypeId.Value, + MetaString.Empty('.', '_'), + MetaString.Empty('$', '_'), + false, + fields, + hasFieldsMeta); + } + + private static void ValidateCompatibleTypeMeta( + TypeMeta remoteTypeMeta, + RegisteredTypeInfo localInfo, + HashSet expectedWireTypes, + TypeId actualWireTypeId) + { + if (remoteTypeMeta.RegisterByName) + { + if (!localInfo.RegisterByName || localInfo.NamespaceName is null) + { + throw new InvalidDataException( + "received name-registered compatible metadata for id-registered local type"); + } + + if (remoteTypeMeta.NamespaceName.Value != localInfo.NamespaceName.Value.Value) + { + throw new InvalidDataException( + $"namespace mismatch: expected {localInfo.NamespaceName.Value.Value}, got {remoteTypeMeta.NamespaceName.Value}"); + } + + if (remoteTypeMeta.TypeName.Value != localInfo.TypeName.Value) + { + throw new InvalidDataException( + $"type name mismatch: expected {localInfo.TypeName.Value}, got {remoteTypeMeta.TypeName.Value}"); + } + } + else + { + if (localInfo.RegisterByName) + { + throw new InvalidDataException( + "received id-registered compatible metadata for name-registered local type"); + } + + if (!remoteTypeMeta.UserTypeId.HasValue) + { + throw new InvalidDataException("missing user type id in compatible type metadata"); + } + + if (!localInfo.UserTypeId.HasValue) + { + throw new InvalidDataException("missing local user type id metadata for id-registered type"); + } + + if (remoteTypeMeta.UserTypeId.Value != localInfo.UserTypeId.Value) + { + throw new TypeMismatchException(localInfo.UserTypeId.Value, remoteTypeMeta.UserTypeId.Value); + } + } + + if (remoteTypeMeta.TypeId.HasValue && + Enum.IsDefined(typeof(TypeId), remoteTypeMeta.TypeId.Value)) + { + TypeId remoteWireTypeId = (TypeId)remoteTypeMeta.TypeId.Value; + if (!expectedWireTypes.Contains(remoteWireTypeId)) + { + throw new TypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value); + } + } + } + + private static void WriteMetaString( + ref WriteContext context, + MetaString value, + IReadOnlyList encodings, + MetaStringEncoder encoder) + { + MetaString normalized = encodings.Contains(value.Encoding) + ? value + : encoder.Encode(value.Value, encodings); + if (!encodings.Contains(normalized.Encoding)) + { + throw new EncodingException("failed to normalize meta string encoding"); + } + + byte[] bytes = normalized.Bytes; + (uint index, bool isNew) = context.MetaStringWriteState.AssignIndexIfAbsent(normalized); + if (isNew) + { + context.Writer.WriteVarUInt32((uint)(bytes.Length << 1)); + if (bytes.Length > 16) + { + context.Writer.WriteInt64(unchecked((long)JavaMetaStringHash(normalized))); + } + else if (bytes.Length > 0) + { + context.Writer.WriteUInt8((byte)normalized.Encoding); + } + + context.Writer.WriteBytes(bytes); + } + else + { + context.Writer.WriteVarUInt32(((index + 1) << 1) | 1); + } + } + + private static MetaString ReadMetaString( + ref ReadContext context, + MetaStringDecoder decoder, + IReadOnlyList encodings) + { + uint header = context.Reader.ReadVarUInt32(); + int length = checked((int)(header >> 1)); + bool isRef = (header & 1) == 1; + if (isRef) + { + int index = length - 1; + MetaString? cached = context.MetaStringReadState.ValueAt(index); + if (cached is null) + { + throw new InvalidDataException($"unknown meta string ref index {index}"); + } + + return cached.Value; + } + + MetaString value; + if (length == 0) + { + value = MetaString.Empty(decoder.SpecialChar1, decoder.SpecialChar2); + } + else + { + MetaStringEncoding encoding; + if (length > 16) + { + long hash = context.Reader.ReadInt64(); + byte rawEncoding = unchecked((byte)(hash & 0xFF)); + encoding = (MetaStringEncoding)rawEncoding; + } + else + { + encoding = (MetaStringEncoding)context.Reader.ReadUInt8(); + } + + if (!encodings.Contains(encoding)) + { + throw new InvalidDataException($"meta string encoding {encoding} not allowed in this context"); + } + + byte[] bytes = context.Reader.ReadBytes(length); + value = decoder.Decode(bytes, encoding); + } + + context.MetaStringReadState.Append(value); + return value; + } + + private static ulong JavaMetaStringHash(MetaString metaString) + { + (ulong h1, _) = MurmurHash3.X64_128(metaString.Bytes, 47); + long hash = unchecked((long)h1); + if (hash != long.MinValue) + { + hash = Math.Abs(hash); + } + + ulong result = unchecked((ulong)hash); + if (result == 0) + { + result += 256; + } + + result &= 0xffffffffffffff00; + result |= (byte)metaString.Encoding; + return result; + } + private static SerializerBinding GetSharedBinding(Type type) { return SharedBindings.GetOrAdd(type, CreateBindingCore); @@ -565,6 +1006,56 @@ private static SerializerBinding CreateBindingCore(Type type) return new BinarySerializer(); } + if (type == typeof(bool[])) + { + return new BoolArraySerializer(); + } + + if (type == typeof(sbyte[])) + { + return new Int8ArraySerializer(); + } + + if (type == typeof(short[])) + { + return new Int16ArraySerializer(); + } + + if (type == typeof(int[])) + { + return new Int32ArraySerializer(); + } + + if (type == typeof(long[])) + { + return new Int64ArraySerializer(); + } + + if (type == typeof(ushort[])) + { + return new UInt16ArraySerializer(); + } + + if (type == typeof(uint[])) + { + return new UInt32ArraySerializer(); + } + + if (type == typeof(ulong[])) + { + return new UInt64ArraySerializer(); + } + + if (type == typeof(float[])) + { + return new Float32ArraySerializer(); + } + + if (type == typeof(double[])) + { + return new Float64ArraySerializer(); + } + if (type == typeof(DateOnly)) { return new DateOnlySerializer(); diff --git a/csharp/src/Fory/TypedSerializerBinding.cs b/csharp/src/Fory/TypedSerializerBinding.cs index 4c30c158a8..aa05a07a89 100644 --- a/csharp/src/Fory/TypedSerializerBinding.cs +++ b/csharp/src/Fory/TypedSerializerBinding.cs @@ -185,12 +185,12 @@ public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInf public override void WriteTypeInfo(ref WriteContext context) { - SerializerTypeInfo.WriteTypeInfo(Type, this, ref context); + context.TypeResolver.WriteTypeInfo(Type, this, ref context); } public override void ReadTypeInfo(ref ReadContext context) { - SerializerTypeInfo.ReadTypeInfo(Type, this, ref context); + context.TypeResolver.ReadTypeInfo(Type, this, ref context); } public override IReadOnlyList CompatibleTypeMetaFields(bool trackRef) From 7aee8b915a1087af465da47b4203003d8dbbf1ee Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 22 Feb 2026 13:34:51 +0800 Subject: [PATCH 14/16] refactor(csharp): unify serializer hierarchy and inline resolver ctor cache --- .../src/Fory.Generator/ForyObjectGenerator.cs | 2 +- csharp/src/Fory/AnySerializer.cs | 8 +- csharp/src/Fory/CollectionSerializers.cs | 6 +- csharp/src/Fory/DictionarySerializers.cs | 2 +- csharp/src/Fory/EnumSerializer.cs | 2 +- csharp/src/Fory/Fory.cs | 8 +- csharp/src/Fory/NullableKeyDictionary.cs | 2 +- csharp/src/Fory/OptionalSerializer.cs | 2 +- csharp/src/Fory/PrimitiveArraySerializers.cs | 20 +- .../Fory/PrimitiveCollectionSerializers.cs | 30 +- csharp/src/Fory/PrimitiveSerializers.cs | 44 +-- csharp/src/Fory/Serializer.cs | 194 +++++++++--- csharp/src/Fory/StringSerializer.cs | 2 +- csharp/src/Fory/TypeInfo.cs | 4 +- csharp/src/Fory/TypeResolver.cs | 95 ++++-- csharp/src/Fory/TypedSerializerBinding.cs | 291 ------------------ csharp/src/Fory/UnionSerializer.cs | 2 +- csharp/tests/Fory.XlangPeer/Program.cs | 2 +- 18 files changed, 290 insertions(+), 426 deletions(-) delete mode 100644 csharp/src/Fory/TypedSerializerBinding.cs diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs index bbda5d6c71..1febe6b651 100644 --- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs +++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs @@ -135,7 +135,7 @@ private static void Emit(SourceProductionContext context, ImmutableArray"); + sb.AppendLine($"file sealed class {model.SerializerName} : global::Apache.Fory.Serializer<{model.TypeName}>"); sb.AppendLine("{"); sb.AppendLine(" private static global::Apache.Fory.RefMode __ForyRefMode(bool nullable, bool trackRef)"); sb.AppendLine(" {"); diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs index e8940f1f83..52268b97ab 100644 --- a/csharp/src/Fory/AnySerializer.cs +++ b/csharp/src/Fory/AnySerializer.cs @@ -17,7 +17,7 @@ namespace Apache.Fory; -public sealed class DynamicAnyObjectSerializer : TypedSerializer +public sealed class DynamicAnyObjectSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Unknown; public override bool IsNullableType => true; @@ -149,7 +149,7 @@ public override void Write(ref WriteContext context, in object? value, RefMode r private static bool AnyValueIsReferenceTrackable(object value, TypeResolver typeResolver) { - SerializerBinding serializer = typeResolver.GetBinding(value.GetType()); + Serializer serializer = typeResolver.GetBinding(value.GetType()); return serializer.IsReferenceTrackableType; } } @@ -164,7 +164,7 @@ internal static void WriteAnyTypeInfo(object value, ref WriteContext context) return; } - SerializerBinding serializer = context.TypeResolver.GetBinding(value.GetType()); + Serializer serializer = context.TypeResolver.GetBinding(value.GetType()); serializer.WriteTypeInfo(ref context); } @@ -210,7 +210,7 @@ public static void WriteAnyPayload(object value, ref WriteContext context, bool return; } - SerializerBinding serializer = context.TypeResolver.GetBinding(value.GetType()); + Serializer serializer = context.TypeResolver.GetBinding(value.GetType()); serializer.WriteDataObject(ref context, value, hasGenerics); } } diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs index b1bb50a10a..c1b026e33a 100644 --- a/csharp/src/Fory/CollectionSerializers.cs +++ b/csharp/src/Fory/CollectionSerializers.cs @@ -391,7 +391,7 @@ private static bool IsSet(Type valueType) } } -public sealed class ArraySerializer : TypedSerializer +public sealed class ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.List; public override bool IsNullableType => true; @@ -416,7 +416,7 @@ public override T[] ReadData(ref ReadContext context) } } -public class ListSerializer : TypedSerializer> +public class ListSerializer : Serializer> { public override TypeId StaticTypeId => TypeId.List; public override bool IsNullableType => true; @@ -436,7 +436,7 @@ public override List ReadData(ref ReadContext context) } } -public sealed class SetSerializer : TypedSerializer> where T : notnull +public sealed class SetSerializer : Serializer> where T : notnull { public override TypeId StaticTypeId => TypeId.Set; public override bool IsNullableType => true; diff --git a/csharp/src/Fory/DictionarySerializers.cs b/csharp/src/Fory/DictionarySerializers.cs index e2df3943f4..9b1e923a14 100644 --- a/csharp/src/Fory/DictionarySerializers.cs +++ b/csharp/src/Fory/DictionarySerializers.cs @@ -27,7 +27,7 @@ internal static class DictionaryBits public const byte DeclaredValueType = 0b0010_0000; } -public class DictionarySerializer : TypedSerializer> +public class DictionarySerializer : Serializer> where TKey : notnull { public override TypeId StaticTypeId => TypeId.Map; diff --git a/csharp/src/Fory/EnumSerializer.cs b/csharp/src/Fory/EnumSerializer.cs index fadb419306..ba269899e2 100644 --- a/csharp/src/Fory/EnumSerializer.cs +++ b/csharp/src/Fory/EnumSerializer.cs @@ -17,7 +17,7 @@ namespace Apache.Fory; -public sealed class EnumSerializer : TypedSerializer where TEnum : struct, Enum +public sealed class EnumSerializer : Serializer where TEnum : struct, Enum { public override TypeId StaticTypeId => TypeId.Enum; public override TEnum DefaultValue => default; diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs index f458c5c949..bb6cff0577 100644 --- a/csharp/src/Fory/Fory.cs +++ b/csharp/src/Fory/Fory.cs @@ -55,17 +55,17 @@ public Fory Register(string typeNamespace, string typeName) } public Fory Register(uint typeId) - where TSerializer : TypedSerializer, new() + where TSerializer : Serializer, new() { - SerializerBinding serializerBinding = _typeResolver.RegisterCustom(); + Serializer serializerBinding = _typeResolver.RegisterCustom(); _typeResolver.Register(typeof(T), typeId, serializerBinding); return this; } public Fory Register(string typeNamespace, string typeName) - where TSerializer : TypedSerializer, new() + where TSerializer : Serializer, new() { - SerializerBinding serializerBinding = _typeResolver.RegisterCustom(); + Serializer serializerBinding = _typeResolver.RegisterCustom(); _typeResolver.Register(typeof(T), typeNamespace, typeName, serializerBinding); return this; } diff --git a/csharp/src/Fory/NullableKeyDictionary.cs b/csharp/src/Fory/NullableKeyDictionary.cs index fe54ab4d33..19d2462610 100644 --- a/csharp/src/Fory/NullableKeyDictionary.cs +++ b/csharp/src/Fory/NullableKeyDictionary.cs @@ -388,7 +388,7 @@ IEnumerator IEnumerable.GetEnumerator() } } -public sealed class NullableKeyDictionarySerializer : TypedSerializer> +public sealed class NullableKeyDictionarySerializer : Serializer> { public override TypeId StaticTypeId => TypeId.Map; public override bool IsNullableType => true; diff --git a/csharp/src/Fory/OptionalSerializer.cs b/csharp/src/Fory/OptionalSerializer.cs index dfc60fb62a..cd5ae80456 100644 --- a/csharp/src/Fory/OptionalSerializer.cs +++ b/csharp/src/Fory/OptionalSerializer.cs @@ -17,7 +17,7 @@ namespace Apache.Fory; -public sealed class NullableSerializer : TypedSerializer where T : struct +public sealed class NullableSerializer : Serializer where T : struct { public override TypeId StaticTypeId => TypeResolver.StaticTypeIdOf(); diff --git a/csharp/src/Fory/PrimitiveArraySerializers.cs b/csharp/src/Fory/PrimitiveArraySerializers.cs index 47ef1a398c..dffbcaa461 100644 --- a/csharp/src/Fory/PrimitiveArraySerializers.cs +++ b/csharp/src/Fory/PrimitiveArraySerializers.cs @@ -17,7 +17,7 @@ namespace Apache.Fory; -internal sealed class BoolArraySerializer : TypedSerializer +internal sealed class BoolArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.BoolArray; @@ -53,7 +53,7 @@ public override bool[] ReadData(ref ReadContext context) } } -internal sealed class Int8ArraySerializer : TypedSerializer +internal sealed class Int8ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.Int8Array; @@ -89,7 +89,7 @@ public override sbyte[] ReadData(ref ReadContext context) } } -internal sealed class Int16ArraySerializer : TypedSerializer +internal sealed class Int16ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.Int16Array; @@ -130,7 +130,7 @@ public override short[] ReadData(ref ReadContext context) } } -internal sealed class Int32ArraySerializer : TypedSerializer +internal sealed class Int32ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.Int32Array; @@ -171,7 +171,7 @@ public override int[] ReadData(ref ReadContext context) } } -internal sealed class Int64ArraySerializer : TypedSerializer +internal sealed class Int64ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.Int64Array; @@ -212,7 +212,7 @@ public override long[] ReadData(ref ReadContext context) } } -internal sealed class UInt16ArraySerializer : TypedSerializer +internal sealed class UInt16ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.UInt16Array; @@ -253,7 +253,7 @@ public override ushort[] ReadData(ref ReadContext context) } } -internal sealed class UInt32ArraySerializer : TypedSerializer +internal sealed class UInt32ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.UInt32Array; @@ -294,7 +294,7 @@ public override uint[] ReadData(ref ReadContext context) } } -internal sealed class UInt64ArraySerializer : TypedSerializer +internal sealed class UInt64ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.UInt64Array; @@ -335,7 +335,7 @@ public override ulong[] ReadData(ref ReadContext context) } } -internal sealed class Float32ArraySerializer : TypedSerializer +internal sealed class Float32ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.Float32Array; @@ -376,7 +376,7 @@ public override float[] ReadData(ref ReadContext context) } } -internal sealed class Float64ArraySerializer : TypedSerializer +internal sealed class Float64ArraySerializer : Serializer { public override TypeId StaticTypeId => TypeId.Float64Array; diff --git a/csharp/src/Fory/PrimitiveCollectionSerializers.cs b/csharp/src/Fory/PrimitiveCollectionSerializers.cs index 9d5638263f..38fbe54285 100644 --- a/csharp/src/Fory/PrimitiveCollectionSerializers.cs +++ b/csharp/src/Fory/PrimitiveCollectionSerializers.cs @@ -65,7 +65,7 @@ public static void WriteMapChunkTypeInfo( } } -internal sealed class ListBoolSerializer : TypedSerializer> +internal sealed class ListBoolSerializer : Serializer> { private static readonly ListSerializer Fallback = new(); @@ -95,7 +95,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class ListIntSerializer : TypedSerializer> +internal sealed class ListIntSerializer : Serializer> { private static readonly ListSerializer Fallback = new(); @@ -125,7 +125,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class ListLongSerializer : TypedSerializer> +internal sealed class ListLongSerializer : Serializer> { private static readonly ListSerializer Fallback = new(); @@ -155,7 +155,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class ListStringSerializer : TypedSerializer> +internal sealed class ListStringSerializer : Serializer> { private static readonly ListSerializer Fallback = new(); @@ -213,7 +213,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class ListDateOnlySerializer : TypedSerializer> +internal sealed class ListDateOnlySerializer : Serializer> { private static readonly ListSerializer Fallback = new(); private static readonly DateOnly Epoch = new(1970, 1, 1); @@ -244,7 +244,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class ListDateTimeOffsetSerializer : TypedSerializer> +internal sealed class ListDateTimeOffsetSerializer : Serializer> { private static readonly ListSerializer Fallback = new(); @@ -276,7 +276,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class ListDateTimeSerializer : TypedSerializer> +internal sealed class ListDateTimeSerializer : Serializer> { private static readonly ListSerializer Fallback = new(); @@ -314,7 +314,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class ListTimeSpanSerializer : TypedSerializer> +internal sealed class ListTimeSpanSerializer : Serializer> { private static readonly ListSerializer Fallback = new(); @@ -347,7 +347,7 @@ public override List ReadData(ref ReadContext context) } } -internal sealed class DictionaryStringStringSerializer : TypedSerializer> +internal sealed class DictionaryStringStringSerializer : Serializer> { private static readonly DictionarySerializer Fallback = new(); @@ -432,7 +432,7 @@ private static void WriteMapStringString(ref WriteContext context, Dictionary> +internal sealed class DictionaryStringIntSerializer : Serializer> { private static readonly DictionarySerializer Fallback = new(); @@ -463,7 +463,7 @@ public override Dictionary ReadData(ref ReadContext context) } } -internal sealed class DictionaryStringLongSerializer : TypedSerializer> +internal sealed class DictionaryStringLongSerializer : Serializer> { private static readonly DictionarySerializer Fallback = new(); @@ -494,7 +494,7 @@ public override Dictionary ReadData(ref ReadContext context) } } -internal sealed class DictionaryStringBoolSerializer : TypedSerializer> +internal sealed class DictionaryStringBoolSerializer : Serializer> { private static readonly DictionarySerializer Fallback = new(); @@ -525,7 +525,7 @@ public override Dictionary ReadData(ref ReadContext context) } } -internal sealed class DictionaryStringDoubleSerializer : TypedSerializer> +internal sealed class DictionaryStringDoubleSerializer : Serializer> { private static readonly DictionarySerializer Fallback = new(); @@ -556,7 +556,7 @@ public override Dictionary ReadData(ref ReadContext context) } } -internal sealed class DictionaryIntIntSerializer : TypedSerializer> +internal sealed class DictionaryIntIntSerializer : Serializer> { private static readonly DictionarySerializer Fallback = new(); @@ -589,7 +589,7 @@ public override Dictionary ReadData(ref ReadContext context) } } -internal sealed class DictionaryLongLongSerializer : TypedSerializer> +internal sealed class DictionaryLongLongSerializer : Serializer> { private static readonly DictionarySerializer Fallback = new(); diff --git a/csharp/src/Fory/PrimitiveSerializers.cs b/csharp/src/Fory/PrimitiveSerializers.cs index f5f26478c0..56e2fd5171 100644 --- a/csharp/src/Fory/PrimitiveSerializers.cs +++ b/csharp/src/Fory/PrimitiveSerializers.cs @@ -66,7 +66,7 @@ private static ForyTimestamp Normalize(long seconds, long nanos) } } -public sealed class BoolSerializer : TypedSerializer +public sealed class BoolSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Bool; @@ -84,7 +84,7 @@ public override bool ReadData(ref ReadContext context) } } -public sealed class Int8Serializer : TypedSerializer +public sealed class Int8Serializer : Serializer { public override TypeId StaticTypeId => TypeId.Int8; @@ -102,7 +102,7 @@ public override sbyte ReadData(ref ReadContext context) } } -public sealed class Int16Serializer : TypedSerializer +public sealed class Int16Serializer : Serializer { public override TypeId StaticTypeId => TypeId.Int16; @@ -120,7 +120,7 @@ public override short ReadData(ref ReadContext context) } } -public sealed class Int32Serializer : TypedSerializer +public sealed class Int32Serializer : Serializer { public override TypeId StaticTypeId => TypeId.VarInt32; @@ -138,7 +138,7 @@ public override int ReadData(ref ReadContext context) } } -public sealed class Int64Serializer : TypedSerializer +public sealed class Int64Serializer : Serializer { public override TypeId StaticTypeId => TypeId.VarInt64; @@ -156,7 +156,7 @@ public override long ReadData(ref ReadContext context) } } -public sealed class UInt8Serializer : TypedSerializer +public sealed class UInt8Serializer : Serializer { public override TypeId StaticTypeId => TypeId.UInt8; @@ -174,7 +174,7 @@ public override byte ReadData(ref ReadContext context) } } -public sealed class UInt16Serializer : TypedSerializer +public sealed class UInt16Serializer : Serializer { public override TypeId StaticTypeId => TypeId.UInt16; @@ -192,7 +192,7 @@ public override ushort ReadData(ref ReadContext context) } } -public sealed class UInt32Serializer : TypedSerializer +public sealed class UInt32Serializer : Serializer { public override TypeId StaticTypeId => TypeId.VarUInt32; @@ -210,7 +210,7 @@ public override uint ReadData(ref ReadContext context) } } -public sealed class UInt64Serializer : TypedSerializer +public sealed class UInt64Serializer : Serializer { public override TypeId StaticTypeId => TypeId.VarUInt64; @@ -228,7 +228,7 @@ public override ulong ReadData(ref ReadContext context) } } -public sealed class Float32Serializer : TypedSerializer +public sealed class Float32Serializer : Serializer { public override TypeId StaticTypeId => TypeId.Float32; @@ -246,7 +246,7 @@ public override float ReadData(ref ReadContext context) } } -public sealed class Float64Serializer : TypedSerializer +public sealed class Float64Serializer : Serializer { public override TypeId StaticTypeId => TypeId.Float64; @@ -264,7 +264,7 @@ public override double ReadData(ref ReadContext context) } } -public sealed class BinarySerializer : TypedSerializer +public sealed class BinarySerializer : Serializer { public override TypeId StaticTypeId => TypeId.Binary; @@ -289,7 +289,7 @@ public override byte[] ReadData(ref ReadContext context) } } -public sealed class ForyInt32FixedSerializer : TypedSerializer +public sealed class ForyInt32FixedSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Int32; @@ -307,7 +307,7 @@ public override ForyInt32Fixed ReadData(ref ReadContext context) } } -public sealed class ForyInt64FixedSerializer : TypedSerializer +public sealed class ForyInt64FixedSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Int64; @@ -325,7 +325,7 @@ public override ForyInt64Fixed ReadData(ref ReadContext context) } } -public sealed class ForyInt64TaggedSerializer : TypedSerializer +public sealed class ForyInt64TaggedSerializer : Serializer { public override TypeId StaticTypeId => TypeId.TaggedInt64; @@ -343,7 +343,7 @@ public override ForyInt64Tagged ReadData(ref ReadContext context) } } -public sealed class ForyUInt32FixedSerializer : TypedSerializer +public sealed class ForyUInt32FixedSerializer : Serializer { public override TypeId StaticTypeId => TypeId.UInt32; @@ -361,7 +361,7 @@ public override ForyUInt32Fixed ReadData(ref ReadContext context) } } -public sealed class ForyUInt64FixedSerializer : TypedSerializer +public sealed class ForyUInt64FixedSerializer : Serializer { public override TypeId StaticTypeId => TypeId.UInt64; @@ -379,7 +379,7 @@ public override ForyUInt64Fixed ReadData(ref ReadContext context) } } -public sealed class ForyUInt64TaggedSerializer : TypedSerializer +public sealed class ForyUInt64TaggedSerializer : Serializer { public override TypeId StaticTypeId => TypeId.TaggedUInt64; @@ -397,7 +397,7 @@ public override ForyUInt64Tagged ReadData(ref ReadContext context) } } -public sealed class DateOnlySerializer : TypedSerializer +public sealed class DateOnlySerializer : Serializer { private static readonly DateOnly Epoch = new(1970, 1, 1); @@ -419,7 +419,7 @@ public override DateOnly ReadData(ref ReadContext context) } } -public sealed class DateTimeOffsetSerializer : TypedSerializer +public sealed class DateTimeOffsetSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Timestamp; @@ -441,7 +441,7 @@ public override DateTimeOffset ReadData(ref ReadContext context) } } -public sealed class DateTimeSerializer : TypedSerializer +public sealed class DateTimeSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Timestamp; @@ -469,7 +469,7 @@ public override DateTime ReadData(ref ReadContext context) } } -public sealed class TimeSpanSerializer : TypedSerializer +public sealed class TimeSpanSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Duration; diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs index 1af1fdf6c8..94b94a5d5b 100644 --- a/csharp/src/Fory/Serializer.cs +++ b/csharp/src/Fory/Serializer.cs @@ -17,46 +17,159 @@ namespace Apache.Fory; -public readonly struct Serializer +public abstract class Serializer { - private readonly ITypedSerializer? _serializer; + public abstract Type Type { get; } - internal Serializer(ITypedSerializer serializer) + public abstract TypeId StaticTypeId { get; } + + public abstract bool IsNullableType { get; } + + public abstract bool IsReferenceTrackableType { get; } + + public abstract object? DefaultObject { get; } + + public abstract bool IsNoneObject(object? value); + + public abstract void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics); + + public abstract object? ReadDataObject(ref ReadContext context); + + public abstract void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics); + + public abstract object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo); + + public abstract void WriteTypeInfo(ref WriteContext context); + + public abstract void ReadTypeInfo(ref ReadContext context); + + public abstract IReadOnlyList CompatibleTypeMetaFields(bool trackRef); + + public abstract Serializer RequireSerializer(); +} + +public abstract class Serializer : Serializer +{ + public override Type Type => typeof(T); + + public abstract override TypeId StaticTypeId { get; } + + public override bool IsNullableType => false; + + public override bool IsReferenceTrackableType => false; + + public virtual T DefaultValue => default!; + + public override object? DefaultObject => DefaultValue; + + public virtual bool IsNone(in T value) { - _serializer = serializer; + _ = value; + return false; } - private ITypedSerializer SerializerImpl + public abstract void WriteData(ref WriteContext context, in T value, bool hasGenerics); + + public abstract T ReadData(ref ReadContext context); + + public virtual void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { - get + if (refMode != RefMode.None) { - if (_serializer is null) + bool wroteTrackingRefFlag = false; + if (refMode == RefMode.Tracking && + IsReferenceTrackableType && + value is object obj) + { + if (context.RefWriter.TryWriteReference(context.Writer, obj)) + { + return; + } + + wroteTrackingRefFlag = true; + } + + if (!wroteTrackingRefFlag) { - throw new InvalidDataException($"serializer handle for {typeof(T)} is not initialized"); + if (IsNullableType && IsNone(value)) + { + context.Writer.WriteInt8((sbyte)RefFlag.Null); + return; + } + + context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); } + } - return _serializer; + if (writeTypeInfo) + { + WriteTypeInfo(ref context); } - } - public Type Type => typeof(T); + WriteData(ref context, value, hasGenerics); + } - public TypeId StaticTypeId => SerializerImpl.StaticTypeId; + public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + { + if (refMode != RefMode.None) + { + sbyte rawFlag = context.Reader.ReadInt8(); + RefFlag flag = (RefFlag)rawFlag; + switch (flag) + { + case RefFlag.Null: + return DefaultValue; + case RefFlag.Ref: + { + uint refId = context.Reader.ReadVarUInt32(); + return context.RefReader.ReadRef(refId); + } + case RefFlag.RefValue: + { + uint reservedRefId = context.RefReader.ReserveRefId(); + context.PushPendingReference(reservedRefId); + if (readTypeInfo) + { + ReadTypeInfo(ref context); + } + + T value = ReadData(ref context); + context.FinishPendingReferenceIfNeeded(value); + context.PopPendingReference(); + return value; + } + case RefFlag.NotNullValue: + break; + default: + throw new RefException($"invalid ref flag {rawFlag}"); + } + } - public bool IsNullableType => SerializerImpl.IsNullableType; + if (readTypeInfo) + { + ReadTypeInfo(ref context); + } - public bool IsReferenceTrackableType => SerializerImpl.IsReferenceTrackableType; + return ReadData(ref context); + } - public T DefaultValue => SerializerImpl.DefaultValue; + public override void WriteTypeInfo(ref WriteContext context) + { + context.TypeResolver.WriteTypeInfo(Type, this, ref context); + } - public object? DefaultObject => SerializerImpl.DefaultValue; + public override void ReadTypeInfo(ref ReadContext context) + { + context.TypeResolver.ReadTypeInfo(Type, this, ref context); + } - public bool IsNone(in T value) + public override IReadOnlyList CompatibleTypeMetaFields(bool trackRef) { - return SerializerImpl.IsNone(value); + _ = trackRef; + return []; } - public bool IsNoneObject(object? value) + public override bool IsNoneObject(object? value) { if (value is null) { @@ -66,38 +179,49 @@ public bool IsNoneObject(object? value) return value is T typed && IsNone(typed); } - public void WriteData(ref WriteContext context, in T value, bool hasGenerics) + public override void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics) { - SerializerImpl.WriteData(ref context, value, hasGenerics); + WriteData(ref context, CoerceValue(value), hasGenerics); } - public T ReadData(ref ReadContext context) + public override object? ReadDataObject(ref ReadContext context) { - return SerializerImpl.ReadData(ref context); + return ReadData(ref context); } - public void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) + public override void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) { - SerializerImpl.Write(ref context, value, refMode, writeTypeInfo, hasGenerics); + Write(ref context, CoerceValue(value), refMode, writeTypeInfo, hasGenerics); } - public T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) + public override object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo) { - return SerializerImpl.Read(ref context, refMode, readTypeInfo); + return Read(ref context, refMode, readTypeInfo); } - public void WriteTypeInfo(ref WriteContext context) + public override Serializer RequireSerializer() { - SerializerImpl.WriteTypeInfo(ref context); - } + if (typeof(TCast) == typeof(T)) + { + return (Serializer)(object)this; + } - public void ReadTypeInfo(ref ReadContext context) - { - SerializerImpl.ReadTypeInfo(ref context); + throw new InvalidDataException($"serializer type mismatch for {typeof(TCast)}"); } - public IReadOnlyList CompatibleTypeMetaFields(bool trackRef) + protected virtual T CoerceValue(object? value) { - return SerializerImpl.CompatibleTypeMetaFields(trackRef); + if (value is T typed) + { + return typed; + } + + if (value is null && IsNullableType) + { + return DefaultValue; + } + + throw new InvalidDataException( + $"serializer {GetType().Name} expected value of type {typeof(T)}, got {value?.GetType()}"); } } diff --git a/csharp/src/Fory/StringSerializer.cs b/csharp/src/Fory/StringSerializer.cs index 6ea8ad5745..794518b2f2 100644 --- a/csharp/src/Fory/StringSerializer.cs +++ b/csharp/src/Fory/StringSerializer.cs @@ -19,7 +19,7 @@ namespace Apache.Fory; -public sealed class StringSerializer : TypedSerializer +public sealed class StringSerializer : Serializer { public override TypeId StaticTypeId => TypeId.String; diff --git a/csharp/src/Fory/TypeInfo.cs b/csharp/src/Fory/TypeInfo.cs index c5469ef644..63071eb768 100644 --- a/csharp/src/Fory/TypeInfo.cs +++ b/csharp/src/Fory/TypeInfo.cs @@ -19,7 +19,7 @@ namespace Apache.Fory; public sealed class TypeInfo { - internal TypeInfo(Type type, SerializerBinding serializer) + internal TypeInfo(Type type, Serializer serializer) { Type = type; Serializer = serializer; @@ -31,7 +31,7 @@ internal TypeInfo(Type type, SerializerBinding serializer) public Type Type { get; } - internal SerializerBinding Serializer { get; } + internal Serializer Serializer { get; } public TypeId StaticTypeId { get; } diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs index ff9d6143f6..5c6c73dbbb 100644 --- a/csharp/src/Fory/TypeResolver.cs +++ b/csharp/src/Fory/TypeResolver.cs @@ -16,6 +16,7 @@ // under the License. using System.Collections.Concurrent; +using System.Linq.Expressions; namespace Apache.Fory; @@ -25,7 +26,7 @@ internal sealed record RegisteredTypeInfo( bool RegisterByName, MetaString? NamespaceName, MetaString TypeName, - SerializerBinding Serializer); + Serializer Serializer); internal enum DynamicRegistrationMode { @@ -45,8 +46,9 @@ internal sealed class TypeReader public sealed class TypeResolver { - private static readonly ConcurrentDictionary> GeneratedFactories = new(); - private static readonly ConcurrentDictionary SharedBindings = new(); + private static readonly ConcurrentDictionary> GeneratedFactories = new(); + private static readonly ConcurrentDictionary> SerializerConstructors = new(); + private static readonly ConcurrentDictionary SharedBindings = new(); private static readonly ConcurrentDictionary SharedTypeInfos = new(); private static int _sharedCacheVersion; @@ -55,7 +57,7 @@ public sealed class TypeResolver private readonly Dictionary _byTypeName = []; private readonly Dictionary _registrationModeByKind = []; - private readonly ConcurrentDictionary _serializerBindings = new(); + private readonly ConcurrentDictionary _serializerBindings = new(); private readonly ConcurrentDictionary _typeInfos = new(); private int _cacheVersion; @@ -76,14 +78,14 @@ private static class SerializerCache { public static int Version = -1; public static TypeResolver? Resolver; - public static ITypedSerializer? Cached; + public static Serializer? Cached; } public static void RegisterGenerated() - where TSerializer : TypedSerializer, new() + where TSerializer : Serializer, new() { Type type = typeof(T); - GeneratedFactories[type] = SerializerFactory.Create; + GeneratedFactories[type] = CreateSerializer; SharedBindings.TryRemove(type, out _); SharedTypeInfos.TryRemove(type, out _); unchecked @@ -127,14 +129,14 @@ public Serializer GetSerializer() ReferenceEquals(SerializerCache.Resolver, this) && SerializerCache.Cached is not null) { - return new Serializer(SerializerCache.Cached); + return SerializerCache.Cached; } - ITypedSerializer typedSerializer = GetTypeInfo().Serializer.RequireTypedSerializer(); + Serializer typedSerializer = GetTypeInfo().Serializer.RequireSerializer(); SerializerCache.Resolver = this; SerializerCache.Version = _cacheVersion; SerializerCache.Cached = typedSerializer; - return new Serializer(typedSerializer); + return typedSerializer; } public TypeInfo GetTypeInfo(Type type) @@ -158,20 +160,20 @@ public TypeInfo GetTypeInfo() return typeInfo; } - internal SerializerBinding GetBinding(Type type) + internal Serializer GetBinding(Type type) { return _serializerBindings.GetOrAdd(type, CreateBindingCore); } - internal SerializerBinding RegisterCustom() - where TSerializer : TypedSerializer, new() + internal Serializer RegisterCustom() + where TSerializer : Serializer, new() { - SerializerBinding serializerBinding = SerializerFactory.Create(); + Serializer serializerBinding = CreateSerializer(); RegisterCustom(typeof(T), serializerBinding); return serializerBinding; } - internal void RegisterCustom(Type type, SerializerBinding serializerBinding) + internal void RegisterCustom(Type type, Serializer serializerBinding) { _serializerBindings[type] = serializerBinding; _typeInfos.TryRemove(type, out _); @@ -181,9 +183,9 @@ internal void RegisterCustom(Type type, SerializerBinding serializerBinding) } } - internal void Register(Type type, uint id, SerializerBinding? explicitSerializer = null) + internal void Register(Type type, uint id, Serializer? explicitSerializer = null) { - SerializerBinding serializer = explicitSerializer ?? GetBinding(type); + Serializer serializer = explicitSerializer ?? GetBinding(type); RegisteredTypeInfo info = new( id, serializer.StaticTypeId, @@ -205,9 +207,9 @@ internal void Register(Type type, uint id, SerializerBinding? explicitSerializer }; } - internal void Register(Type type, string namespaceName, string typeName, SerializerBinding? explicitSerializer = null) + internal void Register(Type type, string namespaceName, string typeName, Serializer? explicitSerializer = null) { - SerializerBinding serializer = explicitSerializer ?? GetBinding(type); + Serializer serializer = explicitSerializer ?? GetBinding(type); MetaString namespaceMeta = MetaStringEncoder.Namespace.Encode(namespaceName, TypeMetaEncodings.NamespaceMetaStringEncodings); MetaString typeNameMeta = MetaStringEncoder.TypeName.Encode(typeName, TypeMetaEncodings.TypeNameMetaStringEncodings); RegisteredTypeInfo info = new( @@ -246,7 +248,7 @@ internal RegisteredTypeInfo RequireRegisteredTypeInfo(Type type) throw new TypeNotRegisteredException($"{type} is not registered"); } - internal void WriteTypeInfo(Type type, SerializerBinding serializer, ref WriteContext context) + internal void WriteTypeInfo(Type type, Serializer serializer, ref WriteContext context) { TypeId staticTypeId = serializer.StaticTypeId; if (!staticTypeId.IsUserTypeKind()) @@ -313,7 +315,7 @@ internal void WriteTypeInfo(Type type, SerializerBinding serializer, ref WriteCo } } - internal void ReadTypeInfo(Type type, SerializerBinding serializer, ref ReadContext context) + internal void ReadTypeInfo(Type type, Serializer serializer, ref ReadContext context) { uint rawTypeId = context.Reader.ReadVarUInt32(); if (!Enum.IsDefined(typeof(TypeId), rawTypeId)) @@ -924,7 +926,7 @@ private static ulong JavaMetaStringHash(MetaString metaString) return result; } - private static SerializerBinding GetSharedBinding(Type type) + private static Serializer GetSharedBinding(Type type) { return SharedBindings.GetOrAdd(type, CreateBindingCore); } @@ -934,9 +936,9 @@ private static TypeInfo GetSharedTypeInfo(Type type) return SharedTypeInfos.GetOrAdd(type, t => new TypeInfo(t, GetSharedBinding(t))); } - private static SerializerBinding CreateBindingCore(Type type) + private static Serializer CreateBindingCore(Type type) { - if (GeneratedFactories.TryGetValue(type, out Func? generatedFactory)) + if (GeneratedFactories.TryGetValue(type, out Func? generatedFactory)) { return generatedFactory(); } @@ -1189,20 +1191,20 @@ private static SerializerBinding CreateBindingCore(Type type) if (typeof(Union).IsAssignableFrom(type)) { Type serializerType = typeof(UnionSerializer<>).MakeGenericType(type); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } if (type.IsEnum) { Type serializerType = typeof(EnumSerializer<>).MakeGenericType(type); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } if (type.IsArray) { Type elementType = type.GetElementType()!; Type serializerType = typeof(ArraySerializer<>).MakeGenericType(elementType); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } if (type.IsGenericType) @@ -1212,37 +1214,66 @@ private static SerializerBinding CreateBindingCore(Type type) if (genericType == typeof(Nullable<>)) { Type serializerType = typeof(NullableSerializer<>).MakeGenericType(genericArgs[0]); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } if (genericType == typeof(List<>)) { Type serializerType = typeof(ListSerializer<>).MakeGenericType(genericArgs[0]); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } if (genericType == typeof(HashSet<>)) { Type serializerType = typeof(SetSerializer<>).MakeGenericType(genericArgs[0]); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } if (genericType == typeof(Dictionary<,>)) { Type serializerType = typeof(DictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } if (genericType == typeof(NullableKeyDictionary<,>)) { Type serializerType = typeof(NullableKeyDictionarySerializer<,>).MakeGenericType(genericArgs[0], genericArgs[1]); - return SerializerFactory.Create(serializerType); + return CreateSerializer(serializerType); } } throw new TypeNotRegisteredException($"No serializer available for {type}"); } + private static Serializer CreateSerializer() + where TSerializer : Serializer, new() + { + return new TSerializer(); + } + + private static Serializer CreateSerializer(Type serializerType) + { + if (!typeof(Serializer).IsAssignableFrom(serializerType)) + { + throw new InvalidDataException($"{serializerType} is not a serializer"); + } + + return SerializerConstructors.GetOrAdd(serializerType, BuildSerializerConstructor)(); + } + + private static Func BuildSerializerConstructor(Type serializerType) + { + try + { + NewExpression body = Expression.New(serializerType); + return Expression.Lambda>(body).Compile(); + } + catch (Exception ex) + { + throw new InvalidDataException($"failed to build serializer constructor for {serializerType}: {ex.Message}"); + } + } + private static MetaString ReadMetaString(ByteReader reader, MetaStringDecoder decoder, IReadOnlyList encodings) { byte header = reader.ReadUInt8(); diff --git a/csharp/src/Fory/TypedSerializerBinding.cs b/csharp/src/Fory/TypedSerializerBinding.cs deleted file mode 100644 index aa05a07a89..0000000000 --- a/csharp/src/Fory/TypedSerializerBinding.cs +++ /dev/null @@ -1,291 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -using System.Collections.Concurrent; -using System.Linq.Expressions; - -namespace Apache.Fory; - -public interface ITypedSerializer -{ - TypeId StaticTypeId { get; } - - bool IsNullableType { get; } - - bool IsReferenceTrackableType { get; } - - T DefaultValue { get; } - - bool IsNone(in T value); - - void WriteData(ref WriteContext context, in T value, bool hasGenerics); - - T ReadData(ref ReadContext context); - - void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics); - - T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo); - - void WriteTypeInfo(ref WriteContext context); - - void ReadTypeInfo(ref ReadContext context); - - IReadOnlyList CompatibleTypeMetaFields(bool trackRef); -} - -public abstract class SerializerBinding -{ - public abstract Type Type { get; } - - public abstract TypeId StaticTypeId { get; } - - public abstract bool IsNullableType { get; } - - public abstract bool IsReferenceTrackableType { get; } - - public abstract object? DefaultObject { get; } - - public abstract bool IsNoneObject(object? value); - - public abstract void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics); - - public abstract object? ReadDataObject(ref ReadContext context); - - public abstract void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics); - - public abstract object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo); - - public abstract void WriteTypeInfo(ref WriteContext context); - - public abstract void ReadTypeInfo(ref ReadContext context); - - public abstract IReadOnlyList CompatibleTypeMetaFields(bool trackRef); - - public abstract ITypedSerializer RequireTypedSerializer(); -} - -public abstract class TypedSerializer : SerializerBinding, ITypedSerializer -{ - public override Type Type => typeof(T); - - public abstract override TypeId StaticTypeId { get; } - - public override bool IsNullableType => false; - - public override bool IsReferenceTrackableType => false; - - public virtual T DefaultValue => default!; - - public override object? DefaultObject => DefaultValue; - - public virtual bool IsNone(in T value) - { - _ = value; - return false; - } - - public abstract void WriteData(ref WriteContext context, in T value, bool hasGenerics); - - public abstract T ReadData(ref ReadContext context); - - public virtual void Write(ref WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) - { - if (refMode != RefMode.None) - { - bool wroteTrackingRefFlag = false; - if (refMode == RefMode.Tracking && - IsReferenceTrackableType && - value is object obj) - { - if (context.RefWriter.TryWriteReference(context.Writer, obj)) - { - return; - } - - wroteTrackingRefFlag = true; - } - - if (!wroteTrackingRefFlag) - { - if (IsNullableType && IsNone(value)) - { - context.Writer.WriteInt8((sbyte)RefFlag.Null); - return; - } - - context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue); - } - } - - if (writeTypeInfo) - { - WriteTypeInfo(ref context); - } - - WriteData(ref context, value, hasGenerics); - } - - public virtual T Read(ref ReadContext context, RefMode refMode, bool readTypeInfo) - { - if (refMode != RefMode.None) - { - sbyte rawFlag = context.Reader.ReadInt8(); - RefFlag flag = (RefFlag)rawFlag; - switch (flag) - { - case RefFlag.Null: - return DefaultValue; - case RefFlag.Ref: - { - uint refId = context.Reader.ReadVarUInt32(); - return context.RefReader.ReadRef(refId); - } - case RefFlag.RefValue: - { - uint reservedRefId = context.RefReader.ReserveRefId(); - context.PushPendingReference(reservedRefId); - if (readTypeInfo) - { - ReadTypeInfo(ref context); - } - - T value = ReadData(ref context); - context.FinishPendingReferenceIfNeeded(value); - context.PopPendingReference(); - return value; - } - case RefFlag.NotNullValue: - break; - default: - throw new RefException($"invalid ref flag {rawFlag}"); - } - } - - if (readTypeInfo) - { - ReadTypeInfo(ref context); - } - - return ReadData(ref context); - } - - public override void WriteTypeInfo(ref WriteContext context) - { - context.TypeResolver.WriteTypeInfo(Type, this, ref context); - } - - public override void ReadTypeInfo(ref ReadContext context) - { - context.TypeResolver.ReadTypeInfo(Type, this, ref context); - } - - public override IReadOnlyList CompatibleTypeMetaFields(bool trackRef) - { - _ = trackRef; - return []; - } - - public override bool IsNoneObject(object? value) - { - if (value is null) - { - return IsNullableType; - } - - return value is T typed && IsNone(typed); - } - - public override void WriteDataObject(ref WriteContext context, object? value, bool hasGenerics) - { - WriteData(ref context, CoerceValue(value), hasGenerics); - } - - public override object? ReadDataObject(ref ReadContext context) - { - return ReadData(ref context); - } - - public override void WriteObject(ref WriteContext context, object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics) - { - Write(ref context, CoerceValue(value), refMode, writeTypeInfo, hasGenerics); - } - - public override object? ReadObject(ref ReadContext context, RefMode refMode, bool readTypeInfo) - { - return Read(ref context, refMode, readTypeInfo); - } - - public override ITypedSerializer RequireTypedSerializer() - { - if (typeof(TCast) == typeof(T)) - { - return (ITypedSerializer)(object)this; - } - - throw new InvalidDataException($"serializer type mismatch for {typeof(TCast)}"); - } - - protected virtual T CoerceValue(object? value) - { - if (value is T typed) - { - return typed; - } - - if (value is null && IsNullableType) - { - return DefaultValue; - } - - throw new InvalidDataException( - $"serializer {GetType().Name} expected value of type {typeof(T)}, got {value?.GetType()}"); - } -} - -internal static class SerializerFactory -{ - private static readonly ConcurrentDictionary> Ctors = new(); - - public static SerializerBinding Create() - where TSerializer : SerializerBinding, new() - { - return new TSerializer(); - } - - public static SerializerBinding Create(Type serializerType) - { - if (!typeof(SerializerBinding).IsAssignableFrom(serializerType)) - { - throw new InvalidDataException($"{serializerType} is not a serializer binding"); - } - - return Ctors.GetOrAdd(serializerType, BuildCtor)(); - } - - private static Func BuildCtor(Type serializerType) - { - try - { - NewExpression body = Expression.New(serializerType); - return Expression.Lambda>(body).Compile(); - } - catch (Exception ex) - { - throw new InvalidDataException($"failed to build serializer constructor for {serializerType}: {ex.Message}"); - } - } -} diff --git a/csharp/src/Fory/UnionSerializer.cs b/csharp/src/Fory/UnionSerializer.cs index 4b965866c8..0642e419f0 100644 --- a/csharp/src/Fory/UnionSerializer.cs +++ b/csharp/src/Fory/UnionSerializer.cs @@ -20,7 +20,7 @@ namespace Apache.Fory; -public sealed class UnionSerializer : TypedSerializer +public sealed class UnionSerializer : Serializer where TUnion : Union { private static readonly Func Factory = BuildFactory(); diff --git a/csharp/tests/Fory.XlangPeer/Program.cs b/csharp/tests/Fory.XlangPeer/Program.cs index 132adf91c6..b9a70cd999 100644 --- a/csharp/tests/Fory.XlangPeer/Program.cs +++ b/csharp/tests/Fory.XlangPeer/Program.cs @@ -1025,7 +1025,7 @@ public sealed class MyExt public int Id { get; set; } } -public sealed class MyExtSerializer : TypedSerializer +public sealed class MyExtSerializer : Serializer { public override TypeId StaticTypeId => TypeId.Ext; public override bool IsNullableType => true; From 82cde86e7164c9f86e51572c2443a07ec9e0d04e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 22 Feb 2026 13:38:25 +0800 Subject: [PATCH 15/16] refactor(csharp): split primitive dictionary serializers --- .../Fory/PrimitiveCollectionSerializers.cs | 389 ----------------- .../Fory/PrimitiveDictionarySerializers.cs | 410 ++++++++++++++++++ 2 files changed, 410 insertions(+), 389 deletions(-) create mode 100644 csharp/src/Fory/PrimitiveDictionarySerializers.cs diff --git a/csharp/src/Fory/PrimitiveCollectionSerializers.cs b/csharp/src/Fory/PrimitiveCollectionSerializers.cs index 38fbe54285..36fa786146 100644 --- a/csharp/src/Fory/PrimitiveCollectionSerializers.cs +++ b/csharp/src/Fory/PrimitiveCollectionSerializers.cs @@ -45,24 +45,6 @@ public static void WriteListHeader(ref WriteContext context, int count, bool has context.Writer.WriteUInt8((byte)elementTypeId); } } - - public static void WriteMapChunkTypeInfo( - ref WriteContext context, - bool keyDeclared, - bool valueDeclared, - TypeId keyTypeId, - TypeId valueTypeId) - { - if (!keyDeclared) - { - context.Writer.WriteUInt8((byte)keyTypeId); - } - - if (!valueDeclared) - { - context.Writer.WriteUInt8((byte)valueTypeId); - } - } } internal sealed class ListBoolSerializer : Serializer> @@ -346,374 +328,3 @@ public override List ReadData(ref ReadContext context) return Fallback.ReadData(ref context); } } - -internal sealed class DictionaryStringStringSerializer : Serializer> -{ - private static readonly DictionarySerializer Fallback = new(); - - public override TypeId StaticTypeId => TypeId.Map; - - public override bool IsNullableType => true; - - public override bool IsReferenceTrackableType => true; - - public override Dictionary DefaultValue => null!; - - public override bool IsNone(in Dictionary value) => value is null; - - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Dictionary map = value ?? []; - if (ContainsNull(map)) - { - Fallback.WriteData(ref context, map, hasGenerics); - return; - } - - WriteMapStringString(ref context, map, hasGenerics); - } - - public override Dictionary ReadData(ref ReadContext context) - { - return Fallback.ReadData(ref context); - } - - private static bool ContainsNull(Dictionary map) - { - foreach (KeyValuePair pair in map) - { - if (pair.Key is null || pair.Value is null) - { - return true; - } - } - - return false; - } - - private static void WriteMapStringString(ref WriteContext context, Dictionary map, bool hasGenerics) - { - KeyValuePair[] pairs = [.. map]; - context.Writer.WriteVarUInt32((uint)pairs.Length); - if (pairs.Length == 0) - { - return; - } - - bool keyDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); - bool valueDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); - int index = 0; - while (index < pairs.Length) - { - int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); - byte header = 0; - if (keyDeclared) - { - header |= DictionaryBits.DeclaredKeyType; - } - - if (valueDeclared) - { - header |= DictionaryBits.DeclaredValueType; - } - - context.Writer.WriteUInt8(header); - context.Writer.WriteUInt8((byte)chunkSize); - PrimitiveCollectionHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, TypeId.String, TypeId.String); - for (int i = 0; i < chunkSize; i++) - { - KeyValuePair pair = pairs[index + i]; - StringSerializer.WriteString(ref context, pair.Key); - StringSerializer.WriteString(ref context, pair.Value); - } - - index += chunkSize; - } - } -} - -internal sealed class DictionaryStringIntSerializer : Serializer> -{ - private static readonly DictionarySerializer Fallback = new(); - - public override TypeId StaticTypeId => TypeId.Map; - - public override bool IsNullableType => true; - - public override bool IsReferenceTrackableType => true; - - public override Dictionary DefaultValue => null!; - - public override bool IsNone(in Dictionary value) => value is null; - - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Dictionary map = value ?? []; - PrimitiveDictionaryWriter.WriteMapStringValue( - ref context, - map, - hasGenerics, - TypeId.VarInt32, - static (writer, valueItem) => writer.WriteVarInt32(valueItem)); - } - - public override Dictionary ReadData(ref ReadContext context) - { - return Fallback.ReadData(ref context); - } -} - -internal sealed class DictionaryStringLongSerializer : Serializer> -{ - private static readonly DictionarySerializer Fallback = new(); - - public override TypeId StaticTypeId => TypeId.Map; - - public override bool IsNullableType => true; - - public override bool IsReferenceTrackableType => true; - - public override Dictionary DefaultValue => null!; - - public override bool IsNone(in Dictionary value) => value is null; - - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Dictionary map = value ?? []; - PrimitiveDictionaryWriter.WriteMapStringValue( - ref context, - map, - hasGenerics, - TypeId.VarInt64, - static (writer, valueItem) => writer.WriteVarInt64(valueItem)); - } - - public override Dictionary ReadData(ref ReadContext context) - { - return Fallback.ReadData(ref context); - } -} - -internal sealed class DictionaryStringBoolSerializer : Serializer> -{ - private static readonly DictionarySerializer Fallback = new(); - - public override TypeId StaticTypeId => TypeId.Map; - - public override bool IsNullableType => true; - - public override bool IsReferenceTrackableType => true; - - public override Dictionary DefaultValue => null!; - - public override bool IsNone(in Dictionary value) => value is null; - - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Dictionary map = value ?? []; - PrimitiveDictionaryWriter.WriteMapStringValue( - ref context, - map, - hasGenerics, - TypeId.Bool, - static (writer, valueItem) => writer.WriteUInt8(valueItem ? (byte)1 : (byte)0)); - } - - public override Dictionary ReadData(ref ReadContext context) - { - return Fallback.ReadData(ref context); - } -} - -internal sealed class DictionaryStringDoubleSerializer : Serializer> -{ - private static readonly DictionarySerializer Fallback = new(); - - public override TypeId StaticTypeId => TypeId.Map; - - public override bool IsNullableType => true; - - public override bool IsReferenceTrackableType => true; - - public override Dictionary DefaultValue => null!; - - public override bool IsNone(in Dictionary value) => value is null; - - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Dictionary map = value ?? []; - PrimitiveDictionaryWriter.WriteMapStringValue( - ref context, - map, - hasGenerics, - TypeId.Float64, - static (writer, valueItem) => writer.WriteFloat64(valueItem)); - } - - public override Dictionary ReadData(ref ReadContext context) - { - return Fallback.ReadData(ref context); - } -} - -internal sealed class DictionaryIntIntSerializer : Serializer> -{ - private static readonly DictionarySerializer Fallback = new(); - - public override TypeId StaticTypeId => TypeId.Map; - - public override bool IsNullableType => true; - - public override bool IsReferenceTrackableType => true; - - public override Dictionary DefaultValue => null!; - - public override bool IsNone(in Dictionary value) => value is null; - - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Dictionary map = value ?? []; - PrimitiveDictionaryWriter.WriteMap( - ref context, - map, - hasGenerics, - TypeId.VarInt32, - static (writer, key) => writer.WriteVarInt32(key), - TypeId.VarInt32, - static (writer, valueItem) => writer.WriteVarInt32(valueItem)); - } - - public override Dictionary ReadData(ref ReadContext context) - { - return Fallback.ReadData(ref context); - } -} - -internal sealed class DictionaryLongLongSerializer : Serializer> -{ - private static readonly DictionarySerializer Fallback = new(); - - public override TypeId StaticTypeId => TypeId.Map; - - public override bool IsNullableType => true; - - public override bool IsReferenceTrackableType => true; - - public override Dictionary DefaultValue => null!; - - public override bool IsNone(in Dictionary value) => value is null; - - public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) - { - Dictionary map = value ?? []; - PrimitiveDictionaryWriter.WriteMap( - ref context, - map, - hasGenerics, - TypeId.VarInt64, - static (writer, key) => writer.WriteVarInt64(key), - TypeId.VarInt64, - static (writer, valueItem) => writer.WriteVarInt64(valueItem)); - } - - public override Dictionary ReadData(ref ReadContext context) - { - return Fallback.ReadData(ref context); - } -} - -internal static class PrimitiveDictionaryWriter -{ - public static void WriteMapStringValue( - ref WriteContext context, - Dictionary map, - bool hasGenerics, - TypeId valueTypeId, - Action writeValue) - { - KeyValuePair[] pairs = [.. map]; - context.Writer.WriteVarUInt32((uint)pairs.Length); - if (pairs.Length == 0) - { - return; - } - - bool keyDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); - bool valueDeclared = hasGenerics && !valueTypeId.NeedsTypeInfoForField(); - int index = 0; - while (index < pairs.Length) - { - int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); - byte header = 0; - if (keyDeclared) - { - header |= DictionaryBits.DeclaredKeyType; - } - - if (valueDeclared) - { - header |= DictionaryBits.DeclaredValueType; - } - - context.Writer.WriteUInt8(header); - context.Writer.WriteUInt8((byte)chunkSize); - PrimitiveCollectionHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, TypeId.String, valueTypeId); - for (int i = 0; i < chunkSize; i++) - { - KeyValuePair pair = pairs[index + i]; - StringSerializer.WriteString(ref context, pair.Key); - writeValue(context.Writer, pair.Value); - } - - index += chunkSize; - } - } - - public static void WriteMap( - ref WriteContext context, - Dictionary map, - bool hasGenerics, - TypeId keyTypeId, - Action writeKey, - TypeId valueTypeId, - Action writeValue) - where TKey : notnull - { - KeyValuePair[] pairs = [.. map]; - context.Writer.WriteVarUInt32((uint)pairs.Length); - if (pairs.Length == 0) - { - return; - } - - bool keyDeclared = hasGenerics && !keyTypeId.NeedsTypeInfoForField(); - bool valueDeclared = hasGenerics && !valueTypeId.NeedsTypeInfoForField(); - int index = 0; - while (index < pairs.Length) - { - int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); - byte header = 0; - if (keyDeclared) - { - header |= DictionaryBits.DeclaredKeyType; - } - - if (valueDeclared) - { - header |= DictionaryBits.DeclaredValueType; - } - - context.Writer.WriteUInt8(header); - context.Writer.WriteUInt8((byte)chunkSize); - PrimitiveCollectionHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, keyTypeId, valueTypeId); - for (int i = 0; i < chunkSize; i++) - { - KeyValuePair pair = pairs[index + i]; - writeKey(context.Writer, pair.Key); - writeValue(context.Writer, pair.Value); - } - - index += chunkSize; - } - } -} diff --git a/csharp/src/Fory/PrimitiveDictionarySerializers.cs b/csharp/src/Fory/PrimitiveDictionarySerializers.cs new file mode 100644 index 0000000000..724174b354 --- /dev/null +++ b/csharp/src/Fory/PrimitiveDictionarySerializers.cs @@ -0,0 +1,410 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +namespace Apache.Fory; + +internal static class PrimitiveDictionaryHeader +{ + public static void WriteMapChunkTypeInfo( + ref WriteContext context, + bool keyDeclared, + bool valueDeclared, + TypeId keyTypeId, + TypeId valueTypeId) + { + if (!keyDeclared) + { + context.Writer.WriteUInt8((byte)keyTypeId); + } + + if (!valueDeclared) + { + context.Writer.WriteUInt8((byte)valueTypeId); + } + } +} + +internal sealed class DictionaryStringStringSerializer : Serializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + if (ContainsNull(map)) + { + Fallback.WriteData(ref context, map, hasGenerics); + return; + } + + WriteMapStringString(ref context, map, hasGenerics); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } + + private static bool ContainsNull(Dictionary map) + { + foreach (KeyValuePair pair in map) + { + if (pair.Key is null || pair.Value is null) + { + return true; + } + } + + return false; + } + + private static void WriteMapStringString(ref WriteContext context, Dictionary map, bool hasGenerics) + { + KeyValuePair[] pairs = [.. map]; + context.Writer.WriteVarUInt32((uint)pairs.Length); + if (pairs.Length == 0) + { + return; + } + + bool keyDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); + int index = 0; + while (index < pairs.Length) + { + int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); + byte header = 0; + if (keyDeclared) + { + header |= DictionaryBits.DeclaredKeyType; + } + + if (valueDeclared) + { + header |= DictionaryBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + context.Writer.WriteUInt8((byte)chunkSize); + PrimitiveDictionaryHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, TypeId.String, TypeId.String); + for (int i = 0; i < chunkSize; i++) + { + KeyValuePair pair = pairs[index + i]; + StringSerializer.WriteString(ref context, pair.Key); + StringSerializer.WriteString(ref context, pair.Value); + } + + index += chunkSize; + } + } +} + +internal sealed class DictionaryStringIntSerializer : Serializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.VarInt32, + static (writer, valueItem) => writer.WriteVarInt32(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryStringLongSerializer : Serializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.VarInt64, + static (writer, valueItem) => writer.WriteVarInt64(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryStringBoolSerializer : Serializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.Bool, + static (writer, valueItem) => writer.WriteUInt8(valueItem ? (byte)1 : (byte)0)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryStringDoubleSerializer : Serializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMapStringValue( + ref context, + map, + hasGenerics, + TypeId.Float64, + static (writer, valueItem) => writer.WriteFloat64(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryIntIntSerializer : Serializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMap( + ref context, + map, + hasGenerics, + TypeId.VarInt32, + static (writer, key) => writer.WriteVarInt32(key), + TypeId.VarInt32, + static (writer, valueItem) => writer.WriteVarInt32(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal sealed class DictionaryLongLongSerializer : Serializer> +{ + private static readonly DictionarySerializer Fallback = new(); + + public override TypeId StaticTypeId => TypeId.Map; + + public override bool IsNullableType => true; + + public override bool IsReferenceTrackableType => true; + + public override Dictionary DefaultValue => null!; + + public override bool IsNone(in Dictionary value) => value is null; + + public override void WriteData(ref WriteContext context, in Dictionary value, bool hasGenerics) + { + Dictionary map = value ?? []; + PrimitiveDictionaryWriter.WriteMap( + ref context, + map, + hasGenerics, + TypeId.VarInt64, + static (writer, key) => writer.WriteVarInt64(key), + TypeId.VarInt64, + static (writer, valueItem) => writer.WriteVarInt64(valueItem)); + } + + public override Dictionary ReadData(ref ReadContext context) + { + return Fallback.ReadData(ref context); + } +} + +internal static class PrimitiveDictionaryWriter +{ + public static void WriteMapStringValue( + ref WriteContext context, + Dictionary map, + bool hasGenerics, + TypeId valueTypeId, + Action writeValue) + { + KeyValuePair[] pairs = [.. map]; + context.Writer.WriteVarUInt32((uint)pairs.Length); + if (pairs.Length == 0) + { + return; + } + + bool keyDeclared = hasGenerics && !TypeId.String.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !valueTypeId.NeedsTypeInfoForField(); + int index = 0; + while (index < pairs.Length) + { + int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); + byte header = 0; + if (keyDeclared) + { + header |= DictionaryBits.DeclaredKeyType; + } + + if (valueDeclared) + { + header |= DictionaryBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + context.Writer.WriteUInt8((byte)chunkSize); + PrimitiveDictionaryHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, TypeId.String, valueTypeId); + for (int i = 0; i < chunkSize; i++) + { + KeyValuePair pair = pairs[index + i]; + StringSerializer.WriteString(ref context, pair.Key); + writeValue(context.Writer, pair.Value); + } + + index += chunkSize; + } + } + + public static void WriteMap( + ref WriteContext context, + Dictionary map, + bool hasGenerics, + TypeId keyTypeId, + Action writeKey, + TypeId valueTypeId, + Action writeValue) + where TKey : notnull + { + KeyValuePair[] pairs = [.. map]; + context.Writer.WriteVarUInt32((uint)pairs.Length); + if (pairs.Length == 0) + { + return; + } + + bool keyDeclared = hasGenerics && !keyTypeId.NeedsTypeInfoForField(); + bool valueDeclared = hasGenerics && !valueTypeId.NeedsTypeInfoForField(); + int index = 0; + while (index < pairs.Length) + { + int chunkSize = Math.Min(byte.MaxValue, pairs.Length - index); + byte header = 0; + if (keyDeclared) + { + header |= DictionaryBits.DeclaredKeyType; + } + + if (valueDeclared) + { + header |= DictionaryBits.DeclaredValueType; + } + + context.Writer.WriteUInt8(header); + context.Writer.WriteUInt8((byte)chunkSize); + PrimitiveDictionaryHeader.WriteMapChunkTypeInfo(ref context, keyDeclared, valueDeclared, keyTypeId, valueTypeId); + for (int i = 0; i < chunkSize; i++) + { + KeyValuePair pair = pairs[index + i]; + writeKey(context.Writer, pair.Key); + writeValue(context.Writer, pair.Value); + } + + index += chunkSize; + } + } +} From 47a29c98b9f08251a56f31a674a7adf3dd0d1750 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 22 Feb 2026 13:49:40 +0800 Subject: [PATCH 16/16] feat(csharp): add adaptive string encoding serializer --- csharp/src/Fory/StringSerializer.cs | 90 ++++++++++++++++++++- csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 57 +++++++++++++ 2 files changed, 143 insertions(+), 4 deletions(-) diff --git a/csharp/src/Fory/StringSerializer.cs b/csharp/src/Fory/StringSerializer.cs index 794518b2f2..289de7c0ce 100644 --- a/csharp/src/Fory/StringSerializer.cs +++ b/csharp/src/Fory/StringSerializer.cs @@ -43,10 +43,21 @@ public override string ReadData(ref ReadContext context) public static void WriteString(ref WriteContext context, string value) { string safe = value ?? string.Empty; - byte[] utf8 = Encoding.UTF8.GetBytes(safe); - ulong header = ((ulong)utf8.Length << 2) | (ulong)ForyStringEncoding.Utf8; - context.Writer.WriteVarUInt36Small(header); - context.Writer.WriteBytes(utf8); + ForyStringEncoding encoding = SelectEncoding(safe); + switch (encoding) + { + case ForyStringEncoding.Latin1: + WriteLatin1(ref context, safe); + break; + case ForyStringEncoding.Utf8: + WriteUtf8(ref context, safe); + break; + case ForyStringEncoding.Utf16: + WriteUtf16(ref context, safe); + break; + default: + throw new EncodingException($"unsupported string encoding {encoding}"); + } } public static string ReadString(ref ReadContext context) @@ -84,4 +95,75 @@ private static string DecodeUtf16(byte[] bytes) return Encoding.Unicode.GetString(bytes); } + + private static ForyStringEncoding SelectEncoding(string value) + { + int numChars = value.Length; + int sampleNum = Math.Min(64, numChars); + int asciiCount = 0; + int latin1Count = 0; + for (int i = 0; i < sampleNum; i++) + { + char c = value[i]; + if (c < 0x80) + { + asciiCount++; + latin1Count++; + } + else if (c <= 0xFF) + { + latin1Count++; + } + } + + if (latin1Count == numChars || (latin1Count == sampleNum && IsLatin(value, sampleNum))) + { + return ForyStringEncoding.Latin1; + } + + return asciiCount * 2 >= sampleNum ? ForyStringEncoding.Utf8 : ForyStringEncoding.Utf16; + } + + private static bool IsLatin(string value, int start) + { + for (int i = start; i < value.Length; i++) + { + if (value[i] > 0xFF) + { + return false; + } + } + + return true; + } + + private static void WriteLatin1(ref WriteContext context, string value) + { + byte[] latin1 = new byte[value.Length]; + for (int i = 0; i < value.Length; i++) + { + latin1[i] = unchecked((byte)value[i]); + } + + WriteEncodedBytes(ref context, latin1, ForyStringEncoding.Latin1); + } + + private static void WriteUtf8(ref WriteContext context, string value) + { + byte[] utf8 = Encoding.UTF8.GetBytes(value); + WriteEncodedBytes(ref context, utf8, ForyStringEncoding.Utf8); + } + + private static void WriteUtf16(ref WriteContext context, string value) + { + byte[] utf16 = Encoding.Unicode.GetBytes(value); + WriteEncodedBytes(ref context, utf16, ForyStringEncoding.Utf16); + } + + private static void WriteEncodedBytes(ref WriteContext context, byte[] bytes, ForyStringEncoding encoding) + { + ulong header = ((ulong)bytes.Length << 2) | (ulong)encoding; + context.Writer.WriteVarUInt36Small(header); + context.Writer.WriteBytes(bytes); + } } diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs index f497249f7c..c5a399b8ad 100644 --- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs +++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs @@ -118,6 +118,10 @@ public sealed class DynamicAnyHolder public sealed class ForyRuntimeTests { + private const ulong StringEncodingLatin1 = 0; + private const ulong StringEncodingUtf16 = 1; + private const ulong StringEncodingUtf8 = 2; + [Fact] public void PrimitiveRoundTrip() { @@ -510,4 +514,57 @@ public void GeneratedSerializerSupportsObjectKeyMap() Assert.Equal("n", nested[0]); Assert.Equal(1, nested[1]); } + + [Fact] + public void StringSerializerUsesLatin1WhenAllCharsAreLatin1() + { + (ulong encoding, string decoded) = WriteAndReadString("Hello\u00E9\u00FF"); + Assert.Equal(StringEncodingLatin1, encoding); + Assert.Equal("Hello\u00E9\u00FF", decoded); + } + + [Fact] + public void StringSerializerUsesUtf8WhenAsciiRatioIsHigh() + { + (ulong encoding, string decoded) = WriteAndReadString("abc\u4E16\u754C"); + Assert.Equal(StringEncodingUtf8, encoding); + Assert.Equal("abc\u4E16\u754C", decoded); + } + + [Fact] + public void StringSerializerUsesUtf16WhenAsciiRatioIsLow() + { + (ulong encoding, string decoded) = WriteAndReadString("\u4F60\u597D\u4E16\u754Ca"); + Assert.Equal(StringEncodingUtf16, encoding); + Assert.Equal("\u4F60\u597D\u4E16\u754Ca", decoded); + } + + [Fact] + public void StringSerializerValidatesBeyondSampleForLatin1() + { + string value = new string('a', 64) + "\u4E16"; + (ulong encoding, string decoded) = WriteAndReadString(value); + Assert.Equal(StringEncodingUtf8, encoding); + Assert.Equal(value, decoded); + } + + private static (ulong Encoding, string Decoded) WriteAndReadString(string value) + { + ByteWriter writer = new(); + TypeResolver resolver = new(); + WriteContext writeContext = new(writer, resolver, trackRef: false, compatible: false); + StringSerializer.WriteString(ref writeContext, value); + + byte[] payload = writer.ToArray(); + ByteReader headerReader = new(payload); + ulong header = headerReader.ReadVarUInt36Small(); + ulong encoding = header & 0x03; + int byteLength = checked((int)(header >> 2)); + Assert.Equal(payload.Length - headerReader.Cursor, byteLength); + + ReadContext readContext = new(new ByteReader(payload), resolver, trackRef: false, compatible: false); + string decoded = StringSerializer.ReadString(ref readContext); + Assert.Equal(0, readContext.Reader.Remaining); + return (encoding, decoded); + } }