SetWindowsHookExを使ったフックの基礎
Windowsアプリケーションの入力やメッセージを監視するとき、通常のイベント処理だけでは取得しにくい情報があります。SetWindowsHookExは、キーボード入力、マウス操作、ウィンドウメッセージなど、Windowsが処理する各種イベントにフックプロシージャを登録するためのAPIです。
フックは便利である一方、対象範囲や実行コンテキストを誤ると、入力の遅延、プロセス間の不整合、リソースリークを引き起こします。ここでは、フックの種類、引数の意味、コールバックの流れ、解除やデバッグの注意点を、Win32 APIの基本に沿って整理します。
フックの仕組みと役割
SetWindowsHookExは、指定したフックタイプに対応するコールバック関数をシステムへ登録します。関数の戻り値は登録結果を示すHHOOKで、失敗した場合はNULLになります。登録後、対象となるイベントが発生すると、Windowsからフックプロシージャが呼び出されます。
代表的な宣言は次の形です。
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hmod,
DWORD dwThreadId
);
idHookにはWH_KEYBOARD、WH_MOUSE、WH_CALLWNDPROC、WH_GETMESSAGE、WH_KEYBOARD_LL、WH_MOUSE_LLなどを指定します。lpfnはイベントを受け取る関数、hmodはコールバックを含むDLLのモジュールハンドル、dwThreadIdは監視対象スレッドの識別子です。対象スレッドが自分のプロセス内にある場合、hmodをNULLにできるケースがあります。
コールバックでは、nCodeが0以上のときにlParamやwParamを調べます。nCodeが負の場合は処理せず、直ちにCallNextHookExへ渡すのが基本です。自分の処理が終わった後も、後続のフックへ通知するためにCallNextHookExを呼びます。
スレッド単位とシステム全体の違い
dwThreadIdに特定のスレッドIDを指定すると、そのスレッドのメッセージ処理を対象にできます。監視範囲が限定されるため、通常は影響が小さく、テストもしやすい方法です。自分のアプリケーション内で入力を補助したり、特定のウィンドウのメッセージを調査したりする用途に向いています。
0を指定すると、対象はデスクトップ上の複数プロセスにまたがるシステム全体のフックになります。この場合、通常のメッセージフックではフックプロシージャをDLLに置き、hmodへそのDLLのインスタンスを渡します。各プロセスへコードが読み込まれる可能性があるため、グローバル変数やランタイム初期化に依存した設計は避けなければなりません。
一方、WH_KEYBOARD_LLやWH_MOUSE_LLのような低レベルフックは、入力を発生元のプロセスへ注入する方式ではありません。登録したプロセスのコンテキストで通知を受け取るため、DLLを使わずに実装できることがあります。ただし、コールバックを処理するスレッドが停止すると入力全体へ影響するので、重い処理は別スレッドへ移す設計が安全です。
フックタイプの選び方
必要な情報に対して最も狭い範囲のフックを選ぶことが、安定した実装につながります。たとえば自分のウィンドウが受け取るキー入力だけを調べるなら、アプリケーションの通常イベントやスレッド単位のWH_GETMESSAGEで足りる場合があります。別プロセスの入力まで監視する場合は、低レベルキーボードフックなどを検討します。
| フックタイプ | 主な対象 | 実装上の特徴 | 適した用途 |
|---|---|---|---|
| WH_KEYBOARD | キーボードメッセージ | スレッドまたはDLLが必要 | 通常のキー入力監視 |
| WH_MOUSE | マウスメッセージ | メッセージ処理に関連 | マウス操作の調査 |
| WH_GETMESSAGE | GetMessageで取得するメッセージ | 取得前のメッセージを確認 | メッセージ解析 |
| WH_CALLWNDPROC | ウィンドウへ送られるメッセージ | 送信内容を監視 | ウィンドウ間通信の調査 |
| WH_KEYBOARD_LL | 低レベルのキーボード入力 | コールバック遅延に注意 | 複数アプリの入力監視 |
| WH_MOUSE_LL | 低レベルのマウス入力 | 実行時間の制限が重要 | グローバルなマウス監視 |
SetWindowsHookExの解説を補う際には、Windows APIの仕様や関連サンプルをまとめたWindows API資料も参照先になります。ただし、OSのバージョンや対象アーキテクチャによって挙動が変わるため、最終的にはMicrosoftの最新ドキュメントと実機で確認します。
コールバックの安全な実装
フックプロシージャでは、受け取ったイベントを短時間で判定し、必要な情報だけを保存します。ファイルへの書き込み、ネットワーク通信、複雑なウィンドウ操作などを直接実行すると、入力処理が詰まりやすくなります。イベントをキューへ積み、ワーカースレッドで後から処理する方法が一般的です。
実装時に確認する項目は次のとおりです。
- SetWindowsHookExの戻り値がNULLでないか確認する
- nCodeが負のときは処理せずCallNextHookExを呼ぶ
- wParamとlParamをフックタイプに応じて解釈する
- コールバック内で長時間の処理を実行しない
- 共有データへのアクセスを適切に同期する
低レベルキーボードフックではKBDLLHOOKSTRUCT、低レベルマウスフックではMSLLHOOKSTRUCTをlParamから取り出します。ポインタの型変換を誤ると、32ビットと64ビットで問題が発生します。構造体のサイズやULONG_PTRなどの型を意識し、コンパイラの警告を無視しないことが重要です。
終了処理では、登録時に取得したHHOOKをUnhookWindowsHookExへ渡します。ウィンドウ破棄やDLLアンロードの順序にも注意し、解除後にコールバックが呼ばれない設計を確認します。終了処理で確認する項目は次のとおりです。
- HHOOKを有効な状態で保持しているか確認する
- UnhookWindowsHookExを一度だけ呼び出す
- ワーカースレッドを停止してから共有データを解放する
- DLLを使う場合はアンロード前にフックを解除する
- 登録失敗時に後続の終了処理を安全に実行する
セキュリティと動作確認
グローバルフックは、他のアプリケーションの入力やメッセージに関係します。そのため、管理者権限の有無、整合性レベル、UAC、デスクトップの違いによって、同じコードでも監視できる範囲が変わることがあります。保護されたプロセスや別セッションの入力を、通常のフックだけで取得できるとは限りません。
また、32ビットのフックDLLと64ビットの対象プロセスを混在させる構成には制約があります。対象プロセスとフックモジュールのビット数、ビルド構成、DLL検索パスをそろえ、実行環境で検証します。キーロガーと誤解されるような用途では、取得データを必要最小限にし、利用者への説明やアクセス制御も欠かせません。
デバッグでは、まずスレッド単位の範囲で登録し、イベントが届くことを確認します。次にコールバックの呼び出し回数、nCode、対象ウィンドウ、処理時間をログへ記録します。入力を失わせる実装になっていないか確認するため、通常操作と終了処理を繰り返し、最後にUnhookWindowsHookExが正常に実行されることを確認します。最初の検証ではWH_KEYBOARD_LLを使った小さなテストプログラムを作り、キー入力を記録せずイベント種別だけ表示するところから始めます。