diff --git a/benchmark/Benchmark.cs b/benchmark/Benchmark.cs index f7bd8d2..da7bac1 100644 --- a/benchmark/Benchmark.cs +++ b/benchmark/Benchmark.cs @@ -126,10 +126,10 @@ public int DeserializeAllProperties() ReadOnlySpan integers = view.Integers; ReadOnlySpan longs = view.Longs; ReadOnlySpan packedValues = view.PackedValues; - NestedPayloadView nested = view.Nested; - int nestedVersion = nested.Version; - ReadOnlySpan nestedLabel = nested.Label; - PackedBenchmarkValueView nestedSummary = nested.Summary; + NestedPayloadView? nested = view.Nested; + int nestedVersion = nested?.Version ?? -1; + ReadOnlySpan nestedLabel = (nested ?? new()).Label; // Nullable> is invalid + PackedBenchmarkValueView nestedSummary = nested?.Summary ?? new(); NestedStructPayloadView nestedStruct = view.NestedStruct; int nestedStructCode = nestedStruct.Code; long nestedStructAmount = nestedStruct.Amount; diff --git a/src/FieldGenerationModel.cs b/src/FieldGenerationModel.cs index 0d2d1f0..67e9d56 100644 --- a/src/FieldGenerationModel.cs +++ b/src/FieldGenerationModel.cs @@ -33,6 +33,8 @@ internal FieldGenerationModel( internal int ElementByteCount { get; } + internal int BlittableByteOffset { get; set; } + internal ITypeSymbol? ArrayElementType { get; } internal INamedTypeSymbol? NestedSerializableType { get; } diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 5cc1985..82ac9ab 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -328,6 +328,7 @@ private static TypeGenerationModel CreateGenerationModel( } // Roslyn's member order is the wire declaration order; never infer a different order from file paths or spans. + int blittableByteOffset = 0; foreach (ISymbol declaredMember in serializableType.GetMembers()) { // Only public getter properties define the wire contract; fields, setters, and indexers must never leak into it. @@ -362,7 +363,9 @@ private static TypeGenerationModel CreateGenerationModel( continue; } + propertyModel.BlittableByteOffset = blittableByteOffset; generationModel.Fields.Add(propertyModel); + blittableByteOffset += propertyModel.ElementByteCount; } return generationModel; @@ -1194,14 +1197,28 @@ private static void EmitViewProperty( propertyType = field.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } - sourceBuilder.AppendLine($"{propertyAccessibility} {propertyType} {EscapeIdentifier(field.Symbol.Name)}"); + var propertyReturnType + = field.Kind is FieldSerializationKind.BlittableStruct or FieldSerializationKind.Nested + ? (field.NullableUnderlyingType is not null || field.Symbol.Type.TypeKind is TypeKind.Class) + ? GetQualifiedViewName(field.NestedSerializableType) + "?" + : GetQualifiedViewName(field.NestedSerializableType) + : propertyType; + sourceBuilder.AppendLine($"{propertyAccessibility} {propertyReturnType} {EscapeIdentifier(field.Symbol.Name)}"); sourceBuilder.OpenBlock(); sourceBuilder.AppendLine("get"); sourceBuilder.OpenBlock(); if (containingModel.IsBlittableStruct) { - sourceBuilder.AppendLine($"{containingModel.QualifiedSourceTypeName} blittableSourceValue = MemoryMarshal.Read<{containingModel.QualifiedSourceTypeName}>(serializedMemory.Span);"); - sourceBuilder.AppendLine($"return blittableSourceValue.{EscapeIdentifier(field.Symbol.Name)};"); + if (field.Kind == FieldSerializationKind.BlittableStruct + && field.NestedSerializableType is not null) + { + sourceBuilder.AppendLine($"return new {GetQualifiedViewName(field.NestedSerializableType)}(serializedMemory.Slice({field.BlittableByteOffset}, {field.ElementByteCount}));"); + } + else + { + sourceBuilder.AppendLine($"{containingModel.QualifiedSourceTypeName} blittableSourceValue = MemoryMarshal.Read<{containingModel.QualifiedSourceTypeName}>(serializedMemory.Span);"); + sourceBuilder.AppendLine($"return blittableSourceValue.{EscapeIdentifier(field.Symbol.Name)};"); + } sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); return; @@ -1214,14 +1231,8 @@ private static void EmitViewProperty( // Null is represented entirely by the offset table; no property payload marker is read. sourceBuilder.AppendLine("if (fieldDataOffset == 0)"); sourceBuilder.OpenBlock(); - if (field.NullableUnderlyingType is not null && field.Kind != FieldSerializationKind.Nested) - { - sourceBuilder.AppendLine("return null;"); - } - else - { - sourceBuilder.AppendLine("return default;"); - } + // Always use 'default' instead of 'null' for reference types. + sourceBuilder.AppendLine("return default;"); sourceBuilder.CloseBlock(); } diff --git a/tests-unity/UnityCompatibility.cs b/tests-unity/UnityCompatibility.cs index f0bd69e..4b94dd2 100644 --- a/tests-unity/UnityCompatibility.cs +++ b/tests-unity/UnityCompatibility.cs @@ -119,11 +119,11 @@ && variableView.OptionalState == PacketState.Ready && variableView.OptionalPosition!.Value.X == 30 && variableView.MissingOptionalPosition is null - && variableView.Child.Identifier == 99 + && variableView.Child?.Identifier == 99 && variableView.StructChild.Identifier == 100 && variableView.StructChild.Name.SequenceEqual("struct".AsSpan()) - && variableView.OptionalStructChild.Identifier == 101 - && variableView.OptionalStructChild.Name.SequenceEqual("optional struct".AsSpan()) + && variableView.OptionalStructChild?.Identifier == 101 + && variableView.OptionalStructChild?.Name.SequenceEqual("optional struct".AsSpan()) == true && variableView.FloatValues.Length == 3 && variableView.FloatValues[1] == 2.5f && variableView.DoubleValues.Length == 3 diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index 40a306e..aed98f0 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -246,12 +246,12 @@ public void VariableDataRoundTrip() int expectedRequiredByteLength = -(24 + (4 * IntPtr.Size)); TestAssert.Equal(expectedRequiredByteLength, VariableRecordView.RequiredByteLength, nameof(VariableRecordView.RequiredByteLength)); - TestAssert.Equal(source.Text, view.Text.ToString(), nameof(view.Text)); - TestAssert.SequenceEqual(source.Values, view.Values, nameof(view.Values)); - TestAssert.Equal(source.OptionalNumber, view.OptionalNumber, nameof(view.OptionalNumber)); - TestAssert.Equal(source.Child.Identifier, view.Child.Identifier, nameof(view.Child.Identifier)); - TestAssert.Equal(source.Child.State, view.Child.State, nameof(view.Child.State)); - TestAssert.Equal(source.Tail, view.Tail, nameof(view.Tail)); + TestAssert.Equal(source.Text, view.Text.ToString(), nameof(source.Text)); + TestAssert.SequenceEqual(source.Values, view.Values, nameof(source.Values)); + TestAssert.Equal(source.OptionalNumber, view.OptionalNumber, nameof(source.OptionalNumber)); + TestAssert.Equal(source.Child.Identifier, view.Child?.Identifier ?? -1, nameof(source.Child.Identifier)); + TestAssert.Equal(source.Child.State, view.Child?.State ?? ByteState.None, nameof(source.Child.State)); + TestAssert.Equal(source.Tail, view.Tail, nameof(source.Tail)); int textFieldOffset = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4)); int valuesFieldOffset = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(4, 4)); int optionalNumberFieldOffset = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(8, 4)); @@ -284,8 +284,8 @@ public void VariableViewOnlyRequiresCorrectSerializedStart() TestAssert.Equal(source.Text, view.Text.ToString(), nameof(view.Text)); TestAssert.SequenceEqual(source.Values, view.Values, nameof(view.Values)); TestAssert.Equal(source.OptionalNumber, view.OptionalNumber, nameof(view.OptionalNumber)); - TestAssert.Equal(source.Child.Identifier, view.Child.Identifier, nameof(view.Child.Identifier)); - TestAssert.Equal(source.Child.State, view.Child.State, nameof(view.Child.State)); + TestAssert.Equal(source.Child.Identifier, view.Child?.Identifier ?? -1, nameof(FixedClassView.Identifier)); + TestAssert.Equal(source.Child.State, view.Child?.State ?? ByteState.None, nameof(FixedClassView.State)); TestAssert.Equal(source.Tail, view.Tail, nameof(view.Tail)); ReadOnlyMemory borrowedSerializedMemory = view; @@ -521,9 +521,9 @@ public void EveryTruncatedSerializedBufferThrowsStandardBoundsExceptionWhenRead( _ = view.Text.Length; _ = view.Values.Length; _ = view.OptionalNumber; - FixedClassView childView = view.Child; - _ = childView.Identifier; - _ = childView.State; + FixedClassView? childView = view.Child; + _ = childView?.Identifier; + _ = childView?.State; _ = view.Tail; }, nameof(VariableRecord)); @@ -777,7 +777,7 @@ public void NestedTypesReturnViewsTest() // 2. Assert that nested non-blittable type returns view PropertyInfo? childProperty = typeof(VariableRecordView).GetProperty(nameof(VariableRecordView.Child)); Assert.NotNull(childProperty); - Assert.Equal(typeof(FixedClassView), childProperty!.PropertyType); + Assert.Equal(typeof(FixedClassView?), childProperty!.PropertyType); } public void StrictBlittableStructTests()