TL;DR
-
StructLayout
属性でSize
を指定した構造体を用意する。 - それを
ref
とSpan
構造体で読み出す。
従来の方法:unsafeステートメントとマーシャラーを使用する場合
P/Invokeで、固定長サイズ配列を含む構造体を受け渡す場合、主に下記の2つの方法を用いると思います。
GetMonitorInfo
とMONITORINFO
を例にすると、下記のような定義です。
ネイティブの定義(UNICODE
ビルド前提で簡略可)
#define CCHDEVICENAME 32
struct MONITORINFOEX
{
DWORD cbSize;
RECT rcMonitor;
RECT rcWork;
DWORD dwFlags;
WCHAR szDevice[CCHDEVICENAME];
};
BOOL WINAPI GetMonitorInfo(
HMONITOR hMonitor,
LPMONITORINFO lpmi);
unsafeステートメントの場合
[StructLayout(LayoutKind.Sequential)]
unsafe struct MONITORINFOEX
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
public fixed char szDevice[32];
};
[DllImport("User32.dll", CharSet = CharSet.Unicode)]
static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
この後、unsafe
コンテキストでMONITORINFOEX.szDevice
から文字列などにコピーする必要があります。
マーシャリングする場合
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct MONITORINFOEX
{
// ...省略...
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public char[] szDevice;
};
[DllImport("User32.dll", CharSet = CharSet.Unicode)]
static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
※MONITORINFOEX.szDevice
はstring/ByValTStr
にしたほうが扱いやすいですが、対比のためchar[]/ByValArray
にしています。
問題点
unsafeを使った方がヒープにオブジェクトを作成しない分効率がいいんですが、呼び出し元でもunsafeコンテキストが必要になるので、取り扱いづらいです。
(まぁP/Invokeを使う時点でパフォーマンスを気にしないほうがいいんですが。)
C#7.2以降で使用できる方法
StructLayout
属性でSize
を指定した構造体を用意する。
ここからが本題です。
昔からですが、StructLayout
属性にはSize
フィールドがあり、構造体のサイズを設定出来ます。
[StructLayout(LayoutKind.Sequential, Size = 32 * sizeof(char), CharSet = CharSet.Unicode)]
public struct FixedLengthCharArray32
{
public char First;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct MONITORINFOEX
{
// ...省略...
FixedLengthCharArray32 szDevice;
};
ただサイズを設定できるだけで、中身を参照しようとするとunsafe
ステートメントが必要なので、この構造体を使う意義がありませんでした。
ref
とSpan
構造体で読み出す
そこでref
とSpan
構造体です。
フィールドの参照からSpan
構造体を構築すれば、後はToString
なりインデクサーでアクセス出来ます。
unsafeコンテキストも要らないので扱いやすいです。
var monitorInfo = new MONITORINFOEX { cbSize = Unsafe.SizeOf<MONITORINFOEX>() };
GetMonitorInfo(hMonitor, ref monitorInfo);
var s = MemoryMarshal.CreateReadOnlySpan(ref monitorInfo.szDevice.First, Marshal.SizeOf<FixedLengthCharArray32>()).ToString();
残念ながら.Net Standard 2.0(.NET Framework)だとMemoryMarshal.CreateReadOnlySpan
が無いので、相当するメソッドをunsafeコンテキストで実装する(もしくはライブラリを使用する等の)必要があります。
一般化
配列の型とサイズ毎に構造体を用意する必要があり定型コードが多いので、自動生成などをしたほうがいいでしょう。
T4テンプレート等で作成してみたバージョンはこちら。