Windows システム全体入力監視の構築:C# での SetWindowsHookEx 活用と障害回避

Windows プラットフォームにおいて、ユーザーが操作するすべてのアプリケーションやスレッドにおけるキーボード・マウス入力を傍受したい場合、`SetWindowsHookEx` という Win32 API が一般的に採用されます。ただし、この機能をマネージドコード(.NET)から呼び出す際には、いくつかの重要な制約とエラー条件が存在し、単純な実装では動作しないケースが多発します。以下では、効果的な実装方法と、発生しやすいトラブルシューティングについて詳述します。

基本的な実装構造

まずは、機能を確認するための最小限の実装例を示します。プロジェクトに直接組み込む前に、P/Invoke 定義が必要となることを理解してください。以下のコードは、低レベル入力フックを設定する基本的な枠組みです。

<code>using System;
using System.Runtime.InteropServices;

internal sealed class SystemInputInterceptor : IDisposable
{
    private IntPtr _hookHandle = IntPtr.Zero;
    private readonly NativeHookDelegate _nativeCallback;

    public SystemInputInterceptor()
    {
        // キャッシュデリゲートを保持し、GC で回収されないようにする
        _nativeCallback += OnNativeInput;
    }

    public bool InstallKeyboardHook(uint threadId = 0)
    {
        if (_hookHandle != IntPtr.Zero) return true;

        IntPtr hModule = GetKernelModuleHandle();
        _hookHandle = SetWindowsHookEx(
            HookType.WH_KEYBOARD_LL,
            _nativeCallback,
            hModule,
            threadId);

        if (_hookHandle == IntPtr.Zero)
        {
            int err = Marshal.GetLastWin32Error();
            throw new System.ComponentModel.Win32Exception(err);
        }

        return true;
    }

    private IntPtr OnNativeInput(int nCode, IntPtr wParam, IntPtr lParam)
    {
        // イベントロジックをここで記述
        return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
    }

    public void Dispose()
    {
        if (_hookHandle != IntPtr.Zero)
        {
            UnhookWindowsHookEx(_hookHandle);
            _hookHandle = IntPtr.Zero;
        }
        GC.SuppressFinalize(this);
    }
}</code>

P/Invoke 定義

上記クラスが正常に機能するためには、必要かつ十分な Win32 API の外部定義を追加する必要があります。特に `SetWindowsHookEx` は `LastError` を返すため、`SetLastError = true` を指定することが推奨されます。

<code>internal static class Win32Apis
{
    [DllImport("user32.dll", SetLastError = true)]
    internal static extern IntPtr SetWindowsHookEx(HookType idHook,
        NativeHookDelegate lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    internal static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
        IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    internal static extern IntPtr GetModuleHandle(string lpModuleName);

    [DllImport("kernel32.dll")]
    internal static extern IntPtr LoadLibrary(string lpFileName);

    internal delegate IntPtr NativeHookDelegate(int nCode, IntPtr wParam, IntPtr lParam);

    internal enum HookType : int
    {
        WH_MOUSE = 7,
        WH_KEYBOARD_LL = 13,
        WH_MOUSE_LL = 14
        // 他のフックタイプは省略
    }
}</code>

頻出する課題と対策

実装を行う過程で遭遇する主な問題は、主にホストされるランタイムの種類や、プロセス間通信の制限に起因します。

1. モジュールハンドル(hMod)の指定問題

多くのチュートリアルでは、現在のアセンブリのモジュールハンドルを取得するために `Marshal.GetHINSTANCE` が使用されることがありますが、これは時代遅れのアプローチとなりつつあります。.NET Framework 4.0 より後のバージョンでは、JIT コンパイルされたコードが非ネイティブモジュールとして扱われないため、このメソッドが期待通りに動作しないケースがあります。

より安定した方法は、プロセス内で既に読み込まれている既存のシステム DLL のハンドルを取得することです。例えば、`user32.dll` やエントリーポイントアセンブリのハンドルを渡すことが有効です。

<code>// .NET Core または Framework 4.x でも確実な方法
IntPtr hMod = Win32Apis.LoadLibrary("user32.dll");</code>

2. エラーコード 126「指定されたモジュールが見つかりません」

実行時にこのエラーが発生する場合、`Marshal.GetHINSTANCE` が正しく非管理ハンドスを生成していない可能性が高いです。前述の通り、`Assembly.GetExecutingAssembly()` からの代わりとして、`Assembly.GetEntryAssembly()` を参照するか、あるいは `LoadLibrary` を使ってシステム DLL のハンドルを明示的に取得することで解決できます。

3. エラーコード 1429「このフック手順はグローバルに設定する必要があります」

これは、低レベルフック(`WH_KEYBOARD_LL` や `WH_MOUSE_LL`)を使用している場合に発生します。これらのタイプのフックは、特定のワイントレッドではなく全システムに対して適用される必要があるため、`SetWindowsHookEx` の 4 つ目の引数(スレッド ID)には必ず `0` を指定しなければなりません。特定のスレッド ID を指定すると、このエラーとなります。

4. プロセス間の安定性とクラッシュ

最も注意すべき点は、マネージドアセンブリ(.exe や.dll)を他のプロセスへインジェクトしようとする試みです。

  • 低レベルフックの利点: `WH_MOUSE_LL` と `WH_KEYBOARD_LL` は、ターゲットプロセス内に DLL をロードする必要がありません。したがって、.NET コードでこれらを使用する場合、他プロセスのクラッシュリスクは低いです。
  • 通常のフックのリスク: その他のフックタイプ(例:`WH_KEYBOARD`)を使用すると、DLL が対象プロセスへロードされます。しかし、.NET 管コードの DLL には適切なエントリポイントが存在しないか、ランタイム依存関係により競合が発生しやすいため、被対象アプリケーションが停止する原因になります。

もし他プロセスの詳細な監視を行う必要がある場合は、非管理コード(C++)でラッパー DLLを作成するか、または `SetWinEventHook` API の代替手段を検討するのが望ましいでしょう。

5. 特定のウィンドウへのメッセージ限定

グローバル入力監視とは対照的に、特定のウィンドウのみを対象にする場合、メッセージループはスレッド単位で管理されています。`CreateWindowEx` 等で作成されたウィンドウプロシージャとは異なり、フックを使用する場合、最終的にはスレッド ID を指定してフィルタリングを行うことになります。特定のウィンドウハンドルに基づいてフィルタリングするには、`GetWindowThreadProcessId` API を利用し、該当ウィンドウを持つスレッド ID を抽出した上でフックをインストールする必要があります。ただし、より簡潔に特定ウィンドウだけを監視したい場合は、ウィンドウサブクラス化(SubClassing)の方が適切である場合があります。

タグ: C# Win32API SetWindowsHookEx P/Invoke .NET

7月13日 19:02 投稿