Workaround on declaring a unsafe fixed custom struct array?

3.4k Views Asked by At

Is there any workaround for the error CS1663 ("Fixed size buffer type must be one of the following: bool, byte, short, int, long, char, sbyte, ushort, uint, ulong, float or double.")?

I need to declare a unsafe fixed array from another blittable custom type struct but I'm stuck in this compiler error.

Showing some code to elucidate the problem below.

struct s1
{
    byte _b1;
    byte _b2;
}

unsafe struct s2
{
    fixed s1 _s1[5]; // CS1663 here...
}

Note that the two structs are blittable, so the error doesn't make any sense for me.

Anyone have any idea about what I could do?

Thanks.

5

There are 5 best solutions below

0
On BEST ANSWER

Solution for C# 12 (Dotnet 8+)

If you are able to use C# 12 (dotnet 8), there is the System.Runtime.CompilerServices.InlineArray attribute which allows you to create a struct to represent a fixed size array of another struct without any pointer shenanigans.

Take a look:

struct MyData
{
    byte _b1;
    byte _b2;
}

// We want our array to be 5 elements long, so specify a size of 5
[System.Runtime.CompilerServices.InlineArray(5)]
struct MyDataInlineArray
{
   // Note that structs with the InlineArray attribute must contain
   // EXACTLY one member.
   MyData Element;
   // In this example, this struct is now a 5-long inline array of MyData. 
}

// Notice that this doesn't have to be marked unsafe anymore.
struct StructWithInlineArray
{
    MyDataInlineArray myDataArray;
}

You can then index into types with the InlineArray attribute like a normal array:

public static void Main() {
    StructWithInlineArray foo = new();

    foo.myDataArray[0] = new MyData();
    foo.myDataArray[1] = ...;

    // You can also just use it directly
    MyDataInlineArray bar = new();
    bar[0] = ...;
}

You can read more about them here.

0
On

Another workaround is to either use pointer type fields.

public struct Foo
{
    public byte _b1;
    public byte _b2;
}

public unsafe struct Bar
{
    public int Length;
    public unsafe Foo* Data;
}

Alternately, if your intention was to make a single value type object which contains all of its own data, contiguously (ie, so it can be memcpy'd in unmanaged code) -- then the only way to do that is by not using any arrays at all.

public struct Foo
{
    public byte _b1;
    public byte _b2;
}

public struct Bar1
{
    public Foo F1;
}

public struct Bar2
{
    public Foo F1;
    public Foo F2;
}

public struct Bar3
{
    public Foo F1;
    public Foo F2;
    public Foo F3;
}
4
On

It's a restriction of fixed size buffers.

The fixed array can take any of the attributes or modifiers that are allowed for regular struct members. The only restriction is that the array type must be bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float, or double.

You can use only that types but not combination (like struct contains only that types). There is no difference if your type bittable or not. You just can't use it.

Then you can't use your custom struct for the fixed size buffer.

Workaround? Mmmm, yes, may be. You can change your code structure and use something like this:

unsafe struct s2
{
    fixed byte _b1[5]; 
    fixed byte _b2[5]; 
}
0
On
[StructLayout(LayoutKind.Sequential)]
public struct S1
{
    [MarshalAs(UnmanagedType.I1)] public byte _b1; 
    [MarshalAs(UnmanagedType.I1)] public byte _b2; 

    public S1(ushort _ushort)
    {
        this = new Converter(_ushort).s1;
    }
    public ushort USHORT() //for fixed arrays
    {
        return new Converter(this).USHORT;
    }

    [StructLayout(LayoutKind.Explicit)]
    private struct Converter
    {
        [FieldOffset(0)] public S1 s1;
        [FieldOffset(0)] public ushort USHORT;

        public Converter(S1 _s1)
        {
            USHORT = 0;
            s1 = _s1;
        }

        public Converter(ushort _USHORT)
        {
            s1 = default;
            USHORT = _USHORT;
        }
    }
}

public unsafe struct S2
{
    public fixed ushort s1[5];

    public S1 this[int n] {
        get => new S1(s1[n]); //copy!
        set => s1[n] = value.USHORT();
    }
}
0
On

How I would approach your example and similar complexity ones would be to first ask myself if the struct can be reduced to a single primitive type. If it can then I would merely hide the fixed buffer inside the struct via property or indexer access.

In your example you had the following.

struct s1
{
    byte _b1;
    byte _b2;
}

unsafe struct s2
{
    fixed s1 _s1[5]; // CS1663 here...
}

What I might consider if I decided I absolutely needed the structs to line up in a single contiguous block of data is something like this.

[StructLayout(LayoutKind.Explicit, Size = 2)]
struct s1
{   // Field offsets to emulate union style access which makes it
    // simple to get at the raw data in a primitive type format.

    [FieldOffset(0)]ushort _u1;
    [FieldOffset(0)]byte _b1;
    [FieldOffset(1)]byte _b2;

    public s1(ushort data)
    {
        _b1 = 0;
        _b2 = 0;
        _u1 = data;
    }

    public ushort ToUShort()
    {
        return _u1;
    }
}

unsafe struct s2
{
    public const int Size = 5;
    private fixed ushort _s1[Size];

    public s1 this[int index]
    {   // A public indexer that provides the data in a friendlier format.
        get
        {
            if (index < 0 || index >= Size )
                throw new IndexOutOfRangeException();
            return new s1(_s1[index]);
        }
        set
        {
            if (index < 0 || index >= Size)
                throw new IndexOutOfRangeException();
            _s1[index] = value.ToUShort();
        }
    }
}

If this looks like a hack, that's because it kinda is. I wouldn't recommend this as a general solution because it's difficult to maintain but in those rare instances where you're working at this sort of low level and know in advance that the data spec won't be changing, then something like this technique can be viable. But, even in those cases I'd still prefer to encapsulate as much of this low level stuff as is possible to minimize the chances of something going wrong. That said, this does do exactly as asked which is for a workaround to having a fixed sized buffer of custom structs.