diff --git a/README.md b/README.md index a2e0b59..d9ff364 100644 --- a/README.md +++ b/README.md @@ -129,5 +129,6 @@ Blittable structs are stored directly as raw struct bytes without an offset tabl - Validate integrity or authenticity before creating a View when required. - The wire format requires a little-endian runtime. - View structs expose a compile-time constant `IsBlittable`, indicating whether the underlying serialized type is a blittable struct. +- Nested blittable struct properties on blittable Views expose both a zero-copy View property and a `{PropertyName}_AsValue` property that reads the struct value from serialized memory. - You can use `.AsMemory()` extension method (returns `ReadOnlyMemory`) or `.Materialize()` extension method (for views of blittable structs to convert them back to the original struct). - Nested classes and structs must be marked with `[ZeroSerializer]`; otherwise the generator reports an unsupported property diagnostic. diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index bbf1538..05abd9b 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1283,9 +1283,21 @@ var propertyReturnType && property.NestedSerializableType is not null) { sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType)}(serializedMemory.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine($"{propertyAccessibility} {property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {EscapeIdentifier(property.Symbol.Name + "_AsValue")}"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("get"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); + return; } else { + sourceBuilder.AppendLine("// Fallback generated unexpectedly. According to the specification, this fallback should not be reached (the view always returns the view in any case)."); sourceBuilder.AppendLine($"{containingModel.QualifiedSourceTypeName} blittableSourceValue = MemoryMarshal.Read<{containingModel.QualifiedSourceTypeName}>(serializedMemory.Span);"); sourceBuilder.AppendLine($"return blittableSourceValue.{EscapeIdentifier(property.Symbol.Name)};"); } diff --git a/tests/SerializationModels.cs b/tests/SerializationModels.cs index efb6443..56bc0a4 100644 --- a/tests/SerializationModels.cs +++ b/tests/SerializationModels.cs @@ -294,6 +294,13 @@ public struct StrictBlittableStruct public int Value { get; init; } } +[StructLayout(LayoutKind.Sequential, Pack = 1)] +[ZeroSerializer] +public struct StrictBlittableStructWithNestedProperty +{ + public PackedRecord Nested { get; init; } +} + [ZeroSerializer] public sealed class NullableStructContainerModel { diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index 927393b..4cb9ef5 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -765,7 +765,7 @@ public void PrimitiveAndEnumArraysSerializeAndDeserializeCorrectly() } [Fact] - public void NestedSerializableTypePropertiesReturnViewStructInstances() + public void NestedSerializableTypePropertiesExposeExpectedViewAndValueAccessors() { // 1. Assert that nested blittable type returns view PropertyInfo? valueProperty = typeof(PackedContainerView).GetProperty(nameof(PackedContainerView.Value)); @@ -776,12 +776,44 @@ public void NestedSerializableTypePropertiesReturnViewStructInstances() Assert.NotNull(optionalValueProperty); Assert.Equal(typeof(Nullable), optionalValueProperty!.PropertyType); + PropertyInfo? valueAsValueProperty = typeof(StrictBlittableStructWithNestedPropertyView).GetProperty(nameof(StrictBlittableStructWithNestedPropertyView.Nested_AsValue)); + Assert.NotNull(valueAsValueProperty); + Assert.Equal(typeof(PackedRecord), valueAsValueProperty!.PropertyType); + + var source = new StrictBlittableStructWithNestedProperty + { + Nested = new PackedRecord { Number = 42, State = SignedState.Positive } + }; + byte[] buffer = new byte[StrictBlittableStructWithNestedPropertyView.RequiredByteLength]; + source.Serialize(buffer); + var view = new StrictBlittableStructWithNestedPropertyView(buffer); + TestAssert.Equal(42, view.Nested.Number, "Nested view Number"); + TestAssert.Equal(SignedState.Positive, view.Nested.State, "Nested view State"); + TestAssert.Equal(42, view.Nested_AsValue.Number, "Nested value Number"); + TestAssert.Equal(SignedState.Positive, view.Nested_AsValue.State, "Nested value State"); + // 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); } + [Fact] + public void NonBlittableViewsWithBlittableAndNonBlittablePropertiesDoNotExposeAsValueAccessors() + { + TestAssert.True(!NullableStructContainerModelView.IsBlittable, "The combined-property View is non-blittable"); + + PropertyInfo? blittableValueProperty = typeof(NullableStructContainerModelView).GetProperty("BlittableStruct_AsValue"); + PropertyInfo? nullableBlittableValueProperty = typeof(NullableStructContainerModelView).GetProperty("NullableBlittableStruct_AsValue"); + PropertyInfo? nonBlittableValueProperty = typeof(NullableStructContainerModelView).GetProperty("NonBlittableStruct_AsValue"); + PropertyInfo? nullableNonBlittableValueProperty = typeof(NullableStructContainerModelView).GetProperty("NullableNonBlittableStruct_AsValue"); + + Assert.Null(blittableValueProperty); + Assert.Null(nullableBlittableValueProperty); + Assert.Null(nonBlittableValueProperty); + Assert.Null(nullableNonBlittableValueProperty); + } + [Fact] public void StrictBlittableStructWithoutOffsetTableSerializesAsRawPayload() {