diff --git a/src/idl_gen_csharp.cpp b/src/idl_gen_csharp.cpp index 9988523f32f..1430999b1f7 100644 --- a/src/idl_gen_csharp.cpp +++ b/src/idl_gen_csharp.cpp @@ -1189,6 +1189,58 @@ class CSharpGenerator : public BaseGenerator { } code += " }\n"; } + + // Generate Length property and ByteBuffer accessor for arrays in structs. + if (IsArray(field.value.type) && struct_def.fixed && + IsScalar(field.value.type.VectorType().base_type)) { + auto camel_name = Name(field); + if (camel_name == struct_def.name) { camel_name += "_"; } + + // Generate Length constant + code += " public const int " + camel_name; + code += "Length = "; + code += NumToString(field.value.type.fixed_length); + code += ";\n"; + + // Generate GetBytes methods for scalar arrays (similar to vector pattern) + code += "#if ENABLE_SPAN_T\n"; + code += " public Span<" + GenTypeBasic(field.value.type.VectorType()) + + "> Get"; + code += camel_name; + code += "Bytes() { return "; + + // For byte arrays, we can return the span directly + if (field.value.type.VectorType().base_type == BASE_TYPE_UCHAR) { + code += "__p.bb.ToSpan(__p.bb_pos + "; + code += NumToString(field.value.offset); + code += ", "; + code += NumToString(field.value.type.fixed_length * + SizeOf(field.value.type.VectorType().base_type)); + code += ")"; + } else { + // For other types, we need to cast the byte span + code += "System.Runtime.InteropServices.MemoryMarshal.Cast(__p.bb.ToSpan(__p.bb_pos + "; + code += NumToString(field.value.offset); + code += ", "; + code += NumToString(field.value.type.fixed_length * + SizeOf(field.value.type.VectorType().base_type)); + code += "))"; + } + code += "; }\n"; + code += "#else\n"; + code += " public ArraySegment? Get"; + code += camel_name; + code += "Bytes() { return "; + code += "__p.bb.ToArraySegment(__p.bb_pos + "; + code += NumToString(field.value.offset); + code += ", "; + code += NumToString(field.value.type.fixed_length * + SizeOf(field.value.type.VectorType().base_type)); + code += ");}\n"; + code += "#endif\n"; + } + // generate object accessors if is nested_flatbuffer if (field.nested_flatbuffer) { auto nested_type_name = NamespacedName(*field.nested_flatbuffer); diff --git a/tests/FlatBuffers.Test/FlatBuffersFixedLengthArrayTests.cs b/tests/FlatBuffers.Test/FlatBuffersFixedLengthArrayTests.cs new file mode 100644 index 00000000000..1c9a8f327d0 --- /dev/null +++ b/tests/FlatBuffers.Test/FlatBuffersFixedLengthArrayTests.cs @@ -0,0 +1,249 @@ +/* + * Copyright 2025 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Linq; +using MyGame.Example; + +namespace Google.FlatBuffers.Test +{ + [FlatBuffersTestClass] + public class FlatBuffersFixedLengthArrayTests + { + [FlatBuffersTestMethod] + public void FixedLengthArray_LengthConstantsMatchSchema_ReturnTrue() + { + const int nestedALength = NestedStruct.ALength; + const int nestedCLength = NestedStruct.CLength; + const int nestedDLength = NestedStruct.DLength; + const int arrayBLength = ArrayStruct.BLength; + const int arrayFLength = ArrayStruct.FLength; + + Assert.AreEqual(2, nestedALength); + Assert.AreEqual(2, nestedCLength); + Assert.AreEqual(2, nestedDLength); + Assert.AreEqual(15, arrayBLength); + Assert.AreEqual(2, arrayFLength); + } + +#if ENABLE_SPAN_T + [FlatBuffersTestMethod] + public void FixedLengthArray_GetBytesSpanLengthIsCorrect_ReturnTrue() + { + var builder = new FlatBufferBuilder(1024); + var ints = new int[] { 1, 2 }; + var enumB = TestEnum.A; + var enums = new TestEnum[] { TestEnum.B, TestEnum.C }; + var longs = new long[] { 10L, 20L }; + + var structOffset = NestedStruct.CreateNestedStruct(builder, ints, enumB, enums, longs); + builder.Finish(structOffset.Value); + + var bb = builder.DataBuffer; + var nestedStruct = new NestedStruct(); + nestedStruct.__assign(bb.Length - builder.Offset, bb); + + Span intSpan = nestedStruct.GetABytes(); + Span enumSpan = nestedStruct.GetCBytes(); + Span longSpan = nestedStruct.GetDBytes(); + + Assert.AreEqual(intSpan.Length, NestedStruct.ALength); + Assert.AreEqual(enumSpan.Length, NestedStruct.CLength); + Assert.AreEqual(longSpan.Length, NestedStruct.DLength); + } +#endif + +#if !ENABLE_SPAN_T + [FlatBuffersTestMethod] + public void FixedLengthArray_GetBytesArraySegmentLengthIsCorrect_ReturnTrue() + { + var builder = new FlatBufferBuilder(1024); + var ints = new int[] { 1, 2 }; + var enumB = TestEnum.A; + var enums = new TestEnum[] { TestEnum.B, TestEnum.C }; + var longs = new long[] { 10L, 20L }; + + var structOffset = NestedStruct.CreateNestedStruct(builder, ints, enumB, enums, longs); + builder.Finish(structOffset.Value); + + var buffer = builder.DataBuffer; + var nestedStruct = new NestedStruct(); + nestedStruct.__assign(buffer.Length - builder.Offset, buffer); + + Assert.IsTrue(nestedStruct.GetABytes().HasValue); + Assert.IsTrue(nestedStruct.GetCBytes().HasValue); + Assert.IsTrue(nestedStruct.GetDBytes().HasValue); + + ArraySegment intSegment = nestedStruct.GetABytes().Value; + ArraySegment enumSegment = nestedStruct.GetCBytes().Value; + ArraySegment longSegment = nestedStruct.GetDBytes().Value; + + Assert.AreEqual(intSegment.Count, NestedStruct.ALength * sizeof(int)); + Assert.AreEqual(enumSegment.Count, NestedStruct.CLength * sizeof(sbyte)); + Assert.AreEqual(longSegment.Count, NestedStruct.DLength * sizeof(long)); + } +#endif + +#if ENABLE_SPAN_T + [FlatBuffersTestMethod] + public void FixedLengthArray_GetBytesSpanEquality_ReturnTrue() + { + var builder = new FlatBufferBuilder(1024); + + var floatA = 3.14f; + var intArray = Enumerable.Range(1, 15).ToArray(); + var byteC = (sbyte)42; + var intE = 999; + var longArray = new long[] { 5000L, 6000L }; + + var nestedInts = new int[2, 2] { { 10, 20 }, { 30, 40 } }; + var nestedEnumB = new TestEnum[] { TestEnum.A, TestEnum.B }; + var nestedEnums = new TestEnum[2, 2] { { TestEnum.A, TestEnum.B }, { TestEnum.C, TestEnum.A } }; + var nestedLongs = new long[2, 2] { { 100L, 200L }, { 300L, 400L } }; + + var structOffset = ArrayStruct.CreateArrayStruct(builder, floatA, intArray, byteC, + nestedInts, nestedEnumB, nestedEnums, nestedLongs, intE, longArray); + + ArrayTable.StartArrayTable(builder); + ArrayTable.AddA(builder, structOffset); + var rootTable = ArrayTable.EndArrayTable(builder); + builder.Finish(rootTable.Value); + + var finishedBytes = builder.SizedByteArray(); + ByteBuffer bb = new ByteBuffer(finishedBytes); + ArrayTable arrayTable = ArrayTable.GetRootAsArrayTable(bb); + ArrayStruct arrayStruct = arrayTable.A.Value; + + Assert.AreEqual(byteC, arrayStruct.C); + Assert.AreEqual(intE, arrayStruct.E); + + Assert.IsTrue(arrayStruct.GetBBytes().SequenceEqual(intArray)); + Assert.IsTrue(arrayStruct.GetFBytes().SequenceEqual(longArray)); + + // Test nested struct arrays + for (int i = 0; i < 2; i++) + { + var nestedStruct = arrayStruct.D(i); + + var nestedIntSpan = nestedStruct.GetABytes(); + var expectedNestedInts = new int[] { nestedInts[i, 0], nestedInts[i, 1] }; + Assert.IsTrue(nestedIntSpan.SequenceEqual(expectedNestedInts)); + + Assert.AreEqual(nestedEnumB[i], nestedStruct.B); + + var nestedEnumSpan = nestedStruct.GetCBytes(); + var expectedNestedEnums = new TestEnum[] { nestedEnums[i, 0], nestedEnums[i, 1] }; + Assert.IsTrue(nestedEnumSpan.SequenceEqual(expectedNestedEnums)); + + var nestedLongSpan = nestedStruct.GetDBytes(); + var expectedNestedLongs = new long[] { nestedLongs[i, 0], nestedLongs[i, 1] }; + Assert.IsTrue(nestedLongSpan.SequenceEqual(expectedNestedLongs)); + } + } +#endif + +#if !ENABLE_SPAN_T + [FlatBuffersTestMethod] + public void FixedLengthArray_GetBytesArraySegmentEquality_ReturnTrue() + { + var builder = new FlatBufferBuilder(1024); + + var floatA = 3.14f; + var intArray = Enumerable.Range(1, 15).ToArray(); + var byteC = (sbyte)42; + var intE = 999; + var longArray = new long[] { 5000L, 6000L }; + + var nestedInts = new int[2, 2] { { 10, 20 }, { 30, 40 } }; + var nestedEnumB = new TestEnum[] { TestEnum.A, TestEnum.B }; + var nestedEnums = new TestEnum[2, 2] { { TestEnum.A, TestEnum.B }, { TestEnum.C, TestEnum.A } }; + var nestedLongs = new long[2, 2] { { 100L, 200L }, { 300L, 400L } }; + + var structOffset = ArrayStruct.CreateArrayStruct(builder, floatA, intArray, byteC, + nestedInts, nestedEnumB, nestedEnums, nestedLongs, intE, longArray); + + ArrayTable.StartArrayTable(builder); + ArrayTable.AddA(builder, structOffset); + var rootTable = ArrayTable.EndArrayTable(builder); + builder.Finish(rootTable.Value); + + var finishedBytes = builder.SizedByteArray(); + ByteBuffer bb = new ByteBuffer(finishedBytes); + ArrayTable arrayTable = ArrayTable.GetRootAsArrayTable(bb); + ArrayStruct arrayStruct = arrayTable.A.Value; + + // Test that we can read basic scalars correctly + Assert.AreEqual(byteC, arrayStruct.C); + Assert.AreEqual(intE, arrayStruct.E); + + Assert.IsTrue(arrayStruct.GetBBytes().HasValue); + var intSegment = arrayStruct.GetBBytes().Value; + for (int i = 0, offset = 0; i < intArray.Length; i++, offset += sizeof(int)) + { + var segmentValue = BitConverter.ToInt32(intSegment.Array, + intSegment.Offset + offset); + Assert.AreEqual(intArray[i], segmentValue); + } + + Assert.IsTrue(arrayStruct.GetFBytes().HasValue); + var longSegment = arrayStruct.GetFBytes().Value; + for (int i = 0, offset = 0; i < longArray.Length; i++, offset += sizeof(long)) + { + var segmentValue = BitConverter.ToInt64(longSegment.Array, + longSegment.Offset + offset); + Assert.AreEqual(longArray[i], segmentValue); + } + + // Test nested struct arrays + for (int i = 0; i < 2; i++) + { + var nestedStruct = arrayStruct.D(i); + + Assert.IsTrue(nestedStruct.GetABytes().HasValue); + var nestedIntSegment = nestedStruct.GetABytes().Value; + var expectedNestedInts = new int[] { nestedInts[i, 0], nestedInts[i, 1] }; + for (int ii = 0, offset = 0; ii < NestedStruct.ALength; ii++, offset += sizeof(int)) + { + var segmentValue = BitConverter.ToInt32(nestedIntSegment.Array, + nestedIntSegment.Offset + offset); + Assert.AreEqual(expectedNestedInts[ii], segmentValue); + } + + Assert.AreEqual(nestedEnumB[i], nestedStruct.B); + + Assert.IsTrue(nestedStruct.GetCBytes().HasValue); + var nestedEnumSegment = nestedStruct.GetCBytes().Value; + var expectedNestedEnums = new TestEnum[] { nestedEnums[i, 0], nestedEnums[i, 1] }; + for (int ii = 0, offset = 0; ii < NestedStruct.CLength; ii++, offset += sizeof(sbyte)) + { + var segmentValue = (TestEnum)nestedEnumSegment.Array[nestedEnumSegment.Offset + offset]; + Assert.AreEqual(expectedNestedEnums[ii], segmentValue); + } + + Assert.IsTrue(nestedStruct.GetDBytes().HasValue); + var nestedLongSegment = nestedStruct.GetDBytes().Value; + var expectedNestedLongs = new long[] { nestedLongs[i, 0], nestedLongs[i, 1] }; + for (int ii = 0, offset = 0; ii < NestedStruct.DLength; ii++, offset += sizeof(long)) + { + var segmentValue = BitConverter.ToInt64(nestedLongSegment.Array, + nestedLongSegment.Offset + offset); + Assert.AreEqual(expectedNestedLongs[ii], segmentValue); + } + } + } +#endif + } +} diff --git a/tests/MyGame/Example/ArrayStruct.cs b/tests/MyGame/Example/ArrayStruct.cs index 70a8bddd194..d2280f0325c 100644 --- a/tests/MyGame/Example/ArrayStruct.cs +++ b/tests/MyGame/Example/ArrayStruct.cs @@ -17,16 +17,23 @@ public struct ArrayStruct : IFlatbufferObject public ArrayStruct __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } public float A { get { return __p.bb.GetFloat(__p.bb_pos + 0); } } - public void MutateA(float a) { __p.bb.PutFloat(__p.bb_pos + 0, a); } public int B(int j) { return __p.bb.GetInt(__p.bb_pos + 4 + j * 4); } - public void MutateB(int j, int b) { __p.bb.PutInt(__p.bb_pos + 4 + j * 4, b); } + public const int BLength = 15; +#if ENABLE_SPAN_T + public Span GetBBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast(__p.bb.ToSpan(__p.bb_pos + 4, 60)); } +#else + public ArraySegment? GetBBytes() { return __p.bb.ToArraySegment(__p.bb_pos + 4, 60);} +#endif public sbyte C { get { return __p.bb.GetSbyte(__p.bb_pos + 64); } } - public void MutateC(sbyte c) { __p.bb.PutSbyte(__p.bb_pos + 64, c); } public MyGame.Example.NestedStruct D(int j) { return (new MyGame.Example.NestedStruct()).__assign(__p.bb_pos + 72 + j * 32, __p.bb); } public int E { get { return __p.bb.GetInt(__p.bb_pos + 136); } } - public void MutateE(int e) { __p.bb.PutInt(__p.bb_pos + 136, e); } public long F(int j) { return __p.bb.GetLong(__p.bb_pos + 144 + j * 8); } - public void MutateF(int j, long f) { __p.bb.PutLong(__p.bb_pos + 144 + j * 8, f); } + public const int FLength = 2; +#if ENABLE_SPAN_T + public Span GetFBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast(__p.bb.ToSpan(__p.bb_pos + 144, 16)); } +#else + public ArraySegment? GetFBytes() { return __p.bb.ToArraySegment(__p.bb_pos + 144, 16);} +#endif public static Offset CreateArrayStruct(FlatBufferBuilder builder, float A, int[] B, sbyte C, int[,] d_A, MyGame.Example.TestEnum[] d_B, MyGame.Example.TestEnum[,] d_C, long[,] d_D, int E, long[] F) { builder.Prep(8, 160); diff --git a/tests/MyGame/Example/NestedStruct.cs b/tests/MyGame/Example/NestedStruct.cs index 78173acc613..34c99edf494 100644 --- a/tests/MyGame/Example/NestedStruct.cs +++ b/tests/MyGame/Example/NestedStruct.cs @@ -17,13 +17,27 @@ public struct NestedStruct : IFlatbufferObject public NestedStruct __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } public int A(int j) { return __p.bb.GetInt(__p.bb_pos + 0 + j * 4); } - public void MutateA(int j, int a) { __p.bb.PutInt(__p.bb_pos + 0 + j * 4, a); } + public const int ALength = 2; +#if ENABLE_SPAN_T + public Span GetABytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast(__p.bb.ToSpan(__p.bb_pos + 0, 8)); } +#else + public ArraySegment? GetABytes() { return __p.bb.ToArraySegment(__p.bb_pos + 0, 8);} +#endif public MyGame.Example.TestEnum B { get { return (MyGame.Example.TestEnum)__p.bb.GetSbyte(__p.bb_pos + 8); } } - public void MutateB(MyGame.Example.TestEnum b) { __p.bb.PutSbyte(__p.bb_pos + 8, (sbyte)b); } public MyGame.Example.TestEnum C(int j) { return (MyGame.Example.TestEnum)__p.bb.GetSbyte(__p.bb_pos + 9 + j * 1); } - public void MutateC(int j, MyGame.Example.TestEnum c) { __p.bb.PutSbyte(__p.bb_pos + 9 + j * 1, (sbyte)c); } + public const int CLength = 2; +#if ENABLE_SPAN_T + public Span GetCBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast(__p.bb.ToSpan(__p.bb_pos + 9, 2)); } +#else + public ArraySegment? GetCBytes() { return __p.bb.ToArraySegment(__p.bb_pos + 9, 2);} +#endif public long D(int j) { return __p.bb.GetLong(__p.bb_pos + 16 + j * 8); } - public void MutateD(int j, long d) { __p.bb.PutLong(__p.bb_pos + 16 + j * 8, d); } + public const int DLength = 2; +#if ENABLE_SPAN_T + public Span GetDBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast(__p.bb.ToSpan(__p.bb_pos + 16, 16)); } +#else + public ArraySegment? GetDBytes() { return __p.bb.ToArraySegment(__p.bb_pos + 16, 16);} +#endif public static Offset CreateNestedStruct(FlatBufferBuilder builder, int[] A, MyGame.Example.TestEnum B, MyGame.Example.TestEnum[] C, long[] D) { builder.Prep(8, 32);