LoginSignup
4
3

More than 3 years have passed since last update.

[C#7.2] unsafeステートメントにもマーシャラーにも頼らないで、固定サイズ配列を含む構造体をP/Invokeで受け渡しする方法

Last updated at Posted at 2019-12-08

TL;DR

  • StructLayout属性でSizeを指定した構造体を用意する。
  • それをrefSpan構造体で読み出す。

従来の方法:unsafeステートメントとマーシャラーを使用する場合

P/Invokeで、固定長サイズ配列を含む構造体を受け渡す場合、主に下記の2つの方法を用いると思います。
GetMonitorInfoMONITORINFOを例にすると、下記のような定義です。

ネイティブの定義(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.szDevicestring/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ステートメントが必要なので、この構造体を使う意義がありませんでした。

refSpan構造体で読み出す

そこでrefSpan構造体です。
フィールドの参照から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テンプレート等で作成してみたバージョンはこちら。

4
3
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
4
3