From c6a5870ad6d22000f0be1f4500dbabedd3d355d3 Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:50:38 +0900 Subject: [PATCH 1/8] Add value accessors for blittable properties --- README.md | 1 + src/ZeroSerializerGenerator.cs | 42 ++++++++++++++++++++++++++++++++++ tests/SerializationTests.cs | 15 ++++++++++++ 3 files changed, 58 insertions(+) diff --git a/README.md b/README.md index a2e0b59..a82fc2a 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. +- Blittable struct properties 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..d174d72 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1139,6 +1139,12 @@ private static void EmitView( { sourceBuilder.AppendLine(); EmitViewProperty(sourceBuilder, generationModel, generationModel.Properties[propertyIndex], propertyIndex, modelLookup); + if (generationModel.Properties[propertyIndex].Kind == PropertySerializationKind.BlittableStruct + && generationModel.Properties[propertyIndex].NestedSerializableType is not null) + { + sourceBuilder.AppendLine(); + EmitBlittableValueProperty(sourceBuilder, generationModel, generationModel.Properties[propertyIndex], propertyIndex); + } } sourceBuilder.CloseBlock(); } @@ -1361,6 +1367,42 @@ private static void EmitViewCollectionHeader( sourceBuilder.AppendLine($"int propertyPayloadByteCount = BinaryPrimitives.ReadInt32LittleEndian({serializedDataName}.Slice({propertyDataOffsetName}, 4));"); } + private static void EmitBlittableValueProperty( + GeneratedSourceBuilder sourceBuilder, + TypeGenerationModel containingModel, + PropertyGenerationModel property, + int propertyIndex) + { + string propertyAccessibility = IsEffectivelyPublic(property.Symbol.Type) ? "public" : "internal"; + string propertyType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + string serializedPropertyType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + sourceBuilder.AppendLine($"{propertyAccessibility} {propertyType} {EscapeIdentifier(property.Symbol.Name + "_AsValue")}"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("get"); + sourceBuilder.OpenBlock(); + + if (containingModel.IsBlittableStruct) + { + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); + } + else + { + sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); + sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); + if (property.IsNullableType) + { + sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("return default;"); + sourceBuilder.CloseBlock(); + } + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + } + + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); + } + private static void EmitExtensionClass( GeneratedSourceBuilder sourceBuilder, TypeGenerationModel generationModel, diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index 927393b..6a6e11f 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -776,6 +776,14 @@ public void NestedSerializableTypePropertiesReturnViewStructInstances() Assert.NotNull(optionalValueProperty); Assert.Equal(typeof(Nullable), optionalValueProperty!.PropertyType); + PropertyInfo? valueAsValueProperty = typeof(PackedContainerView).GetProperty(nameof(PackedContainerView.Value_AsValue)); + Assert.NotNull(valueAsValueProperty); + Assert.Equal(typeof(PackedRecord), valueAsValueProperty!.PropertyType); + + PropertyInfo? optionalValueAsValueProperty = typeof(PackedContainerView).GetProperty(nameof(PackedContainerView.OptionalValue_AsValue)); + Assert.NotNull(optionalValueAsValueProperty); + Assert.Equal(typeof(PackedRecord?), optionalValueAsValueProperty!.PropertyType); + // 2. Assert that nested non-blittable type returns view PropertyInfo? childProperty = typeof(VariableRecordView).GetProperty(nameof(VariableRecordView.Child)); Assert.NotNull(childProperty); @@ -1357,10 +1365,15 @@ public void NullableStructViewsSerializeAndDeserializeCorrectly() TestAssert.Equal(111, viewNonNull.BlittableStruct.Number, "BlittableStruct.Number"); TestAssert.Equal(SignedState.Positive, viewNonNull.BlittableStruct.State, "BlittableStruct.State"); + TestAssert.Equal(111, viewNonNull.BlittableStruct_AsValue.Number, "BlittableStruct_AsValue.Number"); + TestAssert.Equal(SignedState.Positive, viewNonNull.BlittableStruct_AsValue.State, "BlittableStruct_AsValue.State"); Assert.NotNull(viewNonNull.NullableBlittableStruct); TestAssert.Equal(222, viewNonNull.NullableBlittableStruct!.Value.Number, "NullableBlittableStruct.Number"); TestAssert.Equal(SignedState.Negative, viewNonNull.NullableBlittableStruct!.Value.State, "NullableBlittableStruct.State"); + Assert.NotNull(viewNonNull.NullableBlittableStruct_AsValue); + TestAssert.Equal(222, viewNonNull.NullableBlittableStruct_AsValue!.Value.Number, "NullableBlittableStruct_AsValue.Number"); + TestAssert.Equal(SignedState.Negative, viewNonNull.NullableBlittableStruct_AsValue!.Value.State, "NullableBlittableStruct_AsValue.State"); TestAssert.Equal(ByteState.Ready, viewNonNull.NonBlittableStruct.ByteState, "NonBlittableStruct.ByteState"); TestAssert.Equal(SignedState.Positive, viewNonNull.NonBlittableStruct.SignedState, "NonBlittableStruct.SignedState"); @@ -1383,7 +1396,9 @@ public void NullableStructViewsSerializeAndDeserializeCorrectly() var viewNull = new NullableStructContainerModelView(bufferNull.AsMemory(0, writtenNull)); TestAssert.Equal(333, viewNull.BlittableStruct.Number, "BlittableStruct.Number"); + TestAssert.Equal(333, viewNull.BlittableStruct_AsValue.Number, "BlittableStruct_AsValue.Number"); Assert.Null(viewNull.NullableBlittableStruct); + Assert.Null(viewNull.NullableBlittableStruct_AsValue); TestAssert.Equal(ByteState.Ready, viewNull.NonBlittableStruct.ByteState, "NonBlittableStruct.ByteState"); Assert.Null(viewNull.NullableNonBlittableStruct); } From 9ce6cc61ec82b14e0f8abafd810c41f7081eb527 Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:59:52 +0900 Subject: [PATCH 2/8] Test non-blittable structs omit value accessors --- tests/SerializationTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index 6a6e11f..ab08a58 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -790,6 +790,16 @@ public void NestedSerializableTypePropertiesReturnViewStructInstances() Assert.Equal(typeof(FixedClassView?), childProperty!.PropertyType); } + [Fact] + public void NonBlittableStructPropertiesDoNotExposeAsValueAccessors() + { + PropertyInfo? valueProperty = typeof(NullableStructContainerModelView).GetProperty("NonBlittableStruct_AsValue"); + PropertyInfo? nullableValueProperty = typeof(NullableStructContainerModelView).GetProperty("NullableNonBlittableStruct_AsValue"); + + Assert.Null(valueProperty); + Assert.Null(nullableValueProperty); + } + [Fact] public void StrictBlittableStructWithoutOffsetTableSerializesAsRawPayload() { From ccff39a59161d4741ec83081e7abf500717223a0 Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:06:15 +0900 Subject: [PATCH 3/8] Co-locate blittable view and value generation --- src/ZeroSerializerGenerator.cs | 35 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index d174d72..29588d0 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1139,12 +1139,6 @@ private static void EmitView( { sourceBuilder.AppendLine(); EmitViewProperty(sourceBuilder, generationModel, generationModel.Properties[propertyIndex], propertyIndex, modelLookup); - if (generationModel.Properties[propertyIndex].Kind == PropertySerializationKind.BlittableStruct - && generationModel.Properties[propertyIndex].NestedSerializableType is not null) - { - sourceBuilder.AppendLine(); - EmitBlittableValueProperty(sourceBuilder, generationModel, generationModel.Properties[propertyIndex], propertyIndex); - } } sourceBuilder.CloseBlock(); } @@ -1297,6 +1291,7 @@ var propertyReturnType } sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); + EmitBlittableValuePropertyIfApplicable(sourceBuilder, containingModel, property, propertyIndex); return; } @@ -1356,23 +1351,22 @@ var propertyReturnType sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); + EmitBlittableValuePropertyIfApplicable(sourceBuilder, containingModel, property, propertyIndex); } - private static void EmitViewCollectionHeader( - GeneratedSourceBuilder sourceBuilder, - string serializedDataName, - string propertyDataOffsetName) - { - // Null is handled by the property offset before this point; invalid negative lengths must reach Span.Slice unchanged. - sourceBuilder.AppendLine($"int propertyPayloadByteCount = BinaryPrimitives.ReadInt32LittleEndian({serializedDataName}.Slice({propertyDataOffsetName}, 4));"); - } - - private static void EmitBlittableValueProperty( + private static void EmitBlittableValuePropertyIfApplicable( GeneratedSourceBuilder sourceBuilder, TypeGenerationModel containingModel, PropertyGenerationModel property, int propertyIndex) { + if (property.Kind != PropertySerializationKind.BlittableStruct + || property.NestedSerializableType is null) + { + return; + } + + sourceBuilder.AppendLine(); string propertyAccessibility = IsEffectivelyPublic(property.Symbol.Type) ? "public" : "internal"; string propertyType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); string serializedPropertyType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); @@ -1403,6 +1397,15 @@ private static void EmitBlittableValueProperty( sourceBuilder.CloseBlock(); } + private static void EmitViewCollectionHeader( + GeneratedSourceBuilder sourceBuilder, + string serializedDataName, + string propertyDataOffsetName) + { + // Null is handled by the property offset before this point; invalid negative lengths must reach Span.Slice unchanged. + sourceBuilder.AppendLine($"int propertyPayloadByteCount = BinaryPrimitives.ReadInt32LittleEndian({serializedDataName}.Slice({propertyDataOffsetName}, 4));"); + } + private static void EmitExtensionClass( GeneratedSourceBuilder sourceBuilder, TypeGenerationModel generationModel, From 5a34053bac791c31a2d92ec8f440df176d11a1ff Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:10:34 +0900 Subject: [PATCH 4/8] Inline blittable value property generation --- src/ZeroSerializerGenerator.cs | 168 +++++++++++++++------------------ 1 file changed, 77 insertions(+), 91 deletions(-) diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 29588d0..9cdb97b 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1289,112 +1289,98 @@ var propertyReturnType sourceBuilder.AppendLine($"{containingModel.QualifiedSourceTypeName} blittableSourceValue = MemoryMarshal.Read<{containingModel.QualifiedSourceTypeName}>(serializedMemory.Span);"); sourceBuilder.AppendLine($"return blittableSourceValue.{EscapeIdentifier(property.Symbol.Name)};"); } - sourceBuilder.CloseBlock(); - sourceBuilder.CloseBlock(); - EmitBlittableValuePropertyIfApplicable(sourceBuilder, containingModel, property, propertyIndex); - return; - } - - sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); - sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); - if (IsNullRepresentedByZeroPropertyOffset(property)) - { - // Null is represented entirely by the offset table; no property payload marker is read. - sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); - sourceBuilder.OpenBlock(); - // Always use 'default' instead of 'null' for reference types. - sourceBuilder.AppendLine("return default;"); - sourceBuilder.CloseBlock(); - } - - switch (property.Kind) - { - case PropertySerializationKind.Primitive: - EmitPrimitiveRead( - sourceBuilder, - GetSerializedPropertyType(property), - "serializedData", - "propertyDataOffset"); - break; - case PropertySerializationKind.BlittableStruct: - if (property.NestedSerializableType is not null) - { - sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType)}(serializedMemory.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } - 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($"return MemoryMarshal.Read<{GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } - break; - case PropertySerializationKind.String: - EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); - // Keep string payloads borrowed; constructing a string here would allocate on every View access. - sourceBuilder.AppendLine("return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); - break; - case PropertySerializationKind.Array: - EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); - if (property.ArrayElementType!.SpecialType == SpecialType.System_Byte) - { - sourceBuilder.AppendLine("return serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount);"); - } - else - { - sourceBuilder.AppendLine($"return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); - } - break; - case PropertySerializationKind.Nested: - // Nested views receive an already-positioned memory region; their constructor has no offset semantics. - sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType!)}(serializedMemory.Slice(propertyDataOffset));"); - break; - } - - sourceBuilder.CloseBlock(); - sourceBuilder.CloseBlock(); - EmitBlittableValuePropertyIfApplicable(sourceBuilder, containingModel, property, propertyIndex); - } - - private static void EmitBlittableValuePropertyIfApplicable( - GeneratedSourceBuilder sourceBuilder, - TypeGenerationModel containingModel, - PropertyGenerationModel property, - int propertyIndex) - { - if (property.Kind != PropertySerializationKind.BlittableStruct - || property.NestedSerializableType is null) - { - return; - } - - sourceBuilder.AppendLine(); - string propertyAccessibility = IsEffectivelyPublic(property.Symbol.Type) ? "public" : "internal"; - string propertyType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - string serializedPropertyType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - sourceBuilder.AppendLine($"{propertyAccessibility} {propertyType} {EscapeIdentifier(property.Symbol.Name + "_AsValue")}"); - sourceBuilder.OpenBlock(); - sourceBuilder.AppendLine("get"); - sourceBuilder.OpenBlock(); - - if (containingModel.IsBlittableStruct) - { - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); } else { sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); - if (property.IsNullableType) + if (IsNullRepresentedByZeroPropertyOffset(property)) { + // Null is represented entirely by the offset table; no property payload marker is read. sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); sourceBuilder.OpenBlock(); + // Always use 'default' instead of 'null' for reference types. sourceBuilder.AppendLine("return default;"); sourceBuilder.CloseBlock(); } - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + + switch (property.Kind) + { + case PropertySerializationKind.Primitive: + EmitPrimitiveRead( + sourceBuilder, + GetSerializedPropertyType(property), + "serializedData", + "propertyDataOffset"); + break; + case PropertySerializationKind.BlittableStruct: + if (property.NestedSerializableType is not null) + { + sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType)}(serializedMemory.Slice(propertyDataOffset, {property.ElementByteCount}));"); + } + 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($"return MemoryMarshal.Read<{GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + } + break; + case PropertySerializationKind.String: + EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); + // Keep string payloads borrowed; constructing a string here would allocate on every View access. + sourceBuilder.AppendLine("return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); + break; + case PropertySerializationKind.Array: + EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); + if (property.ArrayElementType!.SpecialType == SpecialType.System_Byte) + { + sourceBuilder.AppendLine("return serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount);"); + } + else + { + sourceBuilder.AppendLine($"return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); + } + break; + case PropertySerializationKind.Nested: + // Nested views receive an already-positioned memory region; their constructor has no offset semantics. + sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType!)}(serializedMemory.Slice(propertyDataOffset));"); + break; + } } sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); + if (property.Kind == PropertySerializationKind.BlittableStruct + && property.NestedSerializableType is not null) + { + sourceBuilder.AppendLine(); + string valuePropertyType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + string serializedPropertyType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + sourceBuilder.AppendLine($"{propertyAccessibility} {valuePropertyType} {EscapeIdentifier(property.Symbol.Name + "_AsValue")}"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("get"); + sourceBuilder.OpenBlock(); + + if (containingModel.IsBlittableStruct) + { + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); + } + else + { + sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); + sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); + if (property.IsNullableType) + { + sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("return default;"); + sourceBuilder.CloseBlock(); + } + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + } + + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); + } } private static void EmitViewCollectionHeader( From 02a921235cc7ec2170399b33488afc48928e662c Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:19:32 +0900 Subject: [PATCH 5/8] Preserve existing view generation call --- src/ZeroSerializerGenerator.cs | 169 +++++++++++++++++---------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 9cdb97b..d8ca6e2 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1139,6 +1139,38 @@ private static void EmitView( { sourceBuilder.AppendLine(); EmitViewProperty(sourceBuilder, generationModel, generationModel.Properties[propertyIndex], propertyIndex, modelLookup); + PropertyGenerationModel property = generationModel.Properties[propertyIndex]; + if (property.Kind == PropertySerializationKind.BlittableStruct + && property.NestedSerializableType is not null) + { + string propertyAccessibility = IsEffectivelyPublic(property.Symbol.Type) ? "public" : "internal"; + string propertyType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + string serializedPropertyType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine($"{propertyAccessibility} {propertyType} {EscapeIdentifier(property.Symbol.Name + "_AsValue")}"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("get"); + sourceBuilder.OpenBlock(); + if (generationModel.IsBlittableStruct) + { + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); + } + else + { + sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); + sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); + if (property.IsNullableType) + { + sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); + sourceBuilder.OpenBlock(); + sourceBuilder.AppendLine("return default;"); + sourceBuilder.CloseBlock(); + } + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + } + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); + } } sourceBuilder.CloseBlock(); } @@ -1289,98 +1321,67 @@ var propertyReturnType sourceBuilder.AppendLine($"{containingModel.QualifiedSourceTypeName} blittableSourceValue = MemoryMarshal.Read<{containingModel.QualifiedSourceTypeName}>(serializedMemory.Span);"); sourceBuilder.AppendLine($"return blittableSourceValue.{EscapeIdentifier(property.Symbol.Name)};"); } - } - else - { - sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); - sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); - if (IsNullRepresentedByZeroPropertyOffset(property)) - { - // Null is represented entirely by the offset table; no property payload marker is read. - sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); - sourceBuilder.OpenBlock(); - // Always use 'default' instead of 'null' for reference types. - sourceBuilder.AppendLine("return default;"); - sourceBuilder.CloseBlock(); - } - - switch (property.Kind) - { - case PropertySerializationKind.Primitive: - EmitPrimitiveRead( - sourceBuilder, - GetSerializedPropertyType(property), - "serializedData", - "propertyDataOffset"); - break; - case PropertySerializationKind.BlittableStruct: - if (property.NestedSerializableType is not null) - { - sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType)}(serializedMemory.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } - 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($"return MemoryMarshal.Read<{GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } - break; - case PropertySerializationKind.String: - EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); - // Keep string payloads borrowed; constructing a string here would allocate on every View access. - sourceBuilder.AppendLine("return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); - break; - case PropertySerializationKind.Array: - EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); - if (property.ArrayElementType!.SpecialType == SpecialType.System_Byte) - { - sourceBuilder.AppendLine("return serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount);"); - } - else - { - sourceBuilder.AppendLine($"return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); - } - break; - case PropertySerializationKind.Nested: - // Nested views receive an already-positioned memory region; their constructor has no offset semantics. - sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType!)}(serializedMemory.Slice(propertyDataOffset));"); - break; - } + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); + return; } - sourceBuilder.CloseBlock(); - sourceBuilder.CloseBlock(); - if (property.Kind == PropertySerializationKind.BlittableStruct - && property.NestedSerializableType is not null) + sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); + sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); + if (IsNullRepresentedByZeroPropertyOffset(property)) { - sourceBuilder.AppendLine(); - string valuePropertyType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - string serializedPropertyType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - sourceBuilder.AppendLine($"{propertyAccessibility} {valuePropertyType} {EscapeIdentifier(property.Symbol.Name + "_AsValue")}"); - sourceBuilder.OpenBlock(); - sourceBuilder.AppendLine("get"); + // Null is represented entirely by the offset table; no property payload marker is read. + sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); sourceBuilder.OpenBlock(); + // Always use 'default' instead of 'null' for reference types. + sourceBuilder.AppendLine("return default;"); + sourceBuilder.CloseBlock(); + } - if (containingModel.IsBlittableStruct) - { - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); - } - else - { - sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); - sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); - if (property.IsNullableType) + switch (property.Kind) + { + case PropertySerializationKind.Primitive: + EmitPrimitiveRead( + sourceBuilder, + GetSerializedPropertyType(property), + "serializedData", + "propertyDataOffset"); + break; + case PropertySerializationKind.BlittableStruct: + if (property.NestedSerializableType is not null) { - sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); - sourceBuilder.OpenBlock(); - sourceBuilder.AppendLine("return default;"); - sourceBuilder.CloseBlock(); + sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType)}(serializedMemory.Slice(propertyDataOffset, {property.ElementByteCount}));"); } - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } - - sourceBuilder.CloseBlock(); - sourceBuilder.CloseBlock(); + 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($"return MemoryMarshal.Read<{GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); + } + break; + case PropertySerializationKind.String: + EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); + // Keep string payloads borrowed; constructing a string here would allocate on every View access. + sourceBuilder.AppendLine("return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); + break; + case PropertySerializationKind.Array: + EmitViewCollectionHeader(sourceBuilder, "serializedData", "propertyDataOffset"); + if (property.ArrayElementType!.SpecialType == SpecialType.System_Byte) + { + sourceBuilder.AppendLine("return serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount);"); + } + else + { + sourceBuilder.AppendLine($"return MemoryMarshal.Cast(serializedData.Slice(propertyDataOffset + 4, propertyPayloadByteCount));"); + } + break; + case PropertySerializationKind.Nested: + // Nested views receive an already-positioned memory region; their constructor has no offset semantics. + sourceBuilder.AppendLine($"return new {GetQualifiedViewName(property.NestedSerializableType!)}(serializedMemory.Slice(propertyDataOffset));"); + break; } + + sourceBuilder.CloseBlock(); + sourceBuilder.CloseBlock(); } private static void EmitViewCollectionHeader( From 16a98e6ae00d39314a0625cfc72d233660ebb26c Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:25:10 +0900 Subject: [PATCH 6/8] Limit value accessors to strict blittable views --- README.md | 2 +- src/ZeroSerializerGenerator.cs | 21 +++------------------ tests/SerializationModels.cs | 7 +++++++ tests/SerializationTests.cs | 31 ++++++++++++++++++------------- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index a82fc2a..d9ff364 100644 --- a/README.md +++ b/README.md @@ -129,6 +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. -- Blittable struct properties expose both a zero-copy View property and a `{PropertyName}_AsValue` property that reads the struct value from serialized memory. +- 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 d8ca6e2..9556e96 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1140,7 +1140,8 @@ private static void EmitView( sourceBuilder.AppendLine(); EmitViewProperty(sourceBuilder, generationModel, generationModel.Properties[propertyIndex], propertyIndex, modelLookup); PropertyGenerationModel property = generationModel.Properties[propertyIndex]; - if (property.Kind == PropertySerializationKind.BlittableStruct + if (generationModel.IsBlittableStruct + && property.Kind == PropertySerializationKind.BlittableStruct && property.NestedSerializableType is not null) { string propertyAccessibility = IsEffectivelyPublic(property.Symbol.Type) ? "public" : "internal"; @@ -1151,23 +1152,7 @@ private static void EmitView( sourceBuilder.OpenBlock(); sourceBuilder.AppendLine("get"); sourceBuilder.OpenBlock(); - if (generationModel.IsBlittableStruct) - { - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); - } - else - { - sourceBuilder.AppendLine("ReadOnlySpan serializedData = serializedMemory.Span;"); - sourceBuilder.AppendLine($"int propertyDataOffset = BinaryPrimitives.ReadInt32LittleEndian(serializedData.Slice({propertyIndex * 4}, 4));"); - if (property.IsNullableType) - { - sourceBuilder.AppendLine("if (propertyDataOffset == 0)"); - sourceBuilder.OpenBlock(); - sourceBuilder.AppendLine("return default;"); - sourceBuilder.CloseBlock(); - } - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedData.Slice(propertyDataOffset, {property.ElementByteCount}));"); - } + sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); sourceBuilder.CloseBlock(); sourceBuilder.CloseBlock(); } 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 ab08a58..de8b4ea 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,13 +776,21 @@ public void NestedSerializableTypePropertiesReturnViewStructInstances() Assert.NotNull(optionalValueProperty); Assert.Equal(typeof(Nullable), optionalValueProperty!.PropertyType); - PropertyInfo? valueAsValueProperty = typeof(PackedContainerView).GetProperty(nameof(PackedContainerView.Value_AsValue)); + PropertyInfo? valueAsValueProperty = typeof(StrictBlittableStructWithNestedPropertyView).GetProperty(nameof(StrictBlittableStructWithNestedPropertyView.Nested_AsValue)); Assert.NotNull(valueAsValueProperty); Assert.Equal(typeof(PackedRecord), valueAsValueProperty!.PropertyType); - PropertyInfo? optionalValueAsValueProperty = typeof(PackedContainerView).GetProperty(nameof(PackedContainerView.OptionalValue_AsValue)); - Assert.NotNull(optionalValueAsValueProperty); - Assert.Equal(typeof(PackedRecord?), optionalValueAsValueProperty!.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)); @@ -791,11 +799,15 @@ public void NestedSerializableTypePropertiesReturnViewStructInstances() } [Fact] - public void NonBlittableStructPropertiesDoNotExposeAsValueAccessors() + public void NonBlittableViewsDoNotExposeAsValueAccessors() { + PropertyInfo? blittableValueProperty = typeof(PackedContainerView).GetProperty("Value_AsValue"); + PropertyInfo? nullableBlittableValueProperty = typeof(PackedContainerView).GetProperty("OptionalValue_AsValue"); PropertyInfo? valueProperty = typeof(NullableStructContainerModelView).GetProperty("NonBlittableStruct_AsValue"); PropertyInfo? nullableValueProperty = typeof(NullableStructContainerModelView).GetProperty("NullableNonBlittableStruct_AsValue"); + Assert.Null(blittableValueProperty); + Assert.Null(nullableBlittableValueProperty); Assert.Null(valueProperty); Assert.Null(nullableValueProperty); } @@ -1375,15 +1387,10 @@ public void NullableStructViewsSerializeAndDeserializeCorrectly() TestAssert.Equal(111, viewNonNull.BlittableStruct.Number, "BlittableStruct.Number"); TestAssert.Equal(SignedState.Positive, viewNonNull.BlittableStruct.State, "BlittableStruct.State"); - TestAssert.Equal(111, viewNonNull.BlittableStruct_AsValue.Number, "BlittableStruct_AsValue.Number"); - TestAssert.Equal(SignedState.Positive, viewNonNull.BlittableStruct_AsValue.State, "BlittableStruct_AsValue.State"); Assert.NotNull(viewNonNull.NullableBlittableStruct); TestAssert.Equal(222, viewNonNull.NullableBlittableStruct!.Value.Number, "NullableBlittableStruct.Number"); TestAssert.Equal(SignedState.Negative, viewNonNull.NullableBlittableStruct!.Value.State, "NullableBlittableStruct.State"); - Assert.NotNull(viewNonNull.NullableBlittableStruct_AsValue); - TestAssert.Equal(222, viewNonNull.NullableBlittableStruct_AsValue!.Value.Number, "NullableBlittableStruct_AsValue.Number"); - TestAssert.Equal(SignedState.Negative, viewNonNull.NullableBlittableStruct_AsValue!.Value.State, "NullableBlittableStruct_AsValue.State"); TestAssert.Equal(ByteState.Ready, viewNonNull.NonBlittableStruct.ByteState, "NonBlittableStruct.ByteState"); TestAssert.Equal(SignedState.Positive, viewNonNull.NonBlittableStruct.SignedState, "NonBlittableStruct.SignedState"); @@ -1406,9 +1413,7 @@ public void NullableStructViewsSerializeAndDeserializeCorrectly() var viewNull = new NullableStructContainerModelView(bufferNull.AsMemory(0, writtenNull)); TestAssert.Equal(333, viewNull.BlittableStruct.Number, "BlittableStruct.Number"); - TestAssert.Equal(333, viewNull.BlittableStruct_AsValue.Number, "BlittableStruct_AsValue.Number"); Assert.Null(viewNull.NullableBlittableStruct); - Assert.Null(viewNull.NullableBlittableStruct_AsValue); TestAssert.Equal(ByteState.Ready, viewNull.NonBlittableStruct.ByteState, "NonBlittableStruct.ByteState"); Assert.Null(viewNull.NullableNonBlittableStruct); } From c33592ce4f8eadd418c6c668edb14bdee94e8301 Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:36:08 +0900 Subject: [PATCH 7/8] Emit strict value accessor beside view getter --- src/ZeroSerializerGenerator.cs | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 9556e96..05abd9b 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -1139,23 +1139,6 @@ private static void EmitView( { sourceBuilder.AppendLine(); EmitViewProperty(sourceBuilder, generationModel, generationModel.Properties[propertyIndex], propertyIndex, modelLookup); - PropertyGenerationModel property = generationModel.Properties[propertyIndex]; - if (generationModel.IsBlittableStruct - && property.Kind == PropertySerializationKind.BlittableStruct - && property.NestedSerializableType is not null) - { - string propertyAccessibility = IsEffectivelyPublic(property.Symbol.Type) ? "public" : "internal"; - string propertyType = property.Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - string serializedPropertyType = GetSerializedPropertyType(property).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - sourceBuilder.AppendLine(); - sourceBuilder.AppendLine($"{propertyAccessibility} {propertyType} {EscapeIdentifier(property.Symbol.Name + "_AsValue")}"); - sourceBuilder.OpenBlock(); - sourceBuilder.AppendLine("get"); - sourceBuilder.OpenBlock(); - sourceBuilder.AppendLine($"return MemoryMarshal.Read<{serializedPropertyType}>(serializedMemory.Span.Slice({property.BlittableByteOffset}, {property.ElementByteCount}));"); - sourceBuilder.CloseBlock(); - sourceBuilder.CloseBlock(); - } } sourceBuilder.CloseBlock(); } @@ -1300,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)};"); } From 9e1afbe873110e295d112dddc55d9e9d95e45b12 Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:36:13 +0900 Subject: [PATCH 8/8] Cover mixed properties on non-blittable views --- tests/SerializationTests.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index de8b4ea..4cb9ef5 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -799,17 +799,19 @@ public void NestedSerializableTypePropertiesExposeExpectedViewAndValueAccessors( } [Fact] - public void NonBlittableViewsDoNotExposeAsValueAccessors() + public void NonBlittableViewsWithBlittableAndNonBlittablePropertiesDoNotExposeAsValueAccessors() { - PropertyInfo? blittableValueProperty = typeof(PackedContainerView).GetProperty("Value_AsValue"); - PropertyInfo? nullableBlittableValueProperty = typeof(PackedContainerView).GetProperty("OptionalValue_AsValue"); - PropertyInfo? valueProperty = typeof(NullableStructContainerModelView).GetProperty("NonBlittableStruct_AsValue"); - PropertyInfo? nullableValueProperty = typeof(NullableStructContainerModelView).GetProperty("NullableNonBlittableStruct_AsValue"); + 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(valueProperty); - Assert.Null(nullableValueProperty); + Assert.Null(nonBlittableValueProperty); + Assert.Null(nullableNonBlittableValueProperty); } [Fact]