11
7

[C#] 固定長配列を持つ構造体の取り扱い方

Last updated at Posted at 2022-03-25

C++のDLLやプログラムと構造体をやり取りする際、構造体が固定長配列を持っている場合があります。
以前、固定長配列の対処が判らず困って試行錯誤した記憶があるので、備忘録としてまとめておきます。

1. マーシャリングで変換する

構造体のフィールドにMarshalAs属性でデータ型を指定すると、P/InvokeでDLLの関数を呼び出す際に、指定した型に自動的にデータ変換を行って受け渡ししてくれます。DLLの関数を呼ぶだけであれば、通常これで十分だと思います。unsafe不要で、C#のマネージド配列や文字列に変換してくれるので扱いやすいです。

欠点としては、マーシャリングで構造体を変換・コピーするための処理時間が発生するのと、マネージド配列は参照型なので、変換後の構造体はC++での構造体とサイズ・データ表現が異なり、メモリ上の互換性はありません。
struct_1.png
(参考URL)

(サンプル)

using System;
using System.Runtime.InteropServices;

class Program
{
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    public struct COORD
    {
        short X;
        short Y;
    }

    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    struct SMALL_RECT
    {
        short Left;
        short Top;
        short Right;
        short Bottom;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct CONSOLE_SCREEN_BUFFER_INFOEX
    {
        public int cbSize;
        public COORD dwSize;
        public COORD dwCursorPosition;
        public short wAttributes;
        public SMALL_RECT srWindow;
        public COORD dwMaximumWindowSize;
        public short wPopupAttributes;
        public bool bFullscreenSupported;

        [MarshalAs(UnmanagedType.ByValArray, SizeConst=16)]
        public int[] ColorTable;
    }

    const int STD_OUTPUT_HANDLE = -11;

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool GetConsoleScreenBufferInfoEx(
        IntPtr hConsoleOutput,
        ref CONSOLE_SCREEN_BUFFER_INFOEX ConsoleScreenBufferInfo
    );

    static void Main(string[] args)
    {
        var screenBuffer = new CONSOLE_SCREEN_BUFFER_INFOEX()
        {
            cbSize = Marshal.SizeOf(typeof(CONSOLE_SCREEN_BUFFER_INFOEX)) 
        };
        var result = GetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), ref screenBuffer);

        for(var i = 0; i < screenBuffer.ColorTable.Length; i++)
        {
            Console.WriteLine($"ColorTable[{i}] = 0x{screenBuffer.ColorTable[i]:X8}");
        }

        Console.ReadKey();
    }
}

2. fixedを使用する(要unsafe)

fixedキーワードを使用すると、真に固定サイズを確保したフィールドを持つ構造体を作成出来ます。C++プログラムと共有メモリで構造体をやり取りするなど、メモリ上の構造体のレイアウトが重要なケースでは、こちらを使用します。その場合、他のフィールドもBlittable型のみで構成してください。意外なところでは、bool、charはBlittable型ではないので注意してください。
(2023/09/11 訂正)
[StructLayout]で CharSet = CharSet.Unicode が指定されている場合、charは Blittable型として扱われます。

欠点としては、固定長配列のフィールドにアクセスする際は生ポインタによるアクセスになるのでunsafeが必要になり、使用時の手間が若干増えます。
struct_2.png
(参考URL)

(サンプル)

using System;
using System.Runtime.InteropServices;

class Program
{
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    public struct COORD
    {
        short X;
        short Y;
    }

    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    struct SMALL_RECT
    {
        short Left;
        short Top;
        short Right;
        short Bottom;
    }

    // WinAPIのBOOL(32bit)互換の構造体
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    struct BOOL
    {
        private int _Value;

        public BOOL(bool value) => _Value = value ? 1 : 0;
        public bool Value => _Value != 0;
        public static implicit operator BOOL(bool b) => new BOOL(b);
        public static implicit operator bool(BOOL b) => b.Value;
        public override string ToString() => ((bool)this).ToString();
    }

    [StructLayout(LayoutKind.Sequential)]
    struct CONSOLE_SCREEN_BUFFER_INFOEX
    {
        public int cbSize;
        public COORD dwSize;
        public COORD dwCursorPosition;
        public short wAttributes;
        public SMALL_RECT srWindow;
        public COORD dwMaximumWindowSize;
        public short wPopupAttributes;
        public BOOL bFullscreenSupported;

        public unsafe fixed int ColorTable[16];
    }

    const int STD_OUTPUT_HANDLE = -11;

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool GetConsoleScreenBufferInfoEx(
        IntPtr hConsoleOutput,
        ref CONSOLE_SCREEN_BUFFER_INFOEX ConsoleScreenBufferInfo
    );

    static void Main(string[] args)
    {
        var screenBuffer = new CONSOLE_SCREEN_BUFFER_INFOEX()
        {
            cbSize = Marshal.SizeOf(typeof(CONSOLE_SCREEN_BUFFER_INFOEX)) 
        };
        var result = GetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), ref screenBuffer);

        unsafe
        {
            // 直接固定長配列の長さは取得出来ないので、定数でも定義するしかなさそう
            for (var i = 0; i < 16; i++)
            {
                Console.WriteLine($"ColorTable[{i}] = 0x{screenBuffer.ColorTable[i]:X8}");
            }
        }

        Console.ReadKey();
    }
}

3. 固定長配列用にダミーのフィールドを詰め込む

fixedは使いたくない、unsafeを避けたいけど、固定長配列を持つ構造体を作成したい場合、配列の長さ分だけ同じ型のフィールドを詰め込んだり、StructLayoutでサイズ指定した構造体を詰め込む事で代替手段にする事は可能です。前者は、固定長配列が長くない場合は使えなくもないですが、100とか200になるとさすがにゾッとするので、おすすめしません。(T4やSourceGeneratorで生成するという手もありますが)
このやり方でも、2と同様にC++の構造体とメモリ上での互換性を持たせる事が出来ます。

(サンプル)

using System;
using System.Runtime.InteropServices;

class Program
{
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    public struct COORD
    {
        short X;
        short Y;
    }

    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    struct SMALL_RECT
    {
        short Left;
        short Top;
        short Right;
        short Bottom;
    }

    // WinAPIのBOOL(32bit)互換の構造体
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    struct BOOL
    {
        private int _Value;

        public BOOL(bool value) => _Value = value ? 1 : 0;
        public bool Value => _Value != 0;
        public static implicit operator BOOL(bool b) => new BOOL(b);
        public static implicit operator bool(BOOL b) => b.Value;
        public override string ToString() => ((bool)this).ToString();
    }

    [StructLayout(LayoutKind.Sequential)]
    struct CONSOLE_SCREEN_BUFFER_INFOEX
    {
        public int cbSize;
        public COORD dwSize;
        public COORD dwCursorPosition;
        public short wAttributes;
        public SMALL_RECT srWindow;
        public COORD dwMaximumWindowSize;
        public short wPopupAttributes;
        public BOOL bFullscreenSupported;

        private int ColorTable0;
        private int ColorTable1;
        private int ColorTable2;
        private int ColorTable3;
        private int ColorTable4;
        private int ColorTable5;
        private int ColorTable6;
        private int ColorTable7;
        private int ColorTable8;
        private int ColorTable9;
        private int ColorTable10;
        private int ColorTable11;
        private int ColorTable12;
        private int ColorTable13;
        private int ColorTable14;
        private int ColorTable15;

        //アクセスしやすいようにSpan<T>で取得出来るようにする
        //※.NET FrameworkではMemoryMarshal.CreateSpanが使用出来ないので不可
        public Span<int> ColorTable => MemoryMarshal.CreateSpan(ref ColorTable0, 16);

        //StructLayoutでサイズ指定した構造体を詰め込む例
        //[StructLayout(LayoutKind.Sequential, Size = 16 * sizeof(int))]
        //struct ColorTableArray { }
        //
        //private ColorTableArray _ColorTable;
        //
        //public Span<int> ColorTable => MemoryMarshal.CreateSpan(
        //    ref Unsafe.As<ColorTableArray, int>(ref _ColorTable),
        //    16);
    }

    const int STD_OUTPUT_HANDLE = -11;

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetStdHandle(int nStdHandle);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool GetConsoleScreenBufferInfoEx(
        IntPtr hConsoleOutput,
        ref CONSOLE_SCREEN_BUFFER_INFOEX ConsoleScreenBufferInfo
    );

    static void Main(string[] args)
    {
        var screenBuffer = new CONSOLE_SCREEN_BUFFER_INFOEX()
        {
            cbSize = Marshal.SizeOf(typeof(CONSOLE_SCREEN_BUFFER_INFOEX)) 
        };
        var result = GetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), ref screenBuffer);

        var spanColorTable = screenBuffer.ColorTable;
        for (var i = 0; i < spanColorTable.Length; i++)
        {            
            Console.WriteLine($"ColorTable[{i}] = 0x{spanColorTable[i]:X8}");
        }

        Console.ReadKey();
    }
}
11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7