This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 03c4c57a4 feat(csharp): support nested container field codec (#3639)
03c4c57a4 is described below
commit 03c4c57a4f45b0b31a5c4a37aa8417af5c45b6b6
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Apr 30 00:27:14 2026 +0800
feat(csharp): support nested container field codec (#3639)
## Why?
C# generated serializers need field-level schema descriptors that can
describe the exact Fory wire type for scalars and nested containers. The
previous `[Field(Encoding = ...)]` model only handled a narrow
integer-encoding override, which was not expressive enough for nested
annotated container metadata or compatible-mode skipping based on remote
field metadata.
## What does this PR do?
- Replaces the C# `[Field]` / `FieldEncoding` API with `[ForyField]`,
supporting optional stable field ids and `Type = typeof(S...)` schema
descriptors.
- Adds `Apache.Fory.Schema.Types` descriptor marker types for scalar,
packed-array, list, set, and map field metadata.
- Extends the C# source generator to emit descriptor-driven field codecs
for scalar, list, and map payloads, including nested generics and
nullability-aware generated read/write helpers.
- Updates compatible field skipping so C# consumes payloads according to
remote nested field metadata, including scalar, packed array, list/set,
and map descriptors.
- Updates the Fory compiler C# generator to emit `[ForyField(Type =
typeof(...))]` hints, including nested map/list descriptors and
reduced-precision `Half` / `BFloat16` carriers.
- Adds C# runtime, compiler-generator, and xlang peer coverage for
unsigned schema descriptors and nested annotated containers, and moves
the nested annotated container xlang checks to `CSharpXlangTest`.
- Refreshes the C# README and field-configuration guide for the new
`[ForyField]` descriptor API.
## Related issues
#1017
#3630
#3625
#3630
#3636
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
benchmarks/csharp/BenchmarkModels.cs | 104 +-
compiler/fory_compiler/generators/csharp.py | 81 +-
.../fory_compiler/tests/test_csharp_generator.py | 45 +-
csharp/README.md | 1 +
csharp/src/Fory.Generator/ForyObjectGenerator.cs | 1215 ++++++++++++++++++--
csharp/src/Fory/Attributes.cs | 65 +-
csharp/src/Fory/FieldSkipper.cs | 275 +++--
csharp/src/Fory/SchemaTypes.cs | 62 +
csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 128 ++-
csharp/tests/Fory.XlangPeer/Program.cs | 61 +-
docs/guide/csharp/field-configuration.md | 57 +-
docs/guide/csharp/index.md | 26 +-
.../java/org/apache/fory/xlang/CPPXlangTest.java | 10 +-
.../org/apache/fory/xlang/CSharpXlangTest.java | 12 +
14 files changed, 1853 insertions(+), 289 deletions(-)
diff --git a/benchmarks/csharp/BenchmarkModels.cs
b/benchmarks/csharp/BenchmarkModels.cs
index f3c2fc7c0..abf945be5 100644
--- a/benchmarks/csharp/BenchmarkModels.cs
+++ b/benchmarks/csharp/BenchmarkModels.cs
@@ -26,35 +26,35 @@ namespace Apache.Fory.Benchmarks.CSharp;
[ProtoContract]
public sealed class NumericStruct
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public int F1 { get; set; }
- [Field(Id = 2)]
+ [ForyField(Id = 2)]
[ProtoMember(2)]
public int F2 { get; set; }
- [Field(Id = 3)]
+ [ForyField(Id = 3)]
[ProtoMember(3)]
public int F3 { get; set; }
- [Field(Id = 4)]
+ [ForyField(Id = 4)]
[ProtoMember(4)]
public int F4 { get; set; }
- [Field(Id = 5)]
+ [ForyField(Id = 5)]
[ProtoMember(5)]
public int F5 { get; set; }
- [Field(Id = 6)]
+ [ForyField(Id = 6)]
[ProtoMember(6)]
public int F6 { get; set; }
- [Field(Id = 7)]
+ [ForyField(Id = 7)]
[ProtoMember(7)]
public int F7 { get; set; }
- [Field(Id = 8)]
+ [ForyField(Id = 8)]
[ProtoMember(8)]
public int F8 { get; set; }
}
@@ -64,7 +64,7 @@ public sealed class NumericStruct
[ProtoContract]
public sealed class StructList
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public List<NumericStruct> Values { get; set; } = [];
}
@@ -74,91 +74,91 @@ public sealed class StructList
[ProtoContract]
public sealed class Sample
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public int IntValue { get; set; }
- [Field(Id = 2)]
+ [ForyField(Id = 2)]
[ProtoMember(2)]
public long LongValue { get; set; }
- [Field(Id = 3)]
+ [ForyField(Id = 3)]
[ProtoMember(3)]
public float FloatValue { get; set; }
- [Field(Id = 4)]
+ [ForyField(Id = 4)]
[ProtoMember(4)]
public double DoubleValue { get; set; }
- [Field(Id = 5)]
+ [ForyField(Id = 5)]
[ProtoMember(5)]
public int ShortValue { get; set; }
- [Field(Id = 6)]
+ [ForyField(Id = 6)]
[ProtoMember(6)]
public int CharValue { get; set; }
- [Field(Id = 7)]
+ [ForyField(Id = 7)]
[ProtoMember(7)]
public bool BooleanValue { get; set; }
- [Field(Id = 8)]
+ [ForyField(Id = 8)]
[ProtoMember(8)]
public int IntValueBoxed { get; set; }
- [Field(Id = 9)]
+ [ForyField(Id = 9)]
[ProtoMember(9)]
public long LongValueBoxed { get; set; }
- [Field(Id = 10)]
+ [ForyField(Id = 10)]
[ProtoMember(10)]
public float FloatValueBoxed { get; set; }
- [Field(Id = 11)]
+ [ForyField(Id = 11)]
[ProtoMember(11)]
public double DoubleValueBoxed { get; set; }
- [Field(Id = 12)]
+ [ForyField(Id = 12)]
[ProtoMember(12)]
public int ShortValueBoxed { get; set; }
- [Field(Id = 13)]
+ [ForyField(Id = 13)]
[ProtoMember(13)]
public int CharValueBoxed { get; set; }
- [Field(Id = 14)]
+ [ForyField(Id = 14)]
[ProtoMember(14)]
public bool BooleanValueBoxed { get; set; }
- [Field(Id = 15)]
+ [ForyField(Id = 15)]
[ProtoMember(15)]
public int[] IntArray { get; set; } = [];
- [Field(Id = 16)]
+ [ForyField(Id = 16)]
[ProtoMember(16)]
public long[] LongArray { get; set; } = [];
- [Field(Id = 17)]
+ [ForyField(Id = 17)]
[ProtoMember(17)]
public float[] FloatArray { get; set; } = [];
- [Field(Id = 18)]
+ [ForyField(Id = 18)]
[ProtoMember(18)]
public double[] DoubleArray { get; set; } = [];
- [Field(Id = 19)]
+ [ForyField(Id = 19)]
[ProtoMember(19)]
public int[] ShortArray { get; set; } = [];
- [Field(Id = 20)]
+ [ForyField(Id = 20)]
[ProtoMember(20)]
public int[] CharArray { get; set; } = [];
- [Field(Id = 21)]
+ [ForyField(Id = 21)]
[ProtoMember(21)]
public bool[] BooleanArray { get; set; } = [];
- [Field(Id = 22)]
+ [ForyField(Id = 22)]
[ProtoMember(22)]
public string String { get; set; } = string.Empty;
}
@@ -168,7 +168,7 @@ public sealed class Sample
[ProtoContract]
public sealed class SampleList
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public List<Sample> Values { get; set; } = [];
}
@@ -198,51 +198,51 @@ public enum MediaSize
[ProtoContract]
public sealed class Media
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public string Uri { get; set; } = string.Empty;
- [Field(Id = 2)]
+ [ForyField(Id = 2)]
[ProtoMember(2)]
public string Title { get; set; } = string.Empty;
- [Field(Id = 3)]
+ [ForyField(Id = 3)]
[ProtoMember(3)]
public int Width { get; set; }
- [Field(Id = 4)]
+ [ForyField(Id = 4)]
[ProtoMember(4)]
public int Height { get; set; }
- [Field(Id = 5)]
+ [ForyField(Id = 5)]
[ProtoMember(5)]
public string Format { get; set; } = string.Empty;
- [Field(Id = 6)]
+ [ForyField(Id = 6)]
[ProtoMember(6)]
public long Duration { get; set; }
- [Field(Id = 7)]
+ [ForyField(Id = 7)]
[ProtoMember(7)]
public long Size { get; set; }
- [Field(Id = 8)]
+ [ForyField(Id = 8)]
[ProtoMember(8)]
public int Bitrate { get; set; }
- [Field(Id = 9)]
+ [ForyField(Id = 9)]
[ProtoMember(9)]
public bool HasBitrate { get; set; }
- [Field(Id = 10)]
+ [ForyField(Id = 10)]
[ProtoMember(10)]
public List<string> Persons { get; set; } = [];
- [Field(Id = 11)]
+ [ForyField(Id = 11)]
[ProtoMember(11)]
public Player Player { get; set; }
- [Field(Id = 12)]
+ [ForyField(Id = 12)]
[ProtoMember(12)]
public string Copyright { get; set; } = string.Empty;
}
@@ -252,23 +252,23 @@ public sealed class Media
[ProtoContract]
public sealed class Image
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public string Uri { get; set; } = string.Empty;
- [Field(Id = 2)]
+ [ForyField(Id = 2)]
[ProtoMember(2)]
public string Title { get; set; } = string.Empty;
- [Field(Id = 3)]
+ [ForyField(Id = 3)]
[ProtoMember(3)]
public int Width { get; set; }
- [Field(Id = 4)]
+ [ForyField(Id = 4)]
[ProtoMember(4)]
public int Height { get; set; }
- [Field(Id = 5)]
+ [ForyField(Id = 5)]
[ProtoMember(5)]
public MediaSize Size { get; set; }
}
@@ -278,11 +278,11 @@ public sealed class Image
[ProtoContract]
public sealed class MediaContent
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public Media Media { get; set; } = new();
- [Field(Id = 2)]
+ [ForyField(Id = 2)]
[ProtoMember(2)]
public List<Image> Images { get; set; } = [];
}
@@ -292,7 +292,7 @@ public sealed class MediaContent
[ProtoContract]
public sealed class MediaContentList
{
- [Field(Id = 1)]
+ [ForyField(Id = 1)]
[ProtoMember(1)]
public List<MediaContent> Values { get; set; } = [];
}
diff --git a/compiler/fory_compiler/generators/csharp.py
b/compiler/fory_compiler/generators/csharp.py
index fc9fdcd81..8b4902539 100644
--- a/compiler/fory_compiler/generators/csharp.py
+++ b/compiler/fory_compiler/generators/csharp.py
@@ -59,8 +59,8 @@ class CSharpGenerator(BaseGenerator):
PrimitiveKind.UINT64: "ulong",
PrimitiveKind.VAR_UINT64: "ulong",
PrimitiveKind.TAGGED_UINT64: "ulong",
- PrimitiveKind.FLOAT16: "float",
- PrimitiveKind.BFLOAT16: "float",
+ PrimitiveKind.FLOAT16: "Half",
+ PrimitiveKind.BFLOAT16: "BFloat16",
PrimitiveKind.FLOAT32: "float",
PrimitiveKind.FLOAT64: "double",
PrimitiveKind.STRING: "string",
@@ -401,6 +401,7 @@ class CSharpGenerator(BaseGenerator):
lines.append("using System;")
lines.append("using System.Collections.Generic;")
lines.append("using Apache.Fory;")
+ lines.append("using S = Apache.Fory.Schema.Types;")
lines.append("")
lines.append(f"namespace {namespace_name};")
lines.append("")
@@ -573,20 +574,74 @@ class CSharpGenerator(BaseGenerator):
return None
- def _field_encoding(self, field: Field) -> Optional[str]:
- field_type = field.field_type
- if not isinstance(field_type, PrimitiveType):
- return None
- kind = field_type.kind
+ def _schema_type_hint(
+ self, field_type: FieldType, force: bool = False
+ ) -> Optional[str]:
+ if isinstance(field_type, PrimitiveType):
+ return self._primitive_schema_type_hint(field_type.kind, force)
+
+ if isinstance(field_type, ListType):
+ element_hint = self._schema_type_hint(field_type.element_type,
force=True)
+ if element_hint is None:
+ return None
+ return f"S.List<{element_hint}>"
+
+ if isinstance(field_type, MapType):
+ key_hint = self._schema_type_hint(field_type.key_type, force=True)
+ value_hint = self._schema_type_hint(field_type.value_type,
force=True)
+ if key_hint is None and value_hint is None:
+ return None
+ if key_hint is None:
+ key_hint = self._schema_type_hint(field_type.key_type,
force=True)
+ if value_hint is None:
+ value_hint = self._schema_type_hint(field_type.value_type,
force=True)
+ if key_hint is None or value_hint is None:
+ return None
+ return f"S.Map<{key_hint}, {value_hint}>"
+
+ return None
+
+ def _primitive_schema_type_hint(
+ self, kind: PrimitiveKind, force: bool
+ ) -> Optional[str]:
+ hints = {
+ PrimitiveKind.BOOL: "S.Bool",
+ PrimitiveKind.INT8: "S.Int8",
+ PrimitiveKind.INT16: "S.Int16",
+ PrimitiveKind.INT32: "S.Int32",
+ PrimitiveKind.VARINT32: "S.VarInt32",
+ PrimitiveKind.INT64: "S.Int64",
+ PrimitiveKind.VARINT64: "S.VarInt64",
+ PrimitiveKind.TAGGED_INT64: "S.TaggedInt64",
+ PrimitiveKind.UINT8: "S.UInt8",
+ PrimitiveKind.UINT16: "S.UInt16",
+ PrimitiveKind.UINT32: "S.UInt32",
+ PrimitiveKind.VAR_UINT32: "S.VarUInt32",
+ PrimitiveKind.UINT64: "S.UInt64",
+ PrimitiveKind.VAR_UINT64: "S.VarUInt64",
+ PrimitiveKind.TAGGED_UINT64: "S.TaggedUInt64",
+ PrimitiveKind.FLOAT16: "S.Float16",
+ PrimitiveKind.BFLOAT16: "S.BFloat16",
+ PrimitiveKind.FLOAT32: "S.Float32",
+ PrimitiveKind.FLOAT64: "S.Float64",
+ PrimitiveKind.STRING: "S.String",
+ PrimitiveKind.BYTES: "S.Binary",
+ PrimitiveKind.DATE: "S.Date",
+ PrimitiveKind.TIMESTAMP: "S.Timestamp",
+ PrimitiveKind.DURATION: "S.Duration",
+ PrimitiveKind.DECIMAL: "S.Decimal",
+ }
+ if force:
+ return hints.get(kind)
if kind in {
PrimitiveKind.INT32,
PrimitiveKind.INT64,
PrimitiveKind.UINT32,
PrimitiveKind.UINT64,
+ PrimitiveKind.TAGGED_INT64,
+ PrimitiveKind.TAGGED_UINT64,
}:
- return "Fixed"
- if kind in {PrimitiveKind.TAGGED_INT64, PrimitiveKind.TAGGED_UINT64}:
- return "Tagged"
+ return hints[kind]
return None
def _type_reference_for_local(
@@ -774,10 +829,10 @@ class CSharpGenerator(BaseGenerator):
used_field_names: Set[str] = set()
for field in message.fields:
lines.append("")
- encoding = self._field_encoding(field)
- if encoding:
+ schema_type = self._schema_type_hint(field.field_type)
+ if schema_type:
lines.append(
- f"{ind}{self.indent_str}[Field(Encoding =
FieldEncoding.{encoding})]"
+ f"{ind}{self.indent_str}[ForyField(Type =
typeof({schema_type}))]"
)
field_name = self._field_member_name(field, message,
used_field_names)
field_type = self.generate_type(
diff --git a/compiler/fory_compiler/tests/test_csharp_generator.py
b/compiler/fory_compiler/tests/test_csharp_generator.py
index a0181eeff..507cbc051 100644
--- a/compiler/fory_compiler/tests/test_csharp_generator.py
+++ b/compiler/fory_compiler/tests/test_csharp_generator.py
@@ -101,11 +101,52 @@ def test_csharp_field_encoding_attributes():
"""
)
- assert "[Field(Encoding = FieldEncoding.Fixed)]" in file.content
- assert "[Field(Encoding = FieldEncoding.Tagged)]" in file.content
+ assert "[ForyField(Type = typeof(S.Int32))]" in file.content
+ assert "[ForyField(Type = typeof(S.TaggedUInt64))]" in file.content
assert "public int Plain { get; set; }" in file.content
+def test_csharp_nested_schema_type_attributes():
+ file = generate(
+ """
+ package example;
+
+ message Nested {
+ map<fixed_uint32, list<optional tagged_uint64>> values = 1;
+ }
+ """
+ )
+
+ assert (
+ "[ForyField(Type = typeof(S.Map<S.UInt32, S.List<S.TaggedUInt64>>))]"
+ in file.content
+ )
+ assert (
+ "public Dictionary<uint, List<ulong?>> Values { get; set; } = new();"
+ in file.content
+ )
+
+
+def test_csharp_reduced_precision_carriers():
+ file = generate(
+ """
+ package example;
+
+ message Reduced {
+ float16 f16 = 1;
+ bfloat16 bf16 = 2;
+ list<float16> f16_values = 3;
+ list<bfloat16> bf16_values = 4;
+ }
+ """
+ )
+
+ assert "public Half F16 { get; set; }" in file.content
+ assert "public BFloat16 Bf16 { get; set; }" in file.content
+ assert "public List<Half> F16Values { get; set; } = new();" in file.content
+ assert "public List<BFloat16> Bf16Values { get; set; } = new();" in
file.content
+
+
def test_csharp_imported_registration_calls_generated():
repo_root = Path(__file__).resolve().parents[3]
idl_dir = repo_root / "integration_tests" / "idl_tests" / "idl"
diff --git a/csharp/README.md b/csharp/README.md
index f310a625e..db4fbd406 100644
--- a/csharp/README.md
+++ b/csharp/README.md
@@ -11,6 +11,7 @@ The C# implementation provides high-performance object graph
serialization for .
- High-performance binary serialization for .NET 8+
- Cross-language compatibility with Java, Python, C++, Go, Rust, and JavaScript
- Source-generator-based serializers for `[ForyObject]` types
+- Field-level schema descriptors with `[ForyField(Type = typeof(...))]`
- Optional shared/circular reference tracking (`TrackRef(true)`)
- Compatible mode for schema evolution
- Reduced-precision carriers for `Half` / `BFloat16` scalars and `Half[]` /
`List<Half>` / `BFloat16[]` / `List<BFloat16>` array payloads
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
index 3f9894e62..c5dc09d5c 100644
--- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -46,10 +46,10 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
- private static readonly DiagnosticDescriptor UnsupportedEncoding = new(
+ private static readonly DiagnosticDescriptor UnsupportedSchemaType = new(
id: "FORY003",
- title: "Unsupported Field encoding",
- messageFormat: "Member '{0}' uses unsupported [Field] encoding for
type '{1}'",
+ title: "Unsupported Fory field schema type",
+ messageFormat: "Member '{0}' uses unsupported [ForyField] schema
descriptor for type '{1}'",
category: "Fory",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
@@ -168,6 +168,14 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
sb.AppendLine(" return nullable ?
global::Apache.Fory.RefMode.NullOnly : global::Apache.Fory.RefMode.None;");
sb.AppendLine(" }");
sb.AppendLine();
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ if (member.FieldCodec is not null)
+ {
+ EmitFieldCodecMethods(sb, member);
+ }
+ }
+
sb.AppendLine(" private static bool
__ForyCanReadCompatiblePrimitive(global::Apache.Fory.TypeId typeId)");
sb.AppendLine(" {");
sb.AppendLine(" return typeId switch");
@@ -625,6 +633,596 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
sb.AppendLine("}");
}
+ private static void EmitFieldCodecMethods(StringBuilder sb, MemberModel
member)
+ {
+ FieldCodecModel codec = member.FieldCodec!;
+ string memberId = Sanitize(member.Name);
+ sb.AppendLine(
+ $" private static void
__ForyWrite{memberId}Field(global::Apache.Fory.WriteContext context,
{member.TypeName} value, global::Apache.Fory.RefMode refMode)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (refMode ==
global::Apache.Fory.RefMode.NullOnly)");
+ sb.AppendLine(" {");
+ if (member.IsNullableValueType)
+ {
+ sb.AppendLine(" if (!value.HasValue)");
+ }
+ else
+ {
+ sb.AppendLine(" if (value is null)");
+ }
+
+ sb.AppendLine(" {");
+ sb.AppendLine("
context.Writer.WriteInt8((sbyte)global::Apache.Fory.RefFlag.Null);");
+ sb.AppendLine(" return;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine("
context.Writer.WriteInt8((sbyte)global::Apache.Fory.RefFlag.NotNullValue);");
+ sb.AppendLine(" }");
+ string writeValueExpr = member.IsNullableValueType ? "value.Value" :
member.IsNullable ? "value!" : "value";
+ int id = 0;
+ EmitWritePayload(sb, codec, writeValueExpr, 2, ref id);
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ sb.AppendLine(
+ $" private static {member.TypeName}
__ForyRead{memberId}Field(global::Apache.Fory.ReadContext context,
global::Apache.Fory.RefMode refMode)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (refMode ==
global::Apache.Fory.RefMode.NullOnly)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" sbyte refFlag =
context.Reader.ReadInt8();");
+ sb.AppendLine(" if (refFlag ==
(sbyte)global::Apache.Fory.RefFlag.Null)");
+ sb.AppendLine(" {");
+ sb.AppendLine($" return ({member.TypeName})default!;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" if (refFlag !=
(sbyte)global::Apache.Fory.RefFlag.NotNullValue)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException($\"invalid nullOnly ref flag
{refFlag}\");");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ string resultVar = $"__{memberId}Value";
+ id = 0;
+ EmitReadPayload(sb, codec, resultVar, 2, ref id);
+ sb.AppendLine($" return {resultVar};");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ }
+
+ private static void EmitWritePayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string valueExpr,
+ int indentLevel,
+ ref int id)
+ {
+ string indent = new(' ', indentLevel * 4);
+ switch (codec.Kind)
+ {
+ case FieldCodecKind.Scalar:
+ if (!TryBuildDirectPayloadWrite(codec.TypeId, valueExpr, out
string? writeCode))
+ {
+
sb.AppendLine($"{indent}context.TypeResolver.GetSerializer<{codec.TypeName}>().WriteData(context,
{valueExpr}, false);");
+ return;
+ }
+
+ sb.AppendLine($"{indent}{writeCode}");
+ return;
+ case FieldCodecKind.PackedArray:
+ EmitWritePackedArrayPayload(sb, codec, valueExpr, indentLevel,
ref id);
+ return;
+ case FieldCodecKind.List:
+ EmitWriteCollectionPayload(sb, codec, valueExpr, indentLevel,
ref id, isSet: false);
+ return;
+ case FieldCodecKind.Set:
+ EmitWriteCollectionPayload(sb, codec, valueExpr, indentLevel,
ref id, isSet: true);
+ return;
+ case FieldCodecKind.Map:
+ EmitWriteMapPayload(sb, codec, valueExpr, indentLevel, ref id);
+ return;
+ }
+ }
+
+ private static void EmitWritePackedArrayPayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string valueExpr,
+ int indentLevel,
+ ref int id)
+ {
+ string indent = new(' ', indentLevel * 4);
+ string valuesVar = $"__foryPacked{id++}";
+ sb.AppendLine($"{indent}{codec.TypeName} {valuesVar} = {valueExpr} ??
[];");
+ string countExpr = codec.CarrierKind == CarrierKind.Array ?
$"{valuesVar}.Length" : $"{valuesVar}.Count";
+ int width = PackedArrayElementWidth(codec.TypeId);
+ string lengthExpr = width == 1 ? countExpr : $"checked({countExpr} *
{width})";
+
sb.AppendLine($"{indent}context.Writer.WriteVarUInt32((uint){lengthExpr});");
+ string packedIndexVar = $"__foryIndex{id++}";
+ sb.AppendLine($"{indent}for (int {packedIndexVar} = 0;
{packedIndexVar} < {countExpr}; {packedIndexVar}++)");
+ sb.AppendLine($"{indent}{{");
+ string itemExpr = $"{valuesVar}[{packedIndexVar}]";
+ uint elementTypeId = PackedArrayElementTypeId(codec.TypeId);
+ if (!TryBuildDirectPayloadWrite(elementTypeId, itemExpr, out string?
writeCode))
+ {
+ throw new InvalidOperationException($"unsupported packed array
type id {codec.TypeId}");
+ }
+
+ sb.AppendLine($"{indent} {writeCode}");
+ sb.AppendLine($"{indent}}}");
+ }
+
+ private static void EmitWriteCollectionPayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string valueExpr,
+ int indentLevel,
+ ref int id,
+ bool isSet)
+ {
+ string indent = new(' ', indentLevel * 4);
+ FieldCodecModel element = codec.Generics[0];
+ string valuesVar = $"__foryCollection{id++}";
+ sb.AppendLine($"{indent}{codec.TypeName} {valuesVar} = {valueExpr} ??
[];");
+ string countExpr = codec.CarrierKind == CarrierKind.Array ?
$"{valuesVar}.Length" : $"{valuesVar}.Count";
+ sb.AppendLine($"{indent}int __foryCount{id} = {countExpr};");
+ string countVar = $"__foryCount{id++}";
+
sb.AppendLine($"{indent}context.Writer.WriteVarUInt32((uint){countVar});");
+ sb.AppendLine($"{indent}if ({countVar} != 0)");
+ sb.AppendLine($"{indent}{{");
+ string innerIndent = indent + " ";
+ string hasNullVar = $"__foryHasNull{id++}";
+ if (element.Nullable)
+ {
+ sb.AppendLine($"{innerIndent}bool {hasNullVar} = false;");
+ if (isSet)
+ {
+ sb.AppendLine($"{innerIndent}foreach ({element.TypeName}
__foryItem in {valuesVar})");
+ sb.AppendLine($"{innerIndent}{{");
+ sb.AppendLine($"{innerIndent} if (__foryItem is null)");
+ sb.AppendLine($"{innerIndent} {{");
+ sb.AppendLine($"{innerIndent} {hasNullVar} = true;");
+ sb.AppendLine($"{innerIndent} break;");
+ sb.AppendLine($"{innerIndent} }}");
+ sb.AppendLine($"{innerIndent}}}");
+ }
+ else
+ {
+ string scanIndexVar = $"__foryIndex{id++}";
+ sb.AppendLine($"{innerIndent}for (int {scanIndexVar} = 0;
{scanIndexVar} < {countVar}; {scanIndexVar}++)");
+ sb.AppendLine($"{innerIndent}{{");
+ string itemExpr = $"{valuesVar}[{scanIndexVar}]";
+ sb.AppendLine($"{innerIndent} if ({itemExpr} is null)");
+ sb.AppendLine($"{innerIndent} {{");
+ sb.AppendLine($"{innerIndent} {hasNullVar} = true;");
+ sb.AppendLine($"{innerIndent} break;");
+ sb.AppendLine($"{innerIndent} }}");
+ sb.AppendLine($"{innerIndent}}}");
+ }
+ }
+ else
+ {
+ sb.AppendLine($"{innerIndent}bool {hasNullVar} = false;");
+ }
+
+ string collectionHeaderVar = $"__foryHeader{id++}";
+ sb.AppendLine($"{innerIndent}byte {collectionHeaderVar} = 0b0000_1000
| 0b0000_0100;");
+ sb.AppendLine($"{innerIndent}if ({hasNullVar})");
+ sb.AppendLine($"{innerIndent}{{");
+ sb.AppendLine($"{innerIndent} {collectionHeaderVar} |=
0b0000_0010;");
+ sb.AppendLine($"{innerIndent}}}");
+
sb.AppendLine($"{innerIndent}context.Writer.WriteUInt8({collectionHeaderVar});");
+ if (isSet)
+ {
+ sb.AppendLine($"{innerIndent}foreach ({element.TypeName}
__foryItem in {valuesVar})");
+ sb.AppendLine($"{innerIndent}{{");
+ EmitWriteNullableElementPayload(sb, element, "__foryItem",
indentLevel + 2, ref id, hasNullVar);
+ sb.AppendLine($"{innerIndent}}}");
+ }
+ else
+ {
+ string writeIndexVar = $"__foryIndex{id++}";
+ sb.AppendLine($"{innerIndent}for (int {writeIndexVar} = 0;
{writeIndexVar} < {countVar}; {writeIndexVar}++)");
+ sb.AppendLine($"{innerIndent}{{");
+ sb.AppendLine($"{innerIndent} {element.TypeName} __foryItem =
{valuesVar}[{writeIndexVar}];");
+ EmitWriteNullableElementPayload(sb, element, "__foryItem",
indentLevel + 2, ref id, hasNullVar);
+ sb.AppendLine($"{innerIndent}}}");
+ }
+
+ sb.AppendLine($"{indent}}}");
+ }
+
+ private static void EmitWriteNullableElementPayload(
+ StringBuilder sb,
+ FieldCodecModel element,
+ string itemExpr,
+ int indentLevel,
+ ref int id,
+ string hasNullVar)
+ {
+ string indent = new(' ', indentLevel * 4);
+ if (!element.Nullable)
+ {
+ EmitWritePayload(sb, element, itemExpr, indentLevel, ref id);
+ return;
+ }
+
+ sb.AppendLine($"{indent}if ({hasNullVar})");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{indent} if ({itemExpr} is null)");
+ sb.AppendLine($"{indent} {{");
+ sb.AppendLine($"{indent}
context.Writer.WriteInt8((sbyte)global::Apache.Fory.RefFlag.Null);");
+ sb.AppendLine($"{indent} continue;");
+ sb.AppendLine($"{indent} }}");
+ sb.AppendLine();
+ sb.AppendLine($"{indent}
context.Writer.WriteInt8((sbyte)global::Apache.Fory.RefFlag.NotNullValue);");
+ string nonNullExpr = element.NullableValueType ?
$"{itemExpr}.GetValueOrDefault()" : $"{itemExpr}!";
+ EmitWritePayload(sb, element, nonNullExpr, indentLevel + 1, ref id);
+ sb.AppendLine($"{indent}}}");
+ sb.AppendLine($"{indent}else");
+ sb.AppendLine($"{indent}{{");
+ EmitWritePayload(sb, element, element.NullableValueType ?
$"{itemExpr}.GetValueOrDefault()" : $"{itemExpr}!", indentLevel + 1, ref id);
+ sb.AppendLine($"{indent}}}");
+ }
+
+ private static void EmitWriteMapPayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string valueExpr,
+ int indentLevel,
+ ref int id)
+ {
+ string indent = new(' ', indentLevel * 4);
+ FieldCodecModel key = codec.Generics[0];
+ FieldCodecModel value = codec.Generics[1];
+ string mapVar = $"__foryMap{id++}";
+ sb.AppendLine($"{indent}{codec.TypeName} {mapVar} = {valueExpr} ??
[];");
+
sb.AppendLine($"{indent}context.Writer.WriteVarUInt32((uint){mapVar}.Count);");
+ sb.AppendLine($"{indent}foreach
(global::System.Collections.Generic.KeyValuePair<{key.TypeName},
{value.TypeName}> __foryEntry in {mapVar})");
+ sb.AppendLine($"{indent}{{");
+ string innerIndent = indent + " ";
+ string keyNullVar = $"__foryKeyNull{id++}";
+ string valueNullVar = $"__foryValueNull{id++}";
+ if (key.Nullable)
+ {
+ sb.AppendLine($"{innerIndent}bool {keyNullVar} = __foryEntry.Key
is null;");
+ }
+ else
+ {
+ sb.AppendLine($"{innerIndent}bool {keyNullVar} = false;");
+ }
+
+ if (value.Nullable)
+ {
+ sb.AppendLine($"{innerIndent}bool {valueNullVar} =
__foryEntry.Value is null;");
+ }
+ else
+ {
+ sb.AppendLine($"{innerIndent}bool {valueNullVar} = false;");
+ }
+
+ string mapHeaderVar = $"__foryHeader{id++}";
+ sb.AppendLine($"{innerIndent}byte {mapHeaderVar} = 0;");
+ sb.AppendLine($"{innerIndent}if ({keyNullVar}) {mapHeaderVar} |=
0b0000_0010; else {mapHeaderVar} |= 0b0000_0100;");
+ sb.AppendLine($"{innerIndent}if ({valueNullVar}) {mapHeaderVar} |=
0b0001_0000; else {mapHeaderVar} |= 0b0010_0000;");
+
sb.AppendLine($"{innerIndent}context.Writer.WriteUInt8({mapHeaderVar});");
+ sb.AppendLine($"{innerIndent}if (!{keyNullVar} && !{valueNullVar})");
+ sb.AppendLine($"{innerIndent}{{");
+ sb.AppendLine($"{innerIndent} context.Writer.WriteUInt8(1);");
+ EmitWritePayload(sb, key, key.NullableValueType ?
"__foryEntry.Key.GetValueOrDefault()" : "__foryEntry.Key!", indentLevel + 2,
ref id);
+ EmitWritePayload(sb, value, value.NullableValueType ?
"__foryEntry.Value.GetValueOrDefault()" : "__foryEntry.Value!", indentLevel +
2, ref id);
+ sb.AppendLine($"{innerIndent} continue;");
+ sb.AppendLine($"{innerIndent}}}");
+ sb.AppendLine($"{innerIndent}if (!{keyNullVar})");
+ sb.AppendLine($"{innerIndent}{{");
+ EmitWritePayload(sb, key, key.NullableValueType ?
"__foryEntry.Key.GetValueOrDefault()" : "__foryEntry.Key!", indentLevel + 2,
ref id);
+ sb.AppendLine($"{innerIndent}}}");
+ sb.AppendLine($"{innerIndent}if (!{valueNullVar})");
+ sb.AppendLine($"{innerIndent}{{");
+ EmitWritePayload(sb, value, value.NullableValueType ?
"__foryEntry.Value.GetValueOrDefault()" : "__foryEntry.Value!", indentLevel +
2, ref id);
+ sb.AppendLine($"{innerIndent}}}");
+ sb.AppendLine($"{indent}}}");
+ }
+
+ private static void EmitReadPayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string targetVar,
+ int indentLevel,
+ ref int id)
+ {
+ string indent = new(' ', indentLevel * 4);
+ switch (codec.Kind)
+ {
+ case FieldCodecKind.Scalar:
+ if (TryBuildDirectPayloadRead(codec.TypeId, out string?
readExpr))
+ {
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} =
{readExpr};");
+ }
+ else
+ {
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} =
context.TypeResolver.GetSerializer<{codec.TypeName}>().ReadData(context);");
+ }
+
+ return;
+ case FieldCodecKind.PackedArray:
+ EmitReadPackedArrayPayload(sb, codec, targetVar, indentLevel,
ref id);
+ return;
+ case FieldCodecKind.List:
+ EmitReadCollectionPayload(sb, codec, targetVar, indentLevel,
ref id, isSet: false);
+ return;
+ case FieldCodecKind.Set:
+ EmitReadCollectionPayload(sb, codec, targetVar, indentLevel,
ref id, isSet: true);
+ return;
+ case FieldCodecKind.Map:
+ EmitReadMapPayload(sb, codec, targetVar, indentLevel, ref id);
+ return;
+ }
+ }
+
+ private static void EmitReadPackedArrayPayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string targetVar,
+ int indentLevel,
+ ref int id)
+ {
+ string indent = new(' ', indentLevel * 4);
+ int width = PackedArrayElementWidth(codec.TypeId);
+ uint elementTypeId = PackedArrayElementTypeId(codec.TypeId);
+ string payloadSizeVar = $"__foryPayloadSize{id++}";
+ string countVar = $"__foryPackedCount{id++}";
+ sb.AppendLine($"{indent}int {payloadSizeVar} =
checked((int)context.Reader.ReadVarUInt32());");
+ if (width > 1)
+ {
+ int mask = width - 1;
+ sb.AppendLine($"{indent}if (({payloadSizeVar} & {mask}) != 0)");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{indent} throw new
global::Apache.Fory.InvalidDataException(\"packed array payload size
mismatch\");");
+ sb.AppendLine($"{indent}}}");
+ }
+
+ sb.AppendLine($"{indent}int {countVar} = {payloadSizeVar}{(width == 1
? string.Empty : $" / {width}")};");
+ if (codec.CarrierKind == CarrierKind.Array)
+ {
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} = new
{ElementTypeName(codec.TypeName)}[{countVar}];");
+ }
+ else
+ {
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} =
new({countVar});");
+ }
+
+ string packedIndexVar = $"__foryIndex{id++}";
+ sb.AppendLine($"{indent}for (int {packedIndexVar} = 0;
{packedIndexVar} < {countVar}; {packedIndexVar}++)");
+ sb.AppendLine($"{indent}{{");
+ if (!TryBuildDirectPayloadRead(elementTypeId, out string? readExpr))
+ {
+ throw new InvalidOperationException($"unsupported packed array
type id {codec.TypeId}");
+ }
+
+ if (codec.CarrierKind == CarrierKind.Array)
+ {
+ sb.AppendLine($"{indent} {targetVar}[{packedIndexVar}] =
{readExpr};");
+ }
+ else
+ {
+ sb.AppendLine($"{indent} {targetVar}.Add({readExpr});");
+ }
+
+ sb.AppendLine($"{indent}}}");
+ }
+
+ private static void EmitReadCollectionPayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string targetVar,
+ int indentLevel,
+ ref int id,
+ bool isSet)
+ {
+ string indent = new(' ', indentLevel * 4);
+ FieldCodecModel element = codec.Generics[0];
+ string lengthVar = $"__foryLength{id++}";
+ string headerVar = $"__foryHeader{id++}";
+ string hasNullVar = $"__foryHasNull{id++}";
+ sb.AppendLine($"{indent}int {lengthVar} =
checked((int)context.Reader.ReadVarUInt32());");
+ if (isSet)
+ {
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} = new();");
+ }
+ else if (codec.CarrierKind == CarrierKind.Array)
+ {
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} = new
{ElementTypeName(codec.TypeName)}[{lengthVar}];");
+ }
+ else
+ {
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} =
new({lengthVar});");
+ }
+
+ sb.AppendLine($"{indent}if ({lengthVar} != 0)");
+ sb.AppendLine($"{indent}{{");
+ string innerIndent = indent + " ";
+ sb.AppendLine($"{innerIndent}byte {headerVar} =
context.Reader.ReadUInt8();");
+ sb.AppendLine($"{innerIndent}bool {hasNullVar} = ({headerVar} &
0b0000_0010) != 0;");
+ string collectionIndexVar = $"__foryIndex{id++}";
+ sb.AppendLine($"{innerIndent}for (int {collectionIndexVar} = 0;
{collectionIndexVar} < {lengthVar}; {collectionIndexVar}++)");
+ sb.AppendLine($"{innerIndent}{{");
+ EmitReadNullableElementPayload(sb, element, "__foryItem", indentLevel
+ 2, ref id, hasNullVar);
+ if (codec.CarrierKind == CarrierKind.Array)
+ {
+ sb.AppendLine($"{innerIndent} {targetVar}[{collectionIndexVar}]
= __foryItem;");
+ }
+ else
+ {
+ sb.AppendLine($"{innerIndent} {targetVar}.Add(__foryItem);");
+ }
+
+ sb.AppendLine($"{innerIndent}}}");
+ sb.AppendLine($"{indent}}}");
+ }
+
+ private static void EmitReadNullableElementPayload(
+ StringBuilder sb,
+ FieldCodecModel element,
+ string targetVar,
+ int indentLevel,
+ ref int id,
+ string hasNullVar)
+ {
+ string indent = new(' ', indentLevel * 4);
+ sb.AppendLine($"{indent}{element.TypeName} {targetVar};");
+ if (element.Nullable)
+ {
+ sb.AppendLine($"{indent}if ({hasNullVar})");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{indent} sbyte __foryRefFlag =
context.Reader.ReadInt8();");
+ sb.AppendLine($"{indent} if (__foryRefFlag ==
(sbyte)global::Apache.Fory.RefFlag.Null)");
+ sb.AppendLine($"{indent} {{");
+ sb.AppendLine($"{indent} {targetVar} =
({element.TypeName})default!;");
+ sb.AppendLine($"{indent} }}");
+ sb.AppendLine($"{indent} else if (__foryRefFlag ==
(sbyte)global::Apache.Fory.RefFlag.NotNullValue)");
+ sb.AppendLine($"{indent} {{");
+ string nullableNonNullVar = $"__foryNonNull{id++}";
+ EmitReadPayload(sb, NonNullableCodec(element), nullableNonNullVar,
indentLevel + 2, ref id);
+ sb.AppendLine($"{indent} {targetVar} =
{nullableNonNullVar};");
+ sb.AppendLine($"{indent} }}");
+ sb.AppendLine($"{indent} else");
+ sb.AppendLine($"{indent} {{");
+ sb.AppendLine($"{indent} throw new
global::Apache.Fory.InvalidDataException($\"invalid collection null flag
{{__foryRefFlag}}\");");
+ sb.AppendLine($"{indent} }}");
+ sb.AppendLine($"{indent}}}");
+ sb.AppendLine($"{indent}else");
+ sb.AppendLine($"{indent}{{");
+ string nonNullVar = $"__foryNonNull{id++}";
+ EmitReadPayload(sb, NonNullableCodec(element), nonNullVar,
indentLevel + 1, ref id);
+ sb.AppendLine($"{indent} {targetVar} = {nonNullVar};");
+ sb.AppendLine($"{indent}}}");
+ return;
+ }
+
+ string directNonNullVar = $"__foryNonNull{id++}";
+ EmitReadPayload(sb, element, directNonNullVar, indentLevel, ref id);
+ sb.AppendLine($"{indent}{targetVar} = {directNonNullVar};");
+ }
+
+ private static void EmitReadMapPayload(
+ StringBuilder sb,
+ FieldCodecModel codec,
+ string targetVar,
+ int indentLevel,
+ ref int id)
+ {
+ string indent = new(' ', indentLevel * 4);
+ FieldCodecModel key = codec.Generics[0];
+ FieldCodecModel value = codec.Generics[1];
+ string totalVar = $"__foryTotal{id++}";
+ sb.AppendLine($"{indent}int {totalVar} =
checked((int)context.Reader.ReadVarUInt32());");
+ sb.AppendLine($"{indent}{codec.TypeName} {targetVar} =
new({totalVar});");
+ sb.AppendLine($"{indent}int __foryRead = 0;");
+ sb.AppendLine($"{indent}while (__foryRead < {totalVar})");
+ sb.AppendLine($"{indent}{{");
+ string innerIndent = indent + " ";
+ sb.AppendLine($"{innerIndent}byte __foryHeader =
context.Reader.ReadUInt8();");
+ sb.AppendLine($"{innerIndent}bool __foryKeyNull = (__foryHeader &
0b0000_0010) != 0;");
+ sb.AppendLine($"{innerIndent}bool __foryValueNull = (__foryHeader &
0b0001_0000) != 0;");
+ sb.AppendLine($"{innerIndent}if (__foryKeyNull || __foryValueNull)");
+ sb.AppendLine($"{innerIndent}{{");
+ sb.AppendLine($"{innerIndent} {key.TypeName} __foryKey =
({key.TypeName})default!;");
+ sb.AppendLine($"{innerIndent} {value.TypeName} __foryValue =
({value.TypeName})default!;");
+ sb.AppendLine($"{innerIndent} if (!__foryKeyNull)");
+ sb.AppendLine($"{innerIndent} {{");
+ EmitReadPayload(sb, NonNullableCodec(key), "__foryReadKey",
indentLevel + 2, ref id);
+ sb.AppendLine($"{innerIndent} __foryKey = __foryReadKey;");
+ sb.AppendLine($"{innerIndent} }}");
+ sb.AppendLine($"{innerIndent} if (!__foryValueNull)");
+ sb.AppendLine($"{innerIndent} {{");
+ EmitReadPayload(sb, NonNullableCodec(value), "__foryReadValue",
indentLevel + 2, ref id);
+ sb.AppendLine($"{innerIndent} __foryValue = __foryReadValue;");
+ sb.AppendLine($"{innerIndent} }}");
+ if (codec.CarrierKind == CarrierKind.NullableKeyDictionary)
+ {
+ sb.AppendLine($"{innerIndent} {targetVar}[__foryKey] =
__foryValue;");
+ }
+ else
+ {
+ sb.AppendLine($"{innerIndent} if (!__foryKeyNull)");
+ sb.AppendLine($"{innerIndent} {{");
+ sb.AppendLine($"{innerIndent} {targetVar}[__foryKey] =
__foryValue;");
+ sb.AppendLine($"{innerIndent} }}");
+ }
+
+ sb.AppendLine($"{innerIndent} __foryRead++;");
+ sb.AppendLine($"{innerIndent} continue;");
+ sb.AppendLine($"{innerIndent}}}");
+ sb.AppendLine($"{innerIndent}int __foryChunkSize =
context.Reader.ReadUInt8();");
+ string mapIndexVar = $"__foryIndex{id++}";
+ sb.AppendLine($"{innerIndent}for (int {mapIndexVar} = 0; {mapIndexVar}
< __foryChunkSize; {mapIndexVar}++)");
+ sb.AppendLine($"{innerIndent}{{");
+ EmitReadPayload(sb, NonNullableCodec(key), "__foryKey", indentLevel +
2, ref id);
+ EmitReadPayload(sb, NonNullableCodec(value), "__foryValue",
indentLevel + 2, ref id);
+ sb.AppendLine($"{innerIndent} {targetVar}[__foryKey] =
__foryValue;");
+ sb.AppendLine($"{innerIndent}}}");
+ sb.AppendLine($"{innerIndent}__foryRead += __foryChunkSize;");
+ sb.AppendLine($"{indent}}}");
+ }
+
+ private static FieldCodecModel NonNullableCodec(FieldCodecModel codec)
+ {
+ if (!codec.Nullable)
+ {
+ return codec;
+ }
+
+ return new FieldCodecModel(
+ codec.Kind,
+ codec.TypeId,
+ codec.NullableValueType && codec.TypeName.EndsWith("?",
StringComparison.Ordinal)
+ ? codec.TypeName.Substring(0, codec.TypeName.Length - 1)
+ : codec.TypeName,
+ false,
+ false,
+ codec.CarrierKind,
+ codec.Generics);
+ }
+
+ private static string ElementTypeName(string arrayTypeName)
+ {
+ return arrayTypeName.EndsWith("[]", StringComparison.Ordinal)
+ ? arrayTypeName.Substring(0, arrayTypeName.Length - 2)
+ : "object";
+ }
+
+ private static int PackedArrayElementWidth(uint typeId)
+ {
+ return typeId switch
+ {
+ 41 or 43 or 44 => 1,
+ 45 or 49 or 53 or 54 => 2,
+ 46 or 50 or 55 => 4,
+ 47 or 51 or 56 => 8,
+ _ => throw new InvalidOperationException($"unsupported packed
array type id {typeId}"),
+ };
+ }
+
+ private static uint PackedArrayElementTypeId(uint typeId)
+ {
+ return typeId switch
+ {
+ 41 => 9,
+ 43 => 1,
+ 44 => 2,
+ 45 => 3,
+ 46 => 4,
+ 47 => 6,
+ 49 => 10,
+ 50 => 11,
+ 51 => 13,
+ 53 => 17,
+ 54 => 18,
+ 55 => 19,
+ 56 => 20,
+ _ => throw new InvalidOperationException($"unsupported packed
array type id {typeId}"),
+ };
+ }
+
private static void EmitWriteMember(StringBuilder sb, MemberModel member,
bool compatibleMode)
{
string refModeExpr = BuildWriteRefModeExpression(member);
@@ -646,6 +1244,13 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
throw new InvalidOperationException($"unsupported dynamic any
kind {member.DynamicAnyKind}");
}
+ if (member.FieldCodec is not null)
+ {
+ sb.AppendLine(
+ $" __ForyWrite{Sanitize(member.Name)}Field(context,
{memberAccess}, {refModeExpr});");
+ return;
+ }
+
if (member.UseDictionaryTypeInfoCache)
{
EmitWriteDictionaryWithTypeInfoCache(
@@ -754,6 +1359,13 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
throw new InvalidOperationException($"unsupported dynamic any
kind {member.DynamicAnyKind}");
}
+ if (member.FieldCodec is not null)
+ {
+ sb.AppendLine(
+ $"{indent}{assignmentTarget} =
__ForyRead{Sanitize(member.Name)}Field(context, {refModeExpr});");
+ return;
+ }
+
if (allowDirectRead && !member.IsNullable &&
TryBuildDirectFieldRead(member, out string? directReadExpr))
{
sb.AppendLine($"{indent}{assignmentTarget} = {directReadExpr};");
@@ -1219,16 +1831,22 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
MemberDeclKind memberDeclKind)
{
(bool isOptional, ITypeSymbol unwrappedType) =
UnwrapNullable(memberType);
- FieldEncoding fieldEncoding = FieldEncoding.None;
short? fieldId = null;
+ SchemaTypeModel? schemaType = null;
foreach (AttributeData attribute in memberSymbol.GetAttributes())
{
string? attrName = attribute.AttributeClass?.ToDisplayString();
- if (!string.Equals(attrName, "Apache.Fory.FieldAttribute",
StringComparison.Ordinal))
+ if (!string.Equals(attrName, "Apache.Fory.ForyFieldAttribute",
StringComparison.Ordinal))
{
continue;
}
+ if (attribute.ConstructorArguments.Length == 1 &&
+ TryGetNonNegativeShort(attribute.ConstructorArguments[0], out
short ctorFieldId))
+ {
+ fieldId = ctorFieldId;
+ }
+
foreach (KeyValuePair<string, TypedConstant> namedArg in
attribute.NamedArguments)
{
if (string.Equals(namedArg.Key, "Id",
StringComparison.Ordinal))
@@ -1241,20 +1859,20 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
continue;
}
- if (!string.Equals(namedArg.Key, "Encoding",
StringComparison.Ordinal))
+ if (!string.Equals(namedArg.Key, "Type",
StringComparison.Ordinal))
{
continue;
}
- if (namedArg.Value.Value is int encoding)
+ if (namedArg.Value.Value is ITypeSymbol schemaSymbol)
{
- fieldEncoding = (FieldEncoding)encoding;
+ schemaType = TryParseSchemaType(schemaSymbol);
}
}
}
DynamicAnyKind dynamicAnyKind = ResolveDynamicAnyKind(unwrappedType);
- TypeResolution resolution = ResolveTypeResolution(unwrappedType,
fieldEncoding);
+ TypeResolution resolution = ResolveTypeResolution(unwrappedType,
schemaType);
if (!resolution.Supported)
{
return null;
@@ -1288,7 +1906,9 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
memberType,
isOptional,
dynamicAnyKind,
- resolution.Classification.TypeId);
+ resolution.Classification.TypeId,
+ schemaType);
+ FieldCodecModel? fieldCodec = BuildFieldCodecModel(memberType,
typeMeta, schemaType, classification);
return new MemberModel(
name,
@@ -1307,28 +1927,31 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
!unwrappedType.IsValueType && classification.TypeId != 21,
FieldNeedsTypeInfo(classification, dynamicAnyKind, unwrappedType),
dynamicAnyKind == DynamicAnyKind.None ? DynamicAnyKind.None :
dynamicAnyKind,
- typeMeta);
+ typeMeta,
+ fieldCodec);
}
private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel(
ITypeSymbol memberType,
bool nullable,
DynamicAnyKind dynamicAnyKind,
- uint explicitTypeId)
+ uint explicitTypeId,
+ SchemaTypeModel? schemaType = null)
{
(bool _, ITypeSymbol unwrapped) = UnwrapNullable(memberType);
+ if (schemaType is not null)
+ {
+ return BuildSchemaTypeMetaFieldTypeModel(memberType, nullable,
schemaType);
+ }
+
if (TryGetListElementType(unwrapped, out ITypeSymbol? listElementType))
{
bool elementNullable = GenericNullable(listElementType!);
if (!elementNullable &&
TryResolvePackedArrayTypeIdForElement(listElementType!) is
uint packedArrayTypeId &&
- explicitTypeId == packedArrayTypeId)
+ (explicitTypeId == packedArrayTypeId || explicitTypeId == 22))
{
- // Align compatible TypeMeta with C++ vector arithmetic
handling:
- // when the wire type is already classified as a packed array
(e.g. int[]),
- // use the specialized array TypeId directly instead of
LIST<elem>.
- // This keeps schema/type-meta bytes consistent across
languages.
return new TypeMetaFieldTypeModel(
packedArrayTypeId.ToString(),
nullable,
@@ -1428,6 +2051,227 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
ImmutableArray<TypeMetaFieldTypeModel>.Empty);
}
+ private static TypeMetaFieldTypeModel BuildSchemaTypeMetaFieldTypeModel(
+ ITypeSymbol carrierType,
+ bool nullable,
+ SchemaTypeModel schemaType)
+ {
+ (bool _, ITypeSymbol unwrapped) = UnwrapNullable(carrierType);
+ switch (schemaType.Kind)
+ {
+ case SchemaTypeKind.List:
+ if (!TryGetListElementType(unwrapped, out ITypeSymbol?
listElementType))
+ {
+ return new TypeMetaFieldTypeModel(
+ schemaType.TypeId.ToString(),
+ nullable,
+ false,
+ ImmutableArray<TypeMetaFieldTypeModel>.Empty);
+ }
+
+ bool elementNullable = GenericNullable(listElementType!);
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.List",
+ nullable,
+ false,
+ ImmutableArray.Create(
+ BuildSchemaTypeMetaFieldTypeModel(
+ listElementType!,
+ elementNullable,
+ schemaType.Generics[0])));
+ case SchemaTypeKind.Set:
+ if (!TryGetSetElementType(unwrapped, out ITypeSymbol?
setElementType))
+ {
+ return new TypeMetaFieldTypeModel(
+ schemaType.TypeId.ToString(),
+ nullable,
+ false,
+ ImmutableArray<TypeMetaFieldTypeModel>.Empty);
+ }
+
+ bool setElementNullable = GenericNullable(setElementType!);
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.Set",
+ nullable,
+ false,
+ ImmutableArray.Create(
+ BuildSchemaTypeMetaFieldTypeModel(
+ setElementType!,
+ setElementNullable,
+ schemaType.Generics[0])));
+ case SchemaTypeKind.Map:
+ if (!TryGetMapTypeArguments(unwrapped, out ITypeSymbol?
keyType, out ITypeSymbol? valueType))
+ {
+ return new TypeMetaFieldTypeModel(
+ schemaType.TypeId.ToString(),
+ nullable,
+ false,
+ ImmutableArray<TypeMetaFieldTypeModel>.Empty);
+ }
+
+ bool keyNullable = GenericNullable(keyType!);
+ bool valueNullable = GenericNullable(valueType!);
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.Map",
+ nullable,
+ false,
+ ImmutableArray.Create(
+ BuildSchemaTypeMetaFieldTypeModel(keyType!,
keyNullable, schemaType.Generics[0]),
+ BuildSchemaTypeMetaFieldTypeModel(valueType!,
valueNullable, schemaType.Generics[1])));
+ default:
+ return new TypeMetaFieldTypeModel(
+ schemaType.TypeId.ToString(),
+ nullable,
+ false,
+ ImmutableArray<TypeMetaFieldTypeModel>.Empty);
+ }
+ }
+
+ private static FieldCodecModel? BuildFieldCodecModel(
+ ITypeSymbol carrierType,
+ TypeMetaFieldTypeModel typeMeta,
+ SchemaTypeModel? schemaType,
+ TypeClassification classification)
+ {
+ (bool nullable, ITypeSymbol unwrapped) = UnwrapNullable(carrierType);
+ bool nullableValueType = carrierType is INamedTypeSymbol nts &&
+ nts.OriginalDefinition.SpecialType ==
SpecialType.System_Nullable_T;
+
+ if (schemaType is not null)
+ {
+ FieldCodecModel codec = BuildFieldCodecFromSchema(carrierType,
nullable, nullableValueType, schemaType);
+ return codec.Kind == FieldCodecKind.Scalar ? null : codec;
+ }
+
+ _ = typeMeta;
+ _ = classification;
+ return null;
+ }
+
+ private static FieldCodecModel BuildFieldCodecFromSchema(
+ ITypeSymbol carrierType,
+ bool nullable,
+ bool nullableValueType,
+ SchemaTypeModel schemaType)
+ {
+ (bool _, ITypeSymbol unwrapped) = UnwrapNullable(carrierType);
+ switch (schemaType.Kind)
+ {
+ case SchemaTypeKind.List:
+ {
+ ITypeSymbol elementType = TryGetListElementType(unwrapped,
out ITypeSymbol? listElementType)
+ ? listElementType!
+ : carrierType;
+ FieldCodecModel element = BuildFieldCodecFromSchema(
+ elementType,
+ GenericNullable(elementType),
+ elementType is INamedTypeSymbol elementNamed &&
+ elementNamed.OriginalDefinition.SpecialType ==
SpecialType.System_Nullable_T,
+ schemaType.Generics[0]);
+ return new FieldCodecModel(
+ FieldCodecKind.List,
+ schemaType.TypeId,
+ carrierType.ToDisplayString(FullNameFormat),
+ nullable,
+ nullableValueType,
+ GetCarrierKind(unwrapped),
+ ImmutableArray.Create(element));
+ }
+ case SchemaTypeKind.Set:
+ {
+ ITypeSymbol elementType = TryGetSetElementType(unwrapped,
out ITypeSymbol? setElementType)
+ ? setElementType!
+ : carrierType;
+ FieldCodecModel element = BuildFieldCodecFromSchema(
+ elementType,
+ GenericNullable(elementType),
+ elementType is INamedTypeSymbol elementNamed &&
+ elementNamed.OriginalDefinition.SpecialType ==
SpecialType.System_Nullable_T,
+ schemaType.Generics[0]);
+ return new FieldCodecModel(
+ FieldCodecKind.Set,
+ schemaType.TypeId,
+ carrierType.ToDisplayString(FullNameFormat),
+ nullable,
+ nullableValueType,
+ GetCarrierKind(unwrapped),
+ ImmutableArray.Create(element));
+ }
+ case SchemaTypeKind.Map:
+ {
+ ITypeSymbol keyType = carrierType;
+ ITypeSymbol valueType = carrierType;
+ if (TryGetMapTypeArguments(unwrapped, out ITypeSymbol?
parsedKeyType, out ITypeSymbol? parsedValueType))
+ {
+ keyType = parsedKeyType!;
+ valueType = parsedValueType!;
+ }
+
+ FieldCodecModel key = BuildFieldCodecFromSchema(
+ keyType,
+ GenericNullable(keyType),
+ keyType is INamedTypeSymbol keyNamed &&
+ keyNamed.OriginalDefinition.SpecialType ==
SpecialType.System_Nullable_T,
+ schemaType.Generics[0]);
+ FieldCodecModel value = BuildFieldCodecFromSchema(
+ valueType,
+ GenericNullable(valueType),
+ valueType is INamedTypeSymbol valueNamed &&
+ valueNamed.OriginalDefinition.SpecialType ==
SpecialType.System_Nullable_T,
+ schemaType.Generics[1]);
+ return new FieldCodecModel(
+ FieldCodecKind.Map,
+ schemaType.TypeId,
+ carrierType.ToDisplayString(FullNameFormat),
+ nullable,
+ nullableValueType,
+ GetCarrierKind(unwrapped),
+ ImmutableArray.Create(key, value));
+ }
+ case SchemaTypeKind.PackedArray:
+ return new FieldCodecModel(
+ FieldCodecKind.PackedArray,
+ schemaType.TypeId,
+ carrierType.ToDisplayString(FullNameFormat),
+ nullable,
+ nullableValueType,
+ GetCarrierKind(unwrapped),
+ ImmutableArray<FieldCodecModel>.Empty);
+ default:
+ return new FieldCodecModel(
+ FieldCodecKind.Scalar,
+ schemaType.TypeId,
+ carrierType.ToDisplayString(FullNameFormat),
+ nullable,
+ nullableValueType,
+ GetCarrierKind(unwrapped),
+ ImmutableArray<FieldCodecModel>.Empty);
+ }
+ }
+
+ private static CarrierKind GetCarrierKind(ITypeSymbol unwrappedType)
+ {
+ if (unwrappedType is IArrayTypeSymbol)
+ {
+ return CarrierKind.Array;
+ }
+
+ if (unwrappedType is not INamedTypeSymbol named)
+ {
+ return CarrierKind.Value;
+ }
+
+ string genericName = named.ConstructedFrom.ToDisplayString();
+ return genericName switch
+ {
+ "System.Collections.Generic.List<T>" => CarrierKind.List,
+ "System.Collections.Generic.HashSet<T>" => CarrierKind.HashSet,
+ "System.Collections.Generic.Dictionary<TKey, TValue>" =>
CarrierKind.Dictionary,
+ "Apache.Fory.NullableKeyDictionary<TKey, TValue>" =>
CarrierKind.NullableKeyDictionary,
+ _ => CarrierKind.Value,
+ };
+ }
+
private static bool TryGetNonNegativeShort(TypedConstant value, out short
fieldId)
{
fieldId = default;
@@ -1569,74 +2413,227 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
return false;
}
- private static TypeResolution ResolveTypeResolution(ITypeSymbol type,
FieldEncoding encoding)
+ private static SchemaTypeModel? TryParseSchemaType(ITypeSymbol symbol)
{
- TypeClassification baseType = ClassifyType(type);
- if (encoding == FieldEncoding.None)
+ if (symbol is not INamedTypeSymbol named)
{
- return new TypeResolution(true, baseType);
+ return 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;
+ string fullName =
named.ConstructedFrom.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ fullName = fullName.StartsWith("global::", StringComparison.Ordinal)
+ ? fullName.Substring("global::".Length)
+ : fullName;
- if (isInt32)
+ if (fullName == "Apache.Fory.Schema.Types.List<TElement>")
{
- return encoding switch
+ if (named.TypeArguments.Length != 1 ||
+ TryParseSchemaType(named.TypeArguments[0]) is not
SchemaTypeModel element)
{
- FieldEncoding.Varint => new TypeResolution(true, baseType),
- FieldEncoding.Fixed => new TypeResolution(
- true,
- new TypeClassification(4, true, true, false, false, false,
4)),
- _ => new TypeResolution(false, baseType),
- };
+ return null;
+ }
+
+ return new SchemaTypeModel(22, SchemaTypeKind.List,
ImmutableArray.Create(element));
}
- if (isUInt32)
+ if (fullName == "Apache.Fory.Schema.Types.Set<TElement>")
{
- return encoding switch
+ if (named.TypeArguments.Length != 1 ||
+ TryParseSchemaType(named.TypeArguments[0]) is not
SchemaTypeModel element)
{
- FieldEncoding.Varint => new TypeResolution(true, baseType),
- FieldEncoding.Fixed => new TypeResolution(
- true,
- new TypeClassification(11, true, true, false, false,
false, 4)),
- _ => new TypeResolution(false, baseType),
- };
+ return null;
+ }
+
+ return new SchemaTypeModel(23, SchemaTypeKind.Set,
ImmutableArray.Create(element));
}
- if (isInt64)
+ if (fullName == "Apache.Fory.Schema.Types.Map<TKey, TValue>")
{
- return encoding switch
+ if (named.TypeArguments.Length != 2 ||
+ TryParseSchemaType(named.TypeArguments[0]) is not
SchemaTypeModel key ||
+ TryParseSchemaType(named.TypeArguments[1]) is not
SchemaTypeModel value)
{
- FieldEncoding.Varint => new TypeResolution(true, baseType),
- FieldEncoding.Fixed => new TypeResolution(
- true,
- new TypeClassification(6, true, true, false, false, false,
8)),
- FieldEncoding.Tagged => new TypeResolution(
- true,
- new TypeClassification(8, true, true, false, false, true,
8)),
- _ => new TypeResolution(false, baseType),
- };
+ return null;
+ }
+
+ return new SchemaTypeModel(24, SchemaTypeKind.Map,
ImmutableArray.Create(key, value));
}
- if (isUInt64)
+ return TryResolveSchemaTypeId(fullName, out uint typeId, out
SchemaTypeKind kind)
+ ? new SchemaTypeModel(typeId, kind,
ImmutableArray<SchemaTypeModel>.Empty)
+ : null;
+ }
+
+ private static bool TryResolveSchemaTypeId(string fullName, out uint
typeId, out SchemaTypeKind kind)
+ {
+ kind = SchemaTypeKind.Scalar;
+ switch (fullName)
{
- return encoding switch
- {
- FieldEncoding.Varint => new TypeResolution(true, baseType),
- FieldEncoding.Fixed => new TypeResolution(
- true,
- new TypeClassification(13, true, true, false, false,
false, 8)),
- FieldEncoding.Tagged => new TypeResolution(
- true,
- new TypeClassification(15, true, true, false, false, true,
8)),
- _ => new TypeResolution(false, baseType),
- };
+ case "Apache.Fory.Schema.Types.Bool":
+ typeId = 1;
+ return true;
+ case "Apache.Fory.Schema.Types.Int8":
+ typeId = 2;
+ return true;
+ case "Apache.Fory.Schema.Types.Int16":
+ typeId = 3;
+ return true;
+ case "Apache.Fory.Schema.Types.Int32":
+ typeId = 4;
+ return true;
+ case "Apache.Fory.Schema.Types.VarInt32":
+ typeId = 5;
+ return true;
+ case "Apache.Fory.Schema.Types.Int64":
+ typeId = 6;
+ return true;
+ case "Apache.Fory.Schema.Types.VarInt64":
+ typeId = 7;
+ return true;
+ case "Apache.Fory.Schema.Types.TaggedInt64":
+ typeId = 8;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt8":
+ typeId = 9;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt16":
+ typeId = 10;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt32":
+ typeId = 11;
+ return true;
+ case "Apache.Fory.Schema.Types.VarUInt32":
+ typeId = 12;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt64":
+ typeId = 13;
+ return true;
+ case "Apache.Fory.Schema.Types.VarUInt64":
+ typeId = 14;
+ return true;
+ case "Apache.Fory.Schema.Types.TaggedUInt64":
+ typeId = 15;
+ return true;
+ case "Apache.Fory.Schema.Types.Float16":
+ typeId = 17;
+ return true;
+ case "Apache.Fory.Schema.Types.BFloat16":
+ typeId = 18;
+ return true;
+ case "Apache.Fory.Schema.Types.Float32":
+ typeId = 19;
+ return true;
+ case "Apache.Fory.Schema.Types.Float64":
+ typeId = 20;
+ return true;
+ case "Apache.Fory.Schema.Types.String":
+ typeId = 21;
+ return true;
+ case "Apache.Fory.Schema.Types.Binary":
+ typeId = 41;
+ return true;
+ case "Apache.Fory.Schema.Types.Duration":
+ typeId = 37;
+ return true;
+ case "Apache.Fory.Schema.Types.Timestamp":
+ typeId = 38;
+ return true;
+ case "Apache.Fory.Schema.Types.Date":
+ typeId = 39;
+ return true;
+ case "Apache.Fory.Schema.Types.Decimal":
+ typeId = 42;
+ return true;
+ case "Apache.Fory.Schema.Types.BoolArray":
+ typeId = 43;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.Int8Array":
+ typeId = 44;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.Int16Array":
+ typeId = 45;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.Int32Array":
+ typeId = 46;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.Int64Array":
+ typeId = 47;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt8Array":
+ typeId = 41;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt16Array":
+ typeId = 49;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt32Array":
+ typeId = 50;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.UInt64Array":
+ typeId = 51;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.Float16Array":
+ typeId = 53;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.BFloat16Array":
+ typeId = 54;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.Float32Array":
+ typeId = 55;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ case "Apache.Fory.Schema.Types.Float64Array":
+ typeId = 56;
+ kind = SchemaTypeKind.PackedArray;
+ return true;
+ default:
+ typeId = 0;
+ return false;
}
+ }
- return new TypeResolution(false, baseType);
+ private static TypeResolution ResolveTypeResolution(ITypeSymbol type,
SchemaTypeModel? schemaType)
+ {
+ TypeClassification baseType = ClassifyType(type);
+ if (schemaType is null)
+ {
+ return new TypeResolution(true, baseType);
+ }
+
+ bool isPrimitive = schemaType.Kind == SchemaTypeKind.Scalar;
+ bool isCollection = schemaType.Kind == SchemaTypeKind.List ||
+ schemaType.Kind == SchemaTypeKind.Set ||
+ schemaType.Kind == SchemaTypeKind.PackedArray;
+ bool isMap = schemaType.Kind == SchemaTypeKind.Map;
+ bool isCompressedNumeric = schemaType.TypeId is 5 or 7 or 8 or 12 or
14 or 15;
+ int primitiveSize = schemaType.TypeId switch
+ {
+ 1 or 2 or 9 => 1,
+ 3 or 10 or 17 or 18 => 2,
+ 4 or 5 or 11 or 12 or 19 => 4,
+ 6 or 7 or 8 or 13 or 14 or 15 or 20 => 8,
+ _ => 0,
+ };
+ return new TypeResolution(
+ true,
+ new TypeClassification(
+ schemaType.TypeId,
+ isPrimitive,
+ true,
+ isCollection,
+ isMap,
+ isCompressedNumeric,
+ primitiveSize));
}
private static TypeClassification ClassifyType(ITypeSymbol type)
@@ -1743,6 +2740,13 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
if (TryGetListElementType(type, out _))
{
+ if (GetCarrierKind(type) == CarrierKind.List &&
+ TryGetListElementType(type, out ITypeSymbol? elementType) &&
+ TryResolvePackedArrayTypeIdForElement(elementType!) is uint
packedArrayTypeId)
+ {
+ return new TypeClassification(packedArrayTypeId, false, true,
true, false, false, 0);
+ }
+
return new TypeClassification(22, false, true, true, false, false,
0);
}
@@ -2053,6 +3057,52 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
public ImmutableArray<TypeMetaFieldTypeModel> Generics { get; }
}
+ private sealed class SchemaTypeModel
+ {
+ public SchemaTypeModel(
+ uint typeId,
+ SchemaTypeKind kind,
+ ImmutableArray<SchemaTypeModel> generics)
+ {
+ TypeId = typeId;
+ Kind = kind;
+ Generics = generics;
+ }
+
+ public uint TypeId { get; }
+ public SchemaTypeKind Kind { get; }
+ public ImmutableArray<SchemaTypeModel> Generics { get; }
+ }
+
+ private sealed class FieldCodecModel
+ {
+ public FieldCodecModel(
+ FieldCodecKind kind,
+ uint typeId,
+ string typeName,
+ bool nullable,
+ bool nullableValueType,
+ CarrierKind carrierKind,
+ ImmutableArray<FieldCodecModel> generics)
+ {
+ Kind = kind;
+ TypeId = typeId;
+ TypeName = typeName;
+ Nullable = nullable;
+ NullableValueType = nullableValueType;
+ CarrierKind = carrierKind;
+ Generics = generics;
+ }
+
+ public FieldCodecKind Kind { get; }
+ public uint TypeId { get; }
+ public string TypeName { get; }
+ public bool Nullable { get; }
+ public bool NullableValueType { get; }
+ public CarrierKind CarrierKind { get; }
+ public ImmutableArray<FieldCodecModel> Generics { get; }
+ }
+
private sealed class TypeModel
{
public TypeModel(
@@ -2094,7 +3144,8 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
bool isRefType,
bool needsFieldTypeInfo,
DynamicAnyKind dynamicAnyKind,
- TypeMetaFieldTypeModel typeMeta)
+ TypeMetaFieldTypeModel typeMeta,
+ FieldCodecModel? fieldCodec)
{
Name = name;
FieldIdentifier = fieldIdentifier;
@@ -2112,6 +3163,7 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
NeedsFieldTypeInfo = needsFieldTypeInfo;
DynamicAnyKind = dynamicAnyKind;
TypeMeta = typeMeta;
+ FieldCodec = fieldCodec;
}
public string Name { get; }
@@ -2130,6 +3182,7 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
public bool NeedsFieldTypeInfo { get; }
public DynamicAnyKind DynamicAnyKind { get; }
public TypeMetaFieldTypeModel TypeMeta { get; }
+ public FieldCodecModel? FieldCodec { get; }
}
private enum MemberDeclKind
@@ -2152,11 +3205,31 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
AnyValue,
}
- private enum FieldEncoding
+ private enum SchemaTypeKind
+ {
+ Scalar,
+ PackedArray,
+ List,
+ Set,
+ Map,
+ }
+
+ private enum FieldCodecKind
+ {
+ Scalar,
+ PackedArray,
+ List,
+ Set,
+ Map,
+ }
+
+ private enum CarrierKind
{
- None = -1,
- Varint = 0,
- Fixed = 1,
- Tagged = 2,
+ Value,
+ Array,
+ List,
+ HashSet,
+ Dictionary,
+ NullableKeyDictionary,
}
}
diff --git a/csharp/src/Fory/Attributes.cs b/csharp/src/Fory/Attributes.cs
index 6c4ce727f..e18be220d 100644
--- a/csharp/src/Fory/Attributes.cs
+++ b/csharp/src/Fory/Attributes.cs
@@ -29,39 +29,58 @@ public sealed class ForyObjectAttribute : Attribute
public bool Evolving { get; set; } = true;
}
-/// <summary>
-/// Specifies field-level integer/number encoding strategy for generated
serializers.
-/// </summary>
-public enum FieldEncoding
-{
- /// <summary>
- /// Variable-length integer encoding.
- /// </summary>
- Varint,
- /// <summary>
- /// Fixed-width integer encoding.
- /// </summary>
- Fixed,
- /// <summary>
- /// Tagged field encoding for schema-evolution scenarios.
- /// </summary>
- Tagged,
-}
-
/// <summary>
/// Overrides generated serializer behavior for a field or property.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
-public sealed class FieldAttribute : Attribute
+public sealed class ForyFieldAttribute : Attribute
{
+ private short id = -1;
+
+ public ForyFieldAttribute()
+ {
+ }
+
+ public ForyFieldAttribute(short id)
+ {
+ ValidateId(id);
+ this.id = id;
+ }
+
+ public ForyFieldAttribute(int id)
+ {
+ if (id is < 0 or > short.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException(nameof(id));
+ }
+
+ this.id = (short)id;
+ }
+
/// <summary>
/// Optional stable field tag id used for compatible metadata dispatch.
/// Use a non-negative value to emit numeric field ids instead of field
names.
/// </summary>
- public short Id { get; set; } = -1;
+ public short Id
+ {
+ get => id;
+ set
+ {
+ ValidateId(value);
+ id = value;
+ }
+ }
/// <summary>
- /// Gets or sets the field encoding strategy used by generated serializers.
+ /// Optional Fory schema descriptor type from
<c>Apache.Fory.Schema.Types</c>.
/// </summary>
- public FieldEncoding Encoding { get; set; } = FieldEncoding.Varint;
+ public Type? Type { get; set; }
+
+ private static void ValidateId(short id)
+ {
+ if (id < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(id));
+ }
+ }
}
diff --git a/csharp/src/Fory/FieldSkipper.cs b/csharp/src/Fory/FieldSkipper.cs
index 652d59cfa..87ba06be5 100644
--- a/csharp/src/Fory/FieldSkipper.cs
+++ b/csharp/src/Fory/FieldSkipper.cs
@@ -21,104 +21,251 @@ public static class FieldSkipper
{
public static void SkipFieldValue(ReadContext context, TypeMetaFieldType
fieldType)
{
- _ = ReadFieldValue(context, fieldType);
+ SkipValue(context, fieldType,
RefModeExtensions.From(fieldType.Nullable, fieldType.TrackRef));
}
- private static uint? ReadEnumOrdinal(ReadContext context, RefMode refMode)
+ private static void SkipValue(ReadContext context, TypeMetaFieldType
fieldType, RefMode refMode)
{
- return refMode switch
+ switch (refMode)
{
- RefMode.None => context.Reader.ReadVarUInt32(),
- RefMode.NullOnly => ReadNullableEnumOrdinal(context),
- RefMode.Tracking => throw new InvalidDataException("enum tracking
ref mode is not supported"),
- _ => throw new InvalidDataException($"unsupported ref mode
{refMode}"),
- };
- }
+ case RefMode.None:
+ SkipPayload(context, fieldType);
+ return;
+ case RefMode.NullOnly:
+ {
+ sbyte flag = context.Reader.ReadInt8();
+ if (flag == (sbyte)RefFlag.Null)
+ {
+ return;
+ }
- private static uint? ReadNullableEnumOrdinal(ReadContext context)
- {
- sbyte flag = context.Reader.ReadInt8();
- if (flag == (sbyte)RefFlag.Null)
- {
- return null;
- }
+ if (flag != (sbyte)RefFlag.NotNullValue)
+ {
+ throw new InvalidDataException($"unexpected nullOnly
flag {flag}");
+ }
- if (flag != (sbyte)RefFlag.NotNullValue)
- {
- throw new InvalidDataException($"unexpected enum nullOnly flag
{flag}");
+ SkipPayload(context, fieldType);
+ return;
+ }
+ case RefMode.Tracking:
+ _ = ReadTrackedValue(context, fieldType);
+ return;
+ default:
+ throw new InvalidDataException($"unsupported ref mode
{refMode}");
}
+ }
- return context.Reader.ReadVarUInt32();
+ private static object? ReadTrackedValue(ReadContext context,
TypeMetaFieldType fieldType)
+ {
+ return fieldType.TypeId switch
+ {
+ (uint)TypeId.String =>
context.TypeResolver.GetSerializer<string>().Read(context, RefMode.Tracking,
false),
+ (uint)TypeId.List =>
context.TypeResolver.GetSerializer<List<object?>>().Read(context,
RefMode.Tracking, false),
+ (uint)TypeId.Set =>
context.TypeResolver.GetSerializer<HashSet<object?>>().Read(context,
RefMode.Tracking, false),
+ (uint)TypeId.Map =>
context.TypeResolver.GetSerializer<NullableKeyDictionary<object,
object?>>().Read(context, RefMode.Tracking, false),
+ (uint)TypeId.Union or
+ (uint)TypeId.TypedUnion or
+ (uint)TypeId.NamedUnion =>
context.TypeResolver.GetSerializer<Union>().Read(context, RefMode.Tracking,
false),
+ _ => throw new InvalidDataException($"unsupported tracked skip
field type id {fieldType.TypeId}"),
+ };
}
- private static object? ReadFieldValue(ReadContext context,
TypeMetaFieldType fieldType)
+ private static void SkipPayload(ReadContext context, TypeMetaFieldType
fieldType)
{
- RefMode refMode = RefModeExtensions.From(fieldType.Nullable,
fieldType.TrackRef);
switch (fieldType.TypeId)
{
case (uint)TypeId.Bool:
- return
context.TypeResolver.GetSerializer<bool>().Read(context, refMode, false);
case (uint)TypeId.Int8:
- return
context.TypeResolver.GetSerializer<sbyte>().Read(context, refMode, false);
+ case (uint)TypeId.UInt8:
+ context.Reader.Skip(1);
+ return;
case (uint)TypeId.Int16:
- return
context.TypeResolver.GetSerializer<short>().Read(context, refMode, false);
- case (uint)TypeId.VarInt32:
- return context.TypeResolver.GetSerializer<int>().Read(context,
refMode, false);
- case (uint)TypeId.VarInt64:
- return
context.TypeResolver.GetSerializer<long>().Read(context, refMode, false);
+ case (uint)TypeId.UInt16:
case (uint)TypeId.Float16:
- return
context.TypeResolver.GetSerializer<Half>().Read(context, refMode, false);
case (uint)TypeId.BFloat16:
- return
context.TypeResolver.GetSerializer<BFloat16>().Read(context, refMode, false);
- case (uint)TypeId.Float16Array:
- return
context.TypeResolver.GetSerializer<Half[]>().Read(context, refMode, false);
- case (uint)TypeId.BFloat16Array:
- return
context.TypeResolver.GetSerializer<BFloat16[]>().Read(context, refMode, false);
+ context.Reader.Skip(2);
+ return;
+ case (uint)TypeId.Int32:
+ case (uint)TypeId.UInt32:
case (uint)TypeId.Float32:
- return
context.TypeResolver.GetSerializer<float>().Read(context, refMode, false);
+ case (uint)TypeId.Date:
+ context.Reader.Skip(4);
+ return;
+ case (uint)TypeId.Int64:
+ case (uint)TypeId.UInt64:
case (uint)TypeId.Float64:
- return
context.TypeResolver.GetSerializer<double>().Read(context, refMode, false);
+ case (uint)TypeId.Timestamp:
+ case (uint)TypeId.Duration:
+ context.Reader.Skip(8);
+ return;
+ case (uint)TypeId.VarInt32:
+ _ = context.Reader.ReadVarInt32();
+ return;
+ case (uint)TypeId.VarUInt32:
+ _ = context.Reader.ReadVarUInt32();
+ return;
+ case (uint)TypeId.VarInt64:
+ _ = context.Reader.ReadVarInt64();
+ return;
+ case (uint)TypeId.VarUInt64:
+ _ = context.Reader.ReadVarUInt64();
+ return;
+ case (uint)TypeId.TaggedInt64:
+ _ = context.Reader.ReadTaggedInt64();
+ return;
+ case (uint)TypeId.TaggedUInt64:
+ _ = context.Reader.ReadTaggedUInt64();
+ return;
case (uint)TypeId.String:
- return
context.TypeResolver.GetSerializer<string>().Read(context, refMode, false);
+ _ = StringSerializer.ReadString(context);
+ return;
case (uint)TypeId.Decimal:
- return
context.TypeResolver.GetSerializer<ForyDecimal>().Read(context, refMode, false);
+ _ =
context.TypeResolver.GetSerializer<ForyDecimal>().ReadData(context);
+ return;
+ case (uint)TypeId.Binary:
+ case (uint)TypeId.BoolArray:
+ case (uint)TypeId.Int8Array:
+ case (uint)TypeId.Int16Array:
+ case (uint)TypeId.Int32Array:
+ case (uint)TypeId.Int64Array:
+ case (uint)TypeId.UInt8Array:
+ case (uint)TypeId.UInt16Array:
+ case (uint)TypeId.UInt32Array:
+ case (uint)TypeId.UInt64Array:
+ case (uint)TypeId.Float16Array:
+ case (uint)TypeId.BFloat16Array:
+ case (uint)TypeId.Float32Array:
+ case (uint)TypeId.Float64Array:
+ SkipPackedArray(context);
+ return;
case (uint)TypeId.List:
- {
- if (fieldType.Generics.Count != 1 ||
fieldType.Generics[0].TypeId != (uint)TypeId.String)
- {
- throw new InvalidDataException("unsupported compatible
list element type");
- }
-
- return
context.TypeResolver.GetSerializer<List<string>>().Read(context, refMode,
false);
- }
case (uint)TypeId.Set:
+ SkipListOrSet(context, fieldType);
+ return;
+ case (uint)TypeId.Map:
+ SkipMap(context, fieldType);
+ return;
+ case (uint)TypeId.Enum:
+ case (uint)TypeId.NamedEnum:
+ _ = context.Reader.ReadVarUInt32();
+ return;
+ case (uint)TypeId.Union:
+ case (uint)TypeId.TypedUnion:
+ case (uint)TypeId.NamedUnion:
+ _ =
context.TypeResolver.GetSerializer<Union>().ReadData(context);
+ return;
+ default:
+ throw new InvalidDataException($"unsupported compatible field
type id {fieldType.TypeId}");
+ }
+ }
+
+ private static void SkipPackedArray(ReadContext context)
+ {
+ int payloadSize = checked((int)context.Reader.ReadVarUInt32());
+ context.Reader.Skip(payloadSize);
+ }
+
+ private static void SkipListOrSet(ReadContext context, TypeMetaFieldType
fieldType)
+ {
+ if (fieldType.Generics.Count != 1)
+ {
+ throw new InvalidDataException("list/set field metadata must have
one element type");
+ }
+
+ int length = checked((int)context.Reader.ReadVarUInt32());
+ if (length == 0)
+ {
+ return;
+ }
+
+ TypeMetaFieldType elementType = fieldType.Generics[0];
+ 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;
+ if (!sameType)
+ {
+ throw new InvalidDataException("dynamic compatible list/set skip
is not supported");
+ }
+
+ if (!declared)
+ {
+ _ = context.TypeResolver.ReadAnyTypeInfo(context);
+ }
+
+ RefMode elementRefMode = trackRef ? RefMode.Tracking : hasNull ?
RefMode.NullOnly : RefMode.None;
+ for (int i = 0; i < length; i++)
+ {
+ SkipValue(context, elementType, elementRefMode);
+ }
+ }
+
+ private static void SkipMap(ReadContext context, TypeMetaFieldType
fieldType)
+ {
+ if (fieldType.Generics.Count != 2)
+ {
+ throw new InvalidDataException("map field metadata must have
key/value types");
+ }
+
+ TypeMetaFieldType keyType = fieldType.Generics[0];
+ TypeMetaFieldType valueType = fieldType.Generics[1];
+ int totalLength = checked((int)context.Reader.ReadVarUInt32());
+ int readCount = 0;
+ while (readCount < totalLength)
+ {
+ byte header = context.Reader.ReadUInt8();
+ 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)
+ {
+ if (!keyNull)
{
- if (fieldType.Generics.Count != 1 ||
fieldType.Generics[0].TypeId != (uint)TypeId.String)
+ if (!keyDeclared)
{
- throw new InvalidDataException("unsupported compatible
set element type");
+ _ = context.TypeResolver.ReadAnyTypeInfo(context);
}
- return
context.TypeResolver.GetSerializer<HashSet<string>>().Read(context, refMode,
false);
+ SkipValue(context, keyType, trackKeyRef ? RefMode.Tracking
: RefMode.None);
}
- case (uint)TypeId.Map:
+
+ if (!valueNull)
{
- if (fieldType.Generics.Count != 2 ||
- fieldType.Generics[0].TypeId != (uint)TypeId.String ||
- fieldType.Generics[1].TypeId != (uint)TypeId.String)
+ if (!valueDeclared)
{
- throw new InvalidDataException("unsupported compatible
map key/value type");
+ _ = context.TypeResolver.ReadAnyTypeInfo(context);
}
- return
context.TypeResolver.GetSerializer<Dictionary<string, string>>().Read(context,
refMode, false);
+ SkipValue(context, valueType, trackValueRef ?
RefMode.Tracking : RefMode.None);
}
- case (uint)TypeId.Enum:
- return ReadEnumOrdinal(context, refMode);
- case (uint)TypeId.Union:
- case (uint)TypeId.TypedUnion:
- case (uint)TypeId.NamedUnion:
- return
context.TypeResolver.GetSerializer<Union>().Read(context, refMode, false);
- default:
- throw new InvalidDataException($"unsupported compatible field
type id {fieldType.TypeId}");
+
+ readCount++;
+ continue;
+ }
+
+ int chunkSize = context.Reader.ReadUInt8();
+ if (!keyDeclared)
+ {
+ _ = context.TypeResolver.ReadAnyTypeInfo(context);
+ }
+
+ if (!valueDeclared)
+ {
+ _ = context.TypeResolver.ReadAnyTypeInfo(context);
+ }
+
+ for (int i = 0; i < chunkSize; i++)
+ {
+ SkipValue(context, keyType, trackKeyRef ? RefMode.Tracking :
RefMode.None);
+ SkipValue(context, valueType, trackValueRef ? RefMode.Tracking
: RefMode.None);
+ }
+
+ readCount += chunkSize;
}
}
}
diff --git a/csharp/src/Fory/SchemaTypes.cs b/csharp/src/Fory/SchemaTypes.cs
new file mode 100644
index 000000000..35eedfa29
--- /dev/null
+++ b/csharp/src/Fory/SchemaTypes.cs
@@ -0,0 +1,62 @@
+// 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.Schema.Types;
+
+public sealed class Bool { private Bool() { } }
+public sealed class Int8 { private Int8() { } }
+public sealed class Int16 { private Int16() { } }
+public sealed class Int32 { private Int32() { } }
+public sealed class VarInt32 { private VarInt32() { } }
+public sealed class Int64 { private Int64() { } }
+public sealed class VarInt64 { private VarInt64() { } }
+public sealed class TaggedInt64 { private TaggedInt64() { } }
+public sealed class UInt8 { private UInt8() { } }
+public sealed class UInt16 { private UInt16() { } }
+public sealed class UInt32 { private UInt32() { } }
+public sealed class VarUInt32 { private VarUInt32() { } }
+public sealed class UInt64 { private UInt64() { } }
+public sealed class VarUInt64 { private VarUInt64() { } }
+public sealed class TaggedUInt64 { private TaggedUInt64() { } }
+public sealed class Float16 { private Float16() { } }
+public sealed class BFloat16 { private BFloat16() { } }
+public sealed class Float32 { private Float32() { } }
+public sealed class Float64 { private Float64() { } }
+public sealed class String { private String() { } }
+public sealed class Binary { private Binary() { } }
+public sealed class Date { private Date() { } }
+public sealed class Timestamp { private Timestamp() { } }
+public sealed class Duration { private Duration() { } }
+public sealed class Decimal { private Decimal() { } }
+
+public sealed class BoolArray { private BoolArray() { } }
+public sealed class Int8Array { private Int8Array() { } }
+public sealed class Int16Array { private Int16Array() { } }
+public sealed class Int32Array { private Int32Array() { } }
+public sealed class Int64Array { private Int64Array() { } }
+public sealed class UInt8Array { private UInt8Array() { } }
+public sealed class UInt16Array { private UInt16Array() { } }
+public sealed class UInt32Array { private UInt32Array() { } }
+public sealed class UInt64Array { private UInt64Array() { } }
+public sealed class Float16Array { private Float16Array() { } }
+public sealed class BFloat16Array { private BFloat16Array() { } }
+public sealed class Float32Array { private Float32Array() { } }
+public sealed class Float64Array { private Float64Array() { } }
+
+public sealed class List<TElement> { private List() { } }
+public sealed class Set<TElement> { private Set() { } }
+public sealed class Map<TKey, TValue> { private Map() { } }
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index 355fcb628..2c6f89bb1 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -21,6 +21,7 @@ using System.Collections.Immutable;
using System.Threading.Tasks;
using Apache.Fory;
using ForyRuntime = Apache.Fory.Fory;
+using S = Apache.Fory.Schema.Types;
namespace Apache.Fory.Tests;
@@ -69,15 +70,44 @@ public sealed class FieldOrder
}
[ForyObject]
-public sealed class EncodedNumbers
+public sealed class SchemaNumbers
{
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt32))]
public uint U32Fixed { get; set; }
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong U64Tagged { get; set; }
}
+[ForyObject]
+public sealed class NestedSchemaByName
+{
+ [ForyField(Type = typeof(S.Map<S.UInt32, S.List<S.TaggedUInt64>>))]
+ public Dictionary<uint, List<ulong?>?> Values { get; set; } = [];
+}
+
+[ForyObject]
+public sealed class NestedSchemaById
+{
+ [ForyField(3, Type = typeof(S.Map<S.UInt32, S.List<S.TaggedUInt64>>))]
+ public Dictionary<uint, List<ulong?>?> Values { get; set; } = [];
+}
+
+[ForyObject]
+public sealed class NestedSchemaSkipWriter
+{
+ [ForyField(Type = typeof(S.Map<S.UInt32, S.List<S.TaggedUInt64>>))]
+ public Dictionary<uint, List<ulong?>?> Values { get; set; } = [];
+
+ public int Tail { get; set; }
+}
+
+[ForyObject]
+public sealed class NestedSchemaSkipReader
+{
+ public int Tail { get; set; }
+}
+
[ForyObject]
public sealed class OneStringField
{
@@ -796,34 +826,108 @@ public sealed class ForyRuntimeTests
}
[Fact]
- public void MacroFieldEncodingOverridesForUnsignedTypes()
+ public void ForyFieldSchemaTypeOverridesForUnsignedTypes()
{
ForyRuntime fory = ForyRuntime.Builder().Build();
- fory.Register<EncodedNumbers>(301);
+ fory.Register<SchemaNumbers>(301);
- EncodedNumbers value = new()
+ SchemaNumbers value = new()
{
U32Fixed = 0x11223344u,
U64Tagged = (ulong)int.MaxValue + 99UL,
};
- EncodedNumbers decoded =
fory.Deserialize<EncodedNumbers>(fory.Serialize(value));
+ SchemaNumbers decoded =
fory.Deserialize<SchemaNumbers>(fory.Serialize(value));
Assert.Equal(value.U32Fixed, decoded.U32Fixed);
Assert.Equal(value.U64Tagged, decoded.U64Tagged);
}
+ [Fact]
+ public void ForyFieldTypeWithoutIdUsesNameBasedNestedSchema()
+ {
+ TypeResolver resolver = new();
+ resolver.GetTypeInfo<NestedSchemaByName>();
+ TypeMetaFieldInfo field =
Assert.Single(resolver.GetTypeInfo<NestedSchemaByName>().TypeMetaFields(false));
+
+ Assert.Null(field.FieldId);
+ Assert.Equal("values", field.FieldName);
+ Assert.Equal((uint)TypeId.Map, field.FieldType.TypeId);
+ Assert.Equal((uint)TypeId.UInt32, field.FieldType.Generics[0].TypeId);
+ Assert.Equal((uint)TypeId.List, field.FieldType.Generics[1].TypeId);
+ Assert.True(field.FieldType.Generics[1].Nullable);
+ Assert.Equal((uint)TypeId.TaggedUInt64,
field.FieldType.Generics[1].Generics[0].TypeId);
+ Assert.True(field.FieldType.Generics[1].Generics[0].Nullable);
+ }
+
+ [Fact]
+ public void ForyFieldTypeWithIdUsesAssignedIdNestedSchema()
+ {
+ TypeResolver resolver = new();
+ resolver.GetTypeInfo<NestedSchemaById>();
+ TypeMetaFieldInfo field =
Assert.Single(resolver.GetTypeInfo<NestedSchemaById>().TypeMetaFields(false));
+
+ Assert.Equal((short)3, field.FieldId);
+ Assert.Equal((uint)TypeId.Map, field.FieldType.TypeId);
+ Assert.Equal((uint)TypeId.UInt32, field.FieldType.Generics[0].TypeId);
+ Assert.Equal((uint)TypeId.TaggedUInt64,
field.FieldType.Generics[1].Generics[0].TypeId);
+ }
+
+ [Fact]
+ public void NestedSchemaAnnotationControlsMapAndListPayload()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ fory.Register<NestedSchemaByName>(303);
+
+ NestedSchemaByName value = new()
+ {
+ Values =
+ {
+ [4_000_000_000u] = [7UL, 1_000_000_000UL, null],
+ [3u] = [42UL],
+ },
+ };
+
+ NestedSchemaByName decoded =
fory.Deserialize<NestedSchemaByName>(fory.Serialize(value));
+ Assert.Equal(value.Values.Count, decoded.Values.Count);
+ Assert.Equal(value.Values[4_000_000_000u],
decoded.Values[4_000_000_000u]);
+ Assert.Equal(value.Values[3u], decoded.Values[3u]);
+ }
+
+ [Fact]
+ public void CompatibleSkipUsesRemoteNestedSchemaMetadata()
+ {
+ ForyRuntime writer = ForyRuntime.Builder().Compatible(true).Build();
+ writer.Register<NestedSchemaSkipWriter>(304);
+
+ ForyRuntime reader = ForyRuntime.Builder().Compatible(true).Build();
+ reader.Register<NestedSchemaSkipReader>(304);
+
+ NestedSchemaSkipWriter value = new()
+ {
+ Values =
+ {
+ [4_000_000_000u] = [7UL, 1_000_000_000UL],
+ [3u] = [42UL],
+ },
+ Tail = 99,
+ };
+
+ NestedSchemaSkipReader decoded =
reader.Deserialize<NestedSchemaSkipReader>(writer.Serialize(value));
+ Assert.Equal(99, decoded.Tail);
+ }
+
[Fact]
public void TaggedUnsignedFieldUsesCompactBoundary()
{
ForyRuntime fory = ForyRuntime.Builder().Build();
- fory.Register<EncodedNumbers>(301);
+ fory.Register<SchemaNumbers>(301);
- EncodedNumbers compact = new()
+ SchemaNumbers compact = new()
{
U32Fixed = 0x11223344u,
U64Tagged = (ulong)int.MaxValue,
};
- EncodedNumbers wide = new()
+ SchemaNumbers wide = new()
{
U32Fixed = 0x11223344u,
U64Tagged = (ulong)int.MaxValue + 1UL,
@@ -833,8 +937,8 @@ public sealed class ForyRuntimeTests
byte[] widePayload = fory.Serialize(wide);
Assert.Equal(5, widePayload.Length - compactPayload.Length);
- Assert.Equal(compact.U64Tagged,
fory.Deserialize<EncodedNumbers>(compactPayload).U64Tagged);
- Assert.Equal(wide.U64Tagged,
fory.Deserialize<EncodedNumbers>(widePayload).U64Tagged);
+ Assert.Equal(compact.U64Tagged,
fory.Deserialize<SchemaNumbers>(compactPayload).U64Tagged);
+ Assert.Equal(wide.U64Tagged,
fory.Deserialize<SchemaNumbers>(widePayload).U64Tagged);
}
[Theory]
diff --git a/csharp/tests/Fory.XlangPeer/Program.cs
b/csharp/tests/Fory.XlangPeer/Program.cs
index 80030eaf2..dfcf7fccb 100644
--- a/csharp/tests/Fory.XlangPeer/Program.cs
+++ b/csharp/tests/Fory.XlangPeer/Program.cs
@@ -20,6 +20,7 @@ using System.Numerics;
using System.Text;
using Apache.Fory;
using ForyRuntime = Apache.Fory.Fory;
+using S = Apache.Fory.Schema.Types;
namespace Apache.Fory.XlangPeer;
@@ -236,6 +237,8 @@ internal static class Program
"test_unsigned_schema_consistent_simple" =>
CaseUnsignedSchemaConsistentSimple(input),
"test_unsigned_schema_consistent" =>
CaseUnsignedSchemaConsistent(input),
"test_unsigned_schema_compatible" =>
CaseUnsignedSchemaCompatible(input),
+ "test_nested_annotated_container_schema_consistent" =>
CaseNestedAnnotatedContainerSchemaConsistent(input),
+ "test_nested_annotated_container_compatible" =>
CaseNestedAnnotatedContainerCompatible(input),
_ => throw new InvalidOperationException($"unknown test case
{caseName}"),
};
}
@@ -995,6 +998,20 @@ internal static class Program
return RoundTripSingle<UnsignedSchemaCompatible>(input, fory);
}
+ private static byte[] CaseNestedAnnotatedContainerSchemaConsistent(byte[]
input)
+ {
+ ForyRuntime fory = BuildFory(compatible: false);
+ fory.Register<NestedAnnotatedContainerSchemaConsistent>(801);
+ return
RoundTripSingle<NestedAnnotatedContainerSchemaConsistent>(input, fory);
+ }
+
+ private static byte[] CaseNestedAnnotatedContainerCompatible(byte[] input)
+ {
+ ForyRuntime fory = BuildFory(compatible: true);
+ fory.Register<NestedAnnotatedContainerCompatible>(802);
+ return RoundTripSingle<NestedAnnotatedContainerCompatible>(input,
fory);
+ }
+
private static byte[] RoundTripSingle<T>(byte[] input, ForyRuntime fory)
{
ReadOnlySequence<byte> sequence = new(input);
@@ -1357,10 +1374,10 @@ public sealed class CircularRefStruct
[ForyObject]
public sealed class UnsignedSchemaConsistentSimple
{
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong U64Tagged { get; set; }
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong? U64TaggedNullable { get; set; }
}
@@ -1371,30 +1388,30 @@ public sealed class UnsignedSchemaConsistent
public ushort U16Field { get; set; }
public uint U32VarField { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt32))]
public uint U32FixedField { get; set; }
public ulong U64VarField { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt64))]
public ulong U64FixedField { get; set; }
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong U64TaggedField { get; set; }
public byte? U8NullableField { get; set; }
public ushort? U16NullableField { get; set; }
public uint? U32VarNullableField { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt32))]
public uint? U32FixedNullableField { get; set; }
public ulong? U64VarNullableField { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt64))]
public ulong? U64FixedNullableField { get; set; }
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong? U64TaggedNullableField { get; set; }
}
@@ -1405,29 +1422,45 @@ public sealed class UnsignedSchemaCompatible
public ushort? U16Field1 { get; set; }
public uint? U32VarField1 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt32))]
public uint? U32FixedField1 { get; set; }
public ulong? U64VarField1 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt64))]
public ulong? U64FixedField1 { get; set; }
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong? U64TaggedField1 { get; set; }
public byte U8Field2 { get; set; }
public ushort U16Field2 { get; set; }
public uint U32VarField2 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt32))]
public uint U32FixedField2 { get; set; }
public ulong U64VarField2 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt64))]
public ulong U64FixedField2 { get; set; }
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong U64TaggedField2 { get; set; }
}
+
+#pragma warning disable CS8714
+[ForyObject]
+public sealed class NestedAnnotatedContainerSchemaConsistent
+{
+ [ForyField(Type = typeof(S.Map<S.UInt32, S.List<S.TaggedUInt64>>))]
+ public NullableKeyDictionary<uint?, List<ulong?>?> Values { get; set; } =
new();
+}
+
+[ForyObject]
+public sealed class NestedAnnotatedContainerCompatible
+{
+ [ForyField(Type = typeof(S.Map<S.UInt32, S.List<S.TaggedUInt64>>))]
+ public NullableKeyDictionary<uint?, List<ulong?>?> Values { get; set; } =
new();
+}
+#pragma warning restore CS8714
diff --git a/docs/guide/csharp/field-configuration.md
b/docs/guide/csharp/field-configuration.md
index 3fc502ee8..0e53eacf2 100644
--- a/docs/guide/csharp/field-configuration.md
+++ b/docs/guide/csharp/field-configuration.md
@@ -21,47 +21,64 @@ license: |
This page covers field-level serializer configuration for C# generated
serializers.
-## `[ForyObject]` and `[Field]`
+## `[ForyObject]` and `[ForyField]`
-Use `[ForyObject]` to enable source-generated serializers. Use `[Field]` to
override integer encoding for a specific field.
+Use `[ForyObject]` to enable source-generated serializers. Use `[ForyField]`
to assign an optional stable field id or to override the Fory schema type used
for a field.
```csharp
using Apache.Fory;
+using S = Apache.Fory.Schema.Types;
[ForyObject]
public sealed class Metrics
{
- // Fixed-width 32-bit encoding
- [Field(Encoding = FieldEncoding.Fixed)]
+ [ForyField(Type = typeof(S.UInt32))]
public uint Count { get; set; }
- // Tagged 64-bit encoding
- [Field(Encoding = FieldEncoding.Tagged)]
+ [ForyField(Type = typeof(S.TaggedUInt64))]
public ulong TraceId { get; set; }
- // Default (varint) encoding
public long LatencyMicros { get; set; }
}
```
-## Available Encodings
+`Id` is optional. When it is omitted, compatible mode still matches the field
by name.
-| Encoding | Meaning |
-| ---------------------- | ----------------------------------------------- |
-| `FieldEncoding.Varint` | Variable-length integer encoding (default) |
-| `FieldEncoding.Fixed` | Fixed-width integer encoding |
-| `FieldEncoding.Tagged` | Tagged integer encoding (`long` / `ulong` only) |
+```csharp
+using Apache.Fory;
+using S = Apache.Fory.Schema.Types;
+
+[ForyObject]
+public sealed class NestedMetrics
+{
+ [ForyField(Type = typeof(S.Map<S.UInt32, S.List<S.TaggedUInt64>>))]
+ public Dictionary<uint, List<ulong?>?> Values { get; set; } = [];
+
+ [ForyField(3, Type = typeof(S.UInt64))]
+ public ulong StableCount { get; set; }
+}
+```
+
+## Schema Descriptor Types
+
+Schema descriptors live under `Apache.Fory.Schema.Types` and are metadata
only. They do not replace normal C# carrier types.
+
+Common scalar descriptors include:
+
+- `S.Int32`, `S.VarInt32`, `S.UInt32`, `S.VarUInt32`
+- `S.Int64`, `S.VarInt64`, `S.TaggedInt64`
+- `S.UInt64`, `S.VarUInt64`, `S.TaggedUInt64`
+- `S.Float16`, `S.BFloat16`, `S.Float32`, `S.Float64`
-## Supported Field Types for Encoding Override
+Container descriptors are composable:
-`[Field(Encoding = ...)]` currently applies to:
+- `S.List<TElement>`
+- `S.Set<TElement>`
+- `S.Map<TKey, TValue>`
-- `int`
-- `uint`
-- `long`
-- `ulong`
+Packed array descriptors such as `S.Int32Array`, `S.UInt32Array`,
`S.Float16Array`, and `S.BFloat16Array` are available when the field should use
the packed array wire type.
-Nullable value variants (for example `long?`) are also handled by generated
serializers.
+Nullability comes from the C# carrier type. Use `List<ulong?>` for nullable
list elements and `NullableKeyDictionary<TKey, TValue>` when a map needs
nullable keys.
## Nullability and Reference Tracking
diff --git a/docs/guide/csharp/index.md b/docs/guide/csharp/index.md
index 95bcac2ac..f0a899c85 100644
--- a/docs/guide/csharp/index.md
+++ b/docs/guide/csharp/index.md
@@ -83,19 +83,19 @@ User decoded = fory.Deserialize<User>(payload);
## Documentation
-| Topic | Description
|
-| --------------------------------------------- |
------------------------------------------------ |
-| [Configuration](configuration.md) | Builder options and runtime
modes |
-| [Basic Serialization](basic-serialization.md) | Typed and dynamic
serialization APIs |
-| [Type Registration](type-registration.md) | Registering user types and
custom serializers |
-| [Custom Serializers](custom-serializers.md) | Implementing `Serializer<T>`
|
-| [Field Configuration](field-configuration.md) | `[Field]` attribute and
integer encoding options |
-| [References](references.md) | Shared/circular reference
handling |
-| [Schema Evolution](schema-evolution.md) | Compatible mode behavior
|
-| [Cross-Language](cross-language.md) | Interoperability guidance
|
-| [Supported Types](supported-types.md) | Built-in and generated type
support |
-| [Thread Safety](thread-safety.md) | `Fory` vs `ThreadSafeFory`
usage |
-| [Troubleshooting](troubleshooting.md) | Common errors and debugging
steps |
+| Topic | Description
|
+| --------------------------------------------- |
--------------------------------------------- |
+| [Configuration](configuration.md) | Builder options and runtime
modes |
+| [Basic Serialization](basic-serialization.md) | Typed and dynamic
serialization APIs |
+| [Type Registration](type-registration.md) | Registering user types and
custom serializers |
+| [Custom Serializers](custom-serializers.md) | Implementing `Serializer<T>`
|
+| [Field Configuration](field-configuration.md) | `[ForyField]` ids and schema
type descriptors |
+| [References](references.md) | Shared/circular reference
handling |
+| [Schema Evolution](schema-evolution.md) | Compatible mode behavior
|
+| [Cross-Language](cross-language.md) | Interoperability guidance
|
+| [Supported Types](supported-types.md) | Built-in and generated type
support |
+| [Thread Safety](thread-safety.md) | `Fory` vs `ThreadSafeFory`
usage |
+| [Troubleshooting](troubleshooting.md) | Common errors and debugging
steps |
## Related Resources
diff --git
a/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
b/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
index c1f320d8a..ded3f3923 100644
--- a/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/xlang/CPPXlangTest.java
@@ -210,6 +210,11 @@ public class CPPXlangTest extends XlangTestBase {
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 testNestedAnnotatedContainerSchemaConsistent(boolean
enableCodegen)
throws java.io.IOException {
@@ -222,11 +227,6 @@ public class CPPXlangTest extends XlangTestBase {
super.testNestedAnnotatedContainerCompatible(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);
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 42f1a4f5a..92c196ed5 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
@@ -355,4 +355,16 @@ public class CSharpXlangTest extends XlangTestBase {
public void testUnsignedSchemaCompatible(boolean enableCodegen) throws
java.io.IOException {
super.testUnsignedSchemaCompatible(enableCodegen);
}
+
+ @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+ public void testNestedAnnotatedContainerSchemaConsistent(boolean
enableCodegen)
+ throws java.io.IOException {
+ super.testNestedAnnotatedContainerSchemaConsistent(enableCodegen);
+ }
+
+ @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+ public void testNestedAnnotatedContainerCompatible(boolean enableCodegen)
+ throws java.io.IOException {
+ super.testNestedAnnotatedContainerCompatible(enableCodegen);
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]