C#のP/Invoke支援パッケージ

研究が炎上して出遅れました。C# その2 Advent Calendar 2019 - Qiitaの何日目かの記事です。

P/Invokeとは

ご存じない方は、まずこちらをお読みください。

プラットフォーム呼び出し (P/Invoke) - Microsoft Docs

要するに、C#からC言語のDLLを呼び出すための機構です。(実際にはCに限りませんが、深入りはしません)。DllImportという属性を付けたC#の関数を定義してあげればあとは普通のC#の関数のように使うだけです。既存言語の中でも比較的手軽で強力な連携機能を持っていると思います。

P/Invokeの重要な用途として独自のDLLの呼び出しの他に、WindowsプログラミングのためのWin32APIを呼び出しが挙げられます。.NET Frameworkや.NET Coreの標準ライブラリはWindowsの深いところを触るためのクラスなどが用意されていないのを、P/Invokeで補うことになります。

めんどくさい関数定義

P/Invoke用の関数定義は手作業で指定する必要があります。例えば、MessageBox関数を呼び出す関数定義は以下のようになります。

[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

また、最後のoptionsのフラグも独自に定義してあげないと不便でしょう。

[Flags]
public enum MessageBoxOptions
{
     OkOnly         = 0x000000,
     OkCancel       = 0x000001,
     AbortRetryIgnore   = 0x000002,
     YesNoCancel    = 0x000003,
     YesNo          = 0x000004,
     RetryCancel    = 0x000005,
     CancelTryContinue  = 0x000006,
     IconHand       = 0x000010,
     IconQuestion       = 0x000020,
     IconExclamation    = 0x000030,
     IconAsterisk       = 0x000040,
     UserIcon       = 0x000080,
     IconWarning    = IconExclamation,
     IconError      = IconHand,
     IconInformation    = IconAsterisk,
     /*以下略*/
}

ドキュメントを参照すればこのような定義を書くことができるのですが、これを一つ一つ書いていくのは地味に辛い作業です。

pinvoke.net

まず参考にしたいのはpinvoke.netです。かなりの数のWin32APIの定義があり、コピペで使えます。「MessageBox pinvoke」とかで検索すると上に出てくるので目にしたことがある方も多いでしょう。

pinvoke.net: the interop wiki!

しかし、数が増えてくるとコピペすら面倒になってきます。しかも、pinvoke.netは多くの人の協力のよって作られていることもあり地味に間違っていたり、統一が取れていない場所があります。一応Visual Studioの拡張機能があったりしますが、あんまり便利ではないような。

Nugetパッケージ

今回紹介したかったのはこれ。Nuget(.NETのパッケージマネージャ)にDllImportを片っ端からまとめたパッケージが存在します。おそらくまともに使えるのは以下の2つです。

どちらもnugetで導入するだけで呼び出せます。ただし、DLLごとぐらいにパッケージが分割されており、PInvoke.User32.EnumWindowsのように名前空間やクラスも分割されています。

共通のデメリットとして、DllImportの定義だけのDLLが生成されてしまいます。用途によっては見栄えが悪いです。.NET CoreのシングルバイナリやILMergeの使用を検討してください。

2つのどちらが良いのか

星の数で見るならAArnott/pinvokeが優勢ですが、正直、甲乙つけがたいです。

Win32APIは領域が広いので、こちらのほうが便利な補助関数がある、こちらは構造体定義がユースケースに合わないといった場合が存在します。自分が使う範囲のAPI定義がどちらが優れているかをまず確認するのが吉です。特にUndocumentedなAPIなど、そもそもDllImportが存在しないみたいなことはよくあります。

また、型の取り扱いなどに方針の差も現れています。個人的に見た傾向をざっくりまとめます。

dahall/Vanara

メリット

C#としての利便性重視。SendMessageにジェネリックの補助メソッドを追加したり、ウィンドウハンドルにHWND構造体、CreateWindowExの戻り値にSafeHWNDなど新たな型を導入していたりします。

型があることで型検査や構造体に操作用のメソッドが生えていたりと利便性が高いです。また、SafeHandleの扱いなど分かっている人が使えば手間が省けるのでカジュアルな利用時におすすめです。

例→ GetWindow

また、単にWin32APIの扱いやマーシャリングに関する処理の参考にもなります。

デメリット

使い方が難しいとは思いませんが、いろいろな型を使うのでDllImport集として使おうとするとイメージが異なるかも知れません。

ヘルパーメソッドも便利ですが、最速、最大効率ではないです。(stackallocを使ったほうが良い場面で使ってないなど。) また、自分の正しく理解していないマーシャリングのテクニックなどが使われていると、誤った取り扱いをしてしまうかもしれません。

例えばネイティブリソースは同じスレッドで解放する必要があるものの、APIによっては外部プロセスやOSに破棄する義務が移る場合があります。DllImportの定義がSafeHandleだと二重解放やUse-after-freeに繋がる可能性があると思います(多分)。ネイティブリソースの開放はここら辺も参照です。もっとも、ネイティブの扱いが難し買ったり、セキュリティリスクが生じるのはこのパッケージに限ったことではないのですが。

信頼性に関するベスト プラクティス - Microsoft Docs

最近ではHBITMAPをIDataObject経由で外部プロセスに渡した場合の処理で困って、最終的にDllImportの定義をコピって編集して使うことにしたこともあります。

AArnott/pinvoke

メリット

こちらはIntPtrやポインタがむき出しの定義が多くされており、シンプルかつ汎用性が高いです。DllImport集として使いたい場合にはこちらが向くかと思います。

パフォーマンス的にも(おそらく)悪影響を及ぼすSafeHandleなどを使わないので優れます。stackallocで確保したポインタや、Spanを使って既存のメモリのポインタを直接P/Invokeに渡すといった処理は今後需要が増しそうです。

例 → GetWindow

デメリット

ネイティブに近く、汎用性が高い定義な分Vanaraより利便性は劣ります。また、ポインタが露出しすぎており、そこらじゅうをunsafe修飾するのが面倒に感じる人もいるかも知れません。

使用例

aarnott/pinvokeの方を使った自作ソフトです。開いているエクスプローラのパスを一覧表示するシンプルなソフトですが、ウィンドウの列挙にカジュアルにEnumWindowsを使っています。

KageShiron/ExplorerLS: Listing Explorer’s pathhttps://github.com/KageShiron/ExplorerLS

まとめ

Win32APIをたくさん使う場合に便利なパッケージの紹介でした。

パッケージを入れるという使い方でもいいですし、Win32APIのリファレンスとしてみたり、DllImportや構造体の定義だけパクってきても良いでしょう。(DllImportだけなら著作権は生じないように感じますし、どれにせよMITライセンスなのでカジュアルに使えます。)

最後に、 pinvoke.netでも言えることですが、この手のものは64bitへの移植をしくじっていたりパフォーマンスが劣ったりするものが書いてある場合があります。信用しすぎず定義を必ず確認し、間違いがあればコピペで修正して使ったりコントリビュートを検討しましょう。