Windows
C#
.NET
dll
WindowsForm

【Windows/C#】なるべく丁寧にDllImportを使う


はじめに

C#でWindowsのソフトウェアを開発しているとWindows APIを呼び出すためによく使うDllImport属性。意外と適当に書いても呼び出せたりするけど、なるべく丁寧に書いてあげたくなるのが開発者の心情というもの。と言うわけでDllImportするときの個人的なメモ。


定義はNativeMethodsクラスで

昨今のVisual Studioでは、DllImportの定義は「NativeMethods」「SafeNativeMethods」「UnsafeNativeMethods」という名称のクラス内に定義しないとコード分析でCA1060の警告が出てくる。

クラスの中に定義したクラスでも良いので、こんな感じにする。

public class Test

{
private static class NativeMethods
{
[DllImport("user32.dll")]
internal static extern IntPtr GetForegroundWindow();
}

public IntPtr GetForeground()
{
return NativeMethods.GetForgroundWindow();
}
}

システムに影響を与える可能性があればUnsafeで、そうじゃなければSafeらしいけど、どっちか悩むやつも結構あるので大人しくNativeMethodsで良いような気がする。


BOOL型のやり取りはUnmanagedType.Bool

Windows APIで頻出するBOOL型。書く意味は薄そうだけど、ちゃんと定義してあげる。

    [DllImport("user32.dll")]

internal static extern int ShowCursor(
[MarshalAs(UnmanagedType.Bool)]bool bShow);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsWindow(IntPtr hWnd);

「return:」って、BOOLでしか使ったことがない。


文字列を渡す

Windows APIに文字列を渡すときは、UnmanagedType.LPWStrを付ける。

そして文字列や構造体を渡すときはIn属性を書く。

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]

[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetCurrentDirectory(
[MarshalAs(UnmanagedType.LPWStr), In] string lpPathName);

文字コードを明示しないとトラブルの元。今どきならUnicode前提で良い気がする。


文字列を受け取る

Windows APIから文字列を受け取るときは、StringBuilderクラスを使うのがベターらしい。

文字列や構造体を受け取るときはOut属性を書く。

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]

internal static extern int GetCurrentDirectory(
int nBufferLength,
[MarshalAs(UnmanagedType.LPWStr), Out] StringBuilder lpPathName);

public void Test()
{
var buff = new StringBuilder(255);
NativeMethods.GetCurrentDirectory(buff.Capacity, buff);
}

StringBuilder → stringの変換が発生するのは気になる・・・

一応もし取得する文字数が分かる場合は、stringに直接突っ込むことも出来なくはない模様。

    public void Test()

{
int len = GetCurrentDirectory(0, null);
string buff = new string('\0', len - 1); // NULL文字分は引く
NativeMethods.GetCurrentDirectory(len, buff);
}

これなら直接string型で文字列が受け取れる。果たしてこの実装はアリなのか?


定数はenumにしちゃう

Windows APIだとdefineで定義された定数を渡すことがある。

constで定義しているサンプルをよく見掛けるけど、enumの方がインテリセンスにも出てきて便利だと思う。


setupapi.h

#define DIGCF_DEFAULT           0x00000001  // only valid with DIGCF_DEVICEINTERFACE

#define DIGCF_PRESENT 0x00000002
#define DIGCF_ALLCLASSES 0x00000004
#define DIGCF_PROFILE 0x00000008
#define DIGCF_DEVICEINTERFACE 0x00000010

[Flags]

internal enum DIGCF
{
Default = 0x00000001,
Present = 0x00000002,
Allclasses = 0x00000004,
Profile = 0x00000008,
DeviceInterface = 0x00000010
}


構造体のあれこれ

Windows APIで構造体が出てきたときに、単純にstructで定義して、refで渡すサンプルをよく見掛けるけど、本当にそれで良いのか一考の余地がある。


classで定義するのも1つの手

クラスで定義すると参照型になるので、refもoutも付けずにポインタが渡せる。

構造体のポインタをやり取りするときは、とりあえずUnmanagedType.LPStruct

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

internal class DEV_BROADCAST_DEVICEINTERFACE
{
private readonly int dbcc_size = Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>();
private readonly uint dbcc_devicetype = 0x0005; //DBT_DEVTP_DEVICEINTERFACE;
private readonly int dbcc_reserved = 0;
public Guid dbcc_classguid = Guid.Empty;
public char dbcc_name;
}

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern SafeDevNotifyHandle RegisterDeviceNotification(
IntPtr hRecipient,
[MarshalAs(UnmanagedType.LPStruct), In] DEV_BROADCAST_DEVICEINTERFACE NotificationFilter,
uint Flags);

classなら初期化子が使えるので、メンバ変数に初期値を仕込める。

またrefやoutを付けるとnullが指定できないので、オプションパラメータなどにnullを渡したいときにclassにしておくとnullにできるメリットも。

それにrefやoutが無ければ、引数の中で直接newして渡すこともできる。

    IntPtr hDevNotify = SafeNativeMethods.RegisterDeviceNotification(

hRecipient,
new SafeNativeMethods.DEV_BROADCAST_DEVICEINTERFACE()
{
dbcc_classguid = GUID_DEVINTERFACE_VOLUME
}, 0);


構造体で結果を受け取るならstructでoutもアリ

C# 7.0からoutで呼び出すときに、変数の宣言も同時にできるようになったので、構造体で結果を受け取る系のAPIはoutにした方がスッキリすることも。

    [DllImport("user32.dll")]

[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetWindowRect(IntPtr hWnd, out Rectangle lpRect);

public void Test()
{
NativeMethods.GetWindowRect(len, out Rectangle rect);
}

outの呼び出しと変数の宣言が同時にできると知ったときの感動たるや・・・

ちなみにRECTやPOINTは、.NetのRectangleやPointと互換性があるので、System.Drawingを参照設定しているなら使える。


GUID構造体

たまに出てくるGUID構造体は、System.Guid構造体がそのまま使える。

しかしポインタで渡すときにrefを付けると、readonlyが付けられない問題・・・

    internal static readonly Guid GUID_DEVINTERFACE_DISK

= new Guid(0x53f56307, 0xb6bf, 0x11d0, 0x94, 0xf2,
0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b);

[DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern IntPtr SetupDiGetClassDevs(
ref Guid ClassGuid,
[MarshalAs(UnmanagedType.LPWStr), In] string Enumerator,
IntPtr hwndParent, int Flags);

public void Test()
{
// 読み取り専用フィールドをrefに使えないエラー
NativeMethods.SetupDiGetClassDevs(
ref GUID_DEVINTERFACE_DISK, null,
IntPtr.Zero, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
}

refを付けずにUnmanagedType.LPStructで定義すると、ポインタとして渡してくれる。

    [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]

internal static extern IntPtr SetupDiGetClassDevs(
[MarshalAs(UnmanagedType.LPStruct), In] Guid ClassGuid,
[MarshalAs(UnmanagedType.LPWStr), In] string Enumerator,
IntPtr hwndParent, int Flags);

public void Test()
{
// これならOK
NativeMethods.SetupDiGetClassDevs(
GUID_DEVINTERFACE_DISK, null,
IntPtr.Zero, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
}


構造体の中に文字列そのものが含まれる場合

かなり特殊なケースだけど稀にあるみたい。

こんなやつ。

typedef struct _DEV_BROADCAST_DEVICEINTERFACE_W {

DWORD dbcc_size;
DWORD dbcc_devicetype;
DWORD dbcc_reserved;
GUID dbcc_classguid;
wchar_t dbcc_name[1];
} DEV_BROADCAST_DEVICEINTERFACE_W, *PDEV_BROADCAST_DEVICEINTERFACE_W;

dbcc_name[1]と言いつつ、実際にはメモリ上に文字列が繋がって存在する。

最大文字サイズが分かっている場合は、UnmanagedType.ByValTStrで対応可能。

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

public struct DEV_BROADCAST_DEVICEINTERFACE
{
public int dbcc_size;
private readonly uint dbcc_devicetype = 0x0005; //DBT_DEVTP_DEVICEINTERFACE;
private readonly int dbcc_reserved = 0;
public Guid dbcc_classguid;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
public string dbcc_name;
}

これならMarshal.PtrToStructureなどで受け取り可能。

~~

【追記:2018/12/27】

pinvoke.netに載ってるDEV_BROADCAST_DEVICEINTERFACE構造体の定義に「SizeConst = 255」って書いてあるから使ったけど、そもそもdbcc_nameが最大255文字って根拠がない気がする・・・。

~~

問題は可変長。

SetupDiGetDeviceInterfaceDetail関数でSP_DEVICE_INTERFACE_DETAIL_DATA構造体を受け取りたいときに困った・・・。

C++なら↓みたいな感じで受け取れる。

DWORD reqSize;

// 必要なバッファサイズを取得
SetupDiGetDeviceInterfaceDetail(hDevInfo, &deviceInterfaceData, nullptr, 0, &reqSize, nullptr);
// バッファ確保
auto pDetailData = static_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA>(malloc(reqSize));
// 初期化
memset(pDetailData, 0, reqSize);
pDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// データを取得
SetupDiGetDeviceInterfaceDetail(hDevInfo, &deviceInterfaceData, pDetailData, reqSize, nullptr, nullptr);

色々と悩んだ末、どうにもならないので無理矢理・・・

    [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]

[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetupDiGetDeviceInterfaceDetail(
SafeDevInfoHandle DeviceInfoSet,
[MarshalAs(UnmanagedType.LPStruct), In]SP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
IntPtr DeviceInterfaceDetailData, int DeviceInterfaceDetailDataSize, out int RequiredSize,
[MarshalAs(UnmanagedType.LPStruct), Out]SP_DEVINFO_DATA DeviceInfoData);

public void Test()
{
// デバイスパスを取得するために必要なバッファ数を取得
NativeMethods.SetupDiGetDeviceInterfaceDetail(
hDevInfo, ifData, IntPtr.Zero, 0, out int reqSize, null);
// バッファ確保
IntPtr pDetailData = Marshal.AllocCoTaskMem(reqSize);
// pDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
Marshal.WriteInt32(pDetail, IntPtr.Size == 8 ? 8 : 6);
// データを取得
NativeMethods.SetupDiGetDeviceInterfaceDetail(
hDevInfo, ifData, pDetail, reqSize, out _, null);
// pDetail->DevicePathを取り出し
string devicePath = Marshal.PtrToStringUni(pDetail + sizeof(int));
// 解放
Marshal.FreeCoTaskMem(pDetailData);
}

構造体を定義せずに直接受け取ってしまう。

綺麗ではないけど1番シンプルで分かりやすい。


ハンドルは、HandleRefやSafeHandleで


.Netのフォームなどのハンドルを渡すときはHandleRef

IntPtrでも問題なく動くけど、HandleRefを使う方が丁寧らしい。

    [DllImport("user32.dll")]

[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsWindowVisible(HandleRef hWnd);

public void Form_Load(object sender, EventArgs e)
{
NativeMethods.IsWindowVisible(new HandleRef(this, this.Handle));
}


閉じる必要のあるハンドルはSafeHandleを使う

CloseHandle関数などで閉じる必要のあるハンドルを受け取るときは、IntPtrではなくSafeHandleクラスを使うべき。

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]

internal static extern SafeFileHandle CreateFile(
[MarshalAs(UnmanagedType.LPWStr), In]string lpFileName,
int dwDesiredAccess, FileShare dwShareMode,
IntPtr lpSecurityAttributes, FileMode dwCreationDisposition,
int dwFlagsAndAttributes, IntPtr hTemplateFile);

IDisposableが実装されてるので、usingが使えたり何かと便利。

Microsoft.Win32.SafeHandlesに用意されていないハンドル型でも実装が単純なので自力で用意するのもアリ。

class SafeDevInfoHandle : SafeHandle

{
static class NativeMethods
{
[DllImport("setupapi.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetupDiDestroyDeviceInfoList(
IntPtr DeviceInfoSet);
}

public SafeDevInfoHandle()
: base(IntPtr.Zero, true)
{ }

public override bool IsInvalid => this.handle == IntPtr.Zero;

protected override bool ReleaseHandle()
{
return NativeMethods.SetupDiDestroyDeviceInfoList(this.handle);
}
}


SuppressUnmanagedCodeSecurity属性

DllImportにSuppressUnmanagedCodeSecurity属性を付けると、APIの実行が速くなる。

    [SuppressUnmanagedCodeSecurity]

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetCurrentDirectory(
[MarshalAs(UnmanagedType.LPWStr), In] string lpPathName);

ただしセキュリティを犠牲にしてるので、何も考えずに常に付加するのは危険?


おわりに

過去のソースを漁って思い出した物を書いたけど、他にもあった気がする。また思い出したら追記するかも。