LoginSignup
199
205

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

Last updated at Posted at 2018-12-26

はじめに

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 = NativeMethods.GetCurrentDirectory(0, null);
		string buff = new string('\0', len - 1);	// NULL文字分は引く
		NativeMethods.GetCurrentDirectory(len, buff);
	}

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

Span<T>を使う(2023/8/15 追記)

Span<T>が登場した今ならStringBuilderを使う必要がなくなり少し無駄が省けるように。
前者のStringBuilderを使用するコードをSpan<T>に置き換えてみた。(ついでに可変長に)

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetCurrentDirectory(int nBufferLength, IntPtr lpBuffer);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetCurrentDirectory(int nBufferLength, ref char lpBuffer);

public static void Test()
{
	int len = NativeMethods.GetCurrentDirectory(0, IntPtr.Zero);
	Span<char> buff = stackalloc char[len];
	len = NativeMethods.GetCurrentDirectory(len, ref MemoryMarshal.GetReference(buff));
	string curdir = buff[..len].ToString();
}

引数の型をref charにしてしまうとNULLが渡せないから、stackallocで取得したSpan<char>からIntPtrを取得する方法とか無いのかなあ・・・(見つけられず。。)

GCHandle(2023/8/15 追記)

強引にstringに直接突っ込む後者のコード、GCHandleを使えば、もう少し安全に実現できる方法を思い出した。

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetCurrentDirectory(int nBufferLength, IntPtr lpBuffer);

public static void Test()
{
	int len = NativeMethods.GetCurrentDirectory(0, IntPtr.Zero);
	string buff = new string('\0', len - 1);    // NULL文字分は引く
	var gch = GCHandle.Alloc(buff, GCHandleType.Pinned);
	NativeMethods.GetCurrentDirectory(len, gch.AddrOfPinnedObject());
	gch.Free();
}

良い方法かと問われると微妙だけど、オブジェクトのアドレスが固定されていることが保証されていて、以前のコードより断然マシなはず。

Span<T>を使う(邪道編)(2023/8/15 追記)

stringから取得したReadOnlySpan<char>に強引に突っ込むこともできそう・・・

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetCurrentDirectory(int nBufferLength, IntPtr lpBuffer);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetCurrentDirectory(int nBufferLength, ref char lpBuffer);

public static void Test()
{
	int len = NativeMethods.GetCurrentDirectory(0, IntPtr.Zero);
	string str = new string('\0', len - 1);		// NULL文字分は引く
	NativeMethods.GetCurrentDirectory(len, ref MemoryMarshal.GetReference(str.AsSpan()));
}

一応動いた。しかしReadOnlySpan<char>の意味が・・・

定数は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);
	}

~~
【追記:2019/11/6】
コメントにてC#7.2以降では、in修飾子が使えるとの情報をいただきました。
in パラメーター修飾子 (C# リファレンス)

	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(
		in Guid ClassGuid,
		[MarshalAs(UnmanagedType.LPWStr), In] string Enumerator,
		IntPtr hwndParent, int Flags);

	public void Test()
	{
		NativeMethods.SetupDiGetClassDevs(
			GUID_DEVINTERFACE_DISK, null,
			IntPtr.Zero, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
	}

inのときはref/outみたいに呼び出し側に修飾子を書く必要なし?
まだまだ知らない機能がたくさん・・・
~~

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

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

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);

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

追記:2023/8/15

この属性は、.NET Core には影響しません。

.NET Coreでは付けても意味が無いらしい。
VS2022上でSuppressUnmanagedCodeSecurityAttributeクラスの定義を見ても

 // Has no effect in .NET Core
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = true, Inherited = false)]
public sealed class SuppressUnmanagedCodeSecurityAttribute : Attribute

とコメントで書かれていた。

DefaultDllImportSearchPathsは付ける必要あるのだろうか?(2023/8/22 追記)

随分と前にコメントで

メソッドにDefaultDllImportSearchPathsを付けてDLLを読み込む場所を制限したほうが、より丁寧になる気がします。

と戴いてたり、属性の存在は知ってたけど実際に付けてるコードを1度も見たことないし、「本当にいるのか?」と思いつつ放置してたけど、もう少しちゃんと調べてみた。

・・・と言っても、DefaultDllImportSearchPathsを付加しなかったことによるプリロード攻撃がどうこうみたいな具体的な情報も見つけられず、結局よく分からず。。

と言うわけで少し実験。
↓こんな感じで実装したkernel32.dllを作ってみて、

extern "C" __declspec(dllexport) int __stdcall GetCurrentDirectoryW(unsigned long nBufferLength, wchar_t* lpBuffer)
{
	return 999;
}

.NET側のアプリケーションと同じフォルダに配置した上で、

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetCurrentDirectory(int nBufferLength, IntPtr lpBuffer);

とか

[DllImport("kernel32.dll", CharSet = CharSet.Unicode), DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static extern int GetCurrentDirectory(int nBufferLength, IntPtr lpBuffer);

とか

[DllImport("kernel32.dll", CharSet = CharSet.Unicode), DefaultDllImportSearchPaths(DllImportSearchPath.ApplicationDirectory)]
internal static extern int GetCurrentDirectory(int nBufferLength, IntPtr lpBuffer);

変えて呼び出してみたけど、どれも標準のWinAPIが実行されて、作ったDLLは呼ばれなかった。

[DllImport("T:\\WpfApp1\\WpfApp1\\bin\\Debug\\net6.0-windows\\kernel32.dll", CharSet = CharSet.Unicode), DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static extern int GetCurrentDirectory(int nBufferLength, IntPtr lpBuffer);

いっそフルパスを指定してみたら、作ったDLLが呼び出されて999が返ってきたけど、「それはそう」って感じ。

挙動から察するに、標準的なWinAPIのDLLは、.NETの内部でSystem32のDLLが早々にロードされていて、ユーザコード側でDllImportしてもそれが使われるだけだから、DefaultDllImportSearchPathsを付けても特に意味はなさそう。

.NET自体のソースコードで検索してみても、ごく一部で付加してるコードもあるけど、大部分では特に付加されてなかった。

https://source.dot.net/#dotnet/Installer/Windows/Security/NativeMethods.cs
Crypt32.dllは、起動時には読み込まれてないから付加してる?

つまりkernel32.dllとかuser32.dllよくあるDLLを使うだけならいちいち付けなくても良いけど、ちょっと珍しいDLLを使うときはつけた方が無難って感じかなー?
全てにこの定義を付けるのも悪くないと思うけど、それは果たして「丁寧」なのか「冗長」なのか・・・

関数ポインタを渡したい(2021/1/26 追記)

WinAPIに、C#のメソッドを関数ポインタとして渡すときについて書かれてないことに今さら気付いた。
例えば、キー入力のグローバルフックをしたいときに、コールバック関数としてC#のメソッドを登録したいときとか。

まず渡したい関数の定義を、delegateとしてNativeMethodsクラスの中に用意。
ついでにコールバックで渡される構造体も。

[StructLayout(LayoutKind.Sequential)]
internal class KBDLLHOOKSTRUCT
{
	public int vkCode;
	public int scanCode;
	public int flags;
	public int time;
	public int dwExtraInfo;
}

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate IntPtr HOOKPROC(
	int code,
	IntPtr wParam,
	[MarshalAs(UnmanagedType.LPStruct), In] KBDLLHOOKSTRUCT lParam);

注意すべきは、C#は標準の呼び出し規約がstdcallだけど、C/C++の呼び出し規約はcdeclのため、そのまま渡してしまうと合わないので、cdeclを指定すること。
構造体のポインタが渡されるときは、このようにしておくとコールバック関数でも直に受け取れて便利。

次にDllImportの定義。

internal const int WH_KEYBOARD_LL = 13;

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern SafeHookHandle SetWindowsHookEx(
	int idHook,
	[MarshalAs(UnmanagedType.FunctionPtr)] HOOKPROC lpfn,
	IntPtr hmod,
	int dwThreadid);

[DllImport("user32.dll")]
internal static extern IntPtr CallNextHookEx(
	SafeHookHandle hhk, int code, IntPtr wParam,
	[MarshalAs(UnmanagedType.LPStruct), In] KBDLLHOOKSTRUCT lParam);

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

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern IntPtr GetModuleHandle(
	[MarshalAs(UnmanagedType.LPWStr), In] string lpModuleName);

UnmanagedType.FunctionPtrで、関数ポインタであることを指定してあげる。
フックのハンドル(HHOOK)は、↑で書いたSafeHandleを定義してあげると、より安全。
↑で「定数はenumにしちゃう」と書いたけど、使うのが1つに限られてるならconstの方がシンプルで良い。

そしてAPIの呼び出し。

private NativeMethods.SafeHookHandle hhk;
private NativeMethods.HOOKPROC hookProc;

public void StartHook()
{
	if (this.hhk == null)
	{
		this.hookProc = this.KeyboardProc;
		this.hhk = NativeMethods.SetWindowsHookEx(
			NativeMethods.WH_KEYBOARD_LL, this.hookProc,
			NativeMethods.GetModuleHandle(null), 0);
	}
}

private IntPtr KeyboardProc(int code, IntPtr wParam, NativeMethods.KBDLLHOOKSTRUCT lParam)
{
	System.Diagnostics.Debug.WriteLine($"HOOK code={code} wParam={wParam} vkCode={lParam.vkCode}");
	return NativeMethods.CallNextHookEx(this.hhk, code, wParam, lParam);
}

HINSTANCEの値は、WinAPIを使わずに、

Marshal.GetHINSTANCE(Assembly.GetEntryAssembly().GetModules()[0]);

でも取得できるけど、いちいち配列を取ったり回りくどくて無駄だし、そもそも既に他でWinAPI使っちゃってるし、素直にGetModuleHandle(NULL)で取っちゃった方がスッキリして良いと思う。

あと、ここ要注意!
引数の型がdelegateだから、ついつい↓みたいにメソッドを直接渡したくなる。

this.hhk = NativeMethods.SetWindowsHookEx(
	NativeMethods.WH_KEYBOARD_LL, this.KeyboardProc, // ←これ
	NativeMethods.GetModuleHandle(null), 0);

しかし、これだと渡したdelegateが、C#側で誰も保持してないので自然消滅する・・・
つまり、フック開始してから数秒間は上手く動くのに、少し時間が経つとコールバック関数が消失してしまい、System.ExecutionEngineExceptionという意味の分からない例外を吐いて落ちるようになる。
必ず、渡した関数ポインタが呼び出される間は生存するところで、delegateを保持すること!
ハマりやすいので、お気を付けて。(ハマった)

COMインターフェースを呼び出す(2023/8/22)

DllImportとはちょっと違うけど、COMインターフェースで提供されてるWindowsの機能をC#から呼び出すやつ。
使う機会があれば調べようかと思いつつ何年も使わなかったけど、最近ようやく使ったので調べた。(面倒だから基本はC++で済ませちゃう派)

インターフェース側の定義

C#のinterfaceとして定義を書いて、属性にComInterfaceType.InterfaceIsIUnknownであることと、
COMインターフェースが定義された元のヘッダファイルを参照してGUID(IID_IMMDeviceEnumeratorみたいなやつ)を指定。
あとは、中身もヘッダファイルを見ながら上から順にゴリゴリと移植。
MarshalAsとかマーシャリング周りの指定は、DllImportと一緒)

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
internal interface IMMDeviceEnumerator
{
	uint EnumAudioEndpoints(
		EDataFlow dataFlow,
		DeviceState dwStateMask,
		[MarshalAs(UnmanagedType.Interface)] out IMMDeviceCollection ppDevices);
}

関連するインターフェースが多いと山ほど定義を書かないといけなくて面倒だけど、これまでに書いたDllImportの知識があれば、基本的に迷うことがないのは救い。

CoCreateInstanceするための定義

C++で言うところのCoCreateInstanceをC#でやる。
ComImport属性とヘッダファイルに載ってるそのCOMのGUID(CLSID_MMDeviceEnumeratorみたいなやつ)を指定するだけの中身のないclassを作る。

[ComImport]
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
internal class MMDeviceEnumerator
{
}

あとは、そのクラスをnewするだけ。

// IMMDeviceEnumeratorを取得
if (new MMDeviceEnumerator() is not IMMDeviceEnumerator mmDevEnum)
	throw new NotSupportedException("IMMDeviceEnumeratorが取得できません。");

定義を書くのがひたすら面倒なだけで、COMインターフェースをスムーズに呼び出せるのは便利。

HRESULTの戻り値を受け取る

標準では、COMを呼び出した戻り値がE_~の場合は、自動的にCOMExceptionの例外として扱ってくれる。
特定のエラーコードを識別したりしたいときは困るので、そのまま受け取りたいときはメソッドの定義にPreserveSig属性を付ける。

[PreserveSig]
uint EnumAdapters(uint Adapter, [MarshalAs(UnmanagedType.Interface)] out IDXGIAdapter ppAdapter);

// たとえば
for (int i = 0; ; ++i)
{
	uint hr = dxgiFactory.EnumAdapters(i, out var adapter);
	if (hr == DXGI_ERROR_NOT_FOUND) break;
	Marshal.ThrowExceptionForHR(hr);
	// ...
}

なんでもかんでも全てのメソッドにPreserveSigを付けちゃうみたいなソースも見かけたけど、ちょっとナンセンスな気がする。

CustomMarshalerを活用(2023/8/22)

ずっと使い道が分からなかったけど、文字列を呼び出した先で確保されたメモリで受け取って、呼び出し元側で解放したい場合に、カスタムすればstring型で受け取りつつ内部で解放できた。

#nullable disable
internal sealed class StringFromTaskMemMarshaler : ICustomMarshaler
{
	private static readonly StringFromTaskMemMarshaler instance = new ();
	internal static ICustomMarshaler GetInstance(string cookie) => instance;

	public IntPtr MarshalManagedToNative(object ManagedObj) => throw new NotImplementedException();
	public void CleanUpManagedData(object ManagedObj) => throw new NotImplementedException();

	public object MarshalNativeToManaged(IntPtr pNativeData)
	{
		if (pNativeData == IntPtr.Zero)
			return null;

		return Marshal.PtrToStringUni(pNativeData);
	}

	public void CleanUpNativeData(IntPtr pNativeData)
	{
		if (pNativeData != IntPtr.Zero)
			Marshal.FreeCoTaskMem(pNativeData);
	}

	public int GetNativeDataSize() => IntPtr.Size;
}

// たとえば
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("AE2DE0E4-5BCA-4F2D-AA46-5D13F8FDB3A9")]
internal interface IPart
{
	uint GetName(
		[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(StringFromTaskMemMarshaler))]
		out string ppwstrName);
}

これで.NETのstringにコピーした上で、FreeCoTaskMemが呼び出されて解放してくれる。
ここまで書いて、さらに調べてると、LPWStrなら勝手にFreeCoTaskMemを呼び出してくれるという情報が・・・

~されるそうです。

みたいな書き方だったり、情報元の記載が無かったり正確な情報かどうか分からない・・・
仮に解放されるとして、どうやって識別しているんだろう・・・
調べてもハッキリしなかったから、とりあえず確実に解放される ICustomMarshaler が無難かな・・・?

他にもICustomMarshalerを活用すれば、上に挙げたSetupDiGetDeviceInterfaceDetailみたいな特殊なAPIでもスッキリ書けたりしそう。

おわりに

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

【追記:2023/8/22】
随分と長い間、たくさんの方に見て頂けているようで・・・少しでもお役に立てていれば幸いです。
あまり積極的に発信したい性格でもなく気まぐれ程度なので長らく放置してましたが、ふと思い立って気まぐれで色々と追記してみました。

199
205
4

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
199
205