Skip to content

[C#] Add GetBytes methods for fixed arrays #8633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/idl_gen_csharp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte, " +
GenTypeBasic(field.value.type.VectorType()) + ">(__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<byte>? 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);
Expand Down
249 changes: 249 additions & 0 deletions tests/FlatBuffers.Test/FlatBuffersFixedLengthArrayTests.cs
Original file line number Diff line number Diff line change
@@ -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<int> intSpan = nestedStruct.GetABytes();
Span<TestEnum> enumSpan = nestedStruct.GetCBytes();
Span<long> 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<byte> intSegment = nestedStruct.GetABytes().Value;
ArraySegment<byte> enumSegment = nestedStruct.GetCBytes().Value;
ArraySegment<byte> 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
}
}
17 changes: 12 additions & 5 deletions tests/MyGame/Example/ArrayStruct.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> GetBBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast<byte, int>(__p.bb.ToSpan(__p.bb_pos + 4, 60)); }
#else
public ArraySegment<byte>? 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<long> GetFBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast<byte, long>(__p.bb.ToSpan(__p.bb_pos + 144, 16)); }
#else
public ArraySegment<byte>? GetFBytes() { return __p.bb.ToArraySegment(__p.bb_pos + 144, 16);}
#endif

public static Offset<MyGame.Example.ArrayStruct> 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);
Expand Down
22 changes: 18 additions & 4 deletions tests/MyGame/Example/NestedStruct.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> GetABytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast<byte, int>(__p.bb.ToSpan(__p.bb_pos + 0, 8)); }
#else
public ArraySegment<byte>? 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<MyGame.Example.TestEnum> GetCBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast<byte, MyGame.Example.TestEnum>(__p.bb.ToSpan(__p.bb_pos + 9, 2)); }
#else
public ArraySegment<byte>? 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<long> GetDBytes() { return System.Runtime.InteropServices.MemoryMarshal.Cast<byte, long>(__p.bb.ToSpan(__p.bb_pos + 16, 16)); }
#else
public ArraySegment<byte>? GetDBytes() { return __p.bb.ToArraySegment(__p.bb_pos + 16, 16);}
#endif

public static Offset<MyGame.Example.NestedStruct> CreateNestedStruct(FlatBufferBuilder builder, int[] A, MyGame.Example.TestEnum B, MyGame.Example.TestEnum[] C, long[] D) {
builder.Prep(8, 32);
Expand Down