読者です 読者をやめる 読者になる 読者になる

S.F. Page

Programming,Music,etc...

音声ファイルから動画ファイルを生成する(18) – フック・サブクラスのラッパーを作る

Windows API 音声動画出力プログラム Windows C++ Audio

フックおよびサブクラスのラッパー

昨日からフックとサブクラスのラッパーを書いてみている。コンパイルを通しただけでまだテスト実行はしていない。下のコードはフックラッパーのコードである。


  // フック
  struct hook : boost::noncopyable {
    // フック処理ファンクションオブジェクト
    typedef std::function<LRESULT(int nCode, WPARAM wp, LPARAM lp)> hook_proc_t;

    hook(int idHook,        // フックタイプ
      hook_proc_t& proc,  // フックプロシージャ
      HINSTANCE hMod,    // アプリケーションインスタンスのハンドル
      DWORD dwThreadId   // スレッドの識別子
      ) : proc_(proc), thunk_((LONG_PTR)this)
    {
      hook_ = SetWindowsHookEx(
        idHook,        // フックタイプ
        (HOOKPROC)thunk_.getCode(),     // フックプロシージャ
        hMod,    // アプリケーションインスタンスのハンドル
        dwThreadId   // スレッドの識別子
        );
    }

    ~hook(){
      UnhookWindowsHookEx(hook_);
    }

  private:

    // 64bitモードのみで動作するサンクコード
    // HookProcの呼び出しにthisを加えてメンバー関数として呼び出すコード
    struct hook_proc_thunk : public Xbyak::CodeGenerator {
      hook_proc_thunk(LONG_PTR this_addr)
      {
        // メンバ関数のアドレスを取得
        auto temp = &hook::hook_proc;
        // メンバ関数のアドレスをLONG_PTRにキャストする
        // 普通にキャストできないので、ポインタのアドレスをvoid**にキャストして参照する
        LONG_PTR proc = reinterpret_cast<LONG_PTR>(*(void**) &temp);

        // 引数の位置をひとつ後ろにずらす
        mov(r9, r8);
        mov(r8, rdx);
        mov(rdx, rcx);
        // thisのアドレスを第一引数(rcx)に格納する
        mov(rcx, (LONG_PTR) this_addr);
        // 関数呼び出し
        // 作業用スタックの確保
        sub(rsp, 32);
        mov(r10, proc);
        call(r10);
        // スタックの清掃
        add(rsp, 32);
        ret(0);
      }
    };

    // 汎用性を高めるために、関数をファンクタ(std::function)で呼び換えるためのラッパー
    LRESULT hook_proc(int nCode, WPARAM wp, LPARAM lp)
    {
      return proc_(nCode, wp, lp);
    }
    hook_proc_thunk thunk_;
    hook_proc_t& proc_;
    HHOOK hook_;
  };

フックプロシージャのコールバックを最終的にはコンストラクタで指定したhook_proc_t型のファンクタ呼び出しに置換する。ファンクタにしておけばただの関数でもメンバ関数でも呼べるようになるので汎用性がある。ほんとはファンクタ経由ではなくて直接呼び出すようにしたいけども、ファンクタに相当する機能を実装するのが面倒なのでそこまではしていない。パフォーマンスが気になるけれどもまあそんなにシビアな処理ではないし、問題となることはないのではないかなと思う。

普通のコールバック関数をメンバ関数に置換するのはサンク(hook_proc_thunk)で実現している。サンクはXYBAKで書いている。元コードはウィンドウ・ラッパクラスのサンクコードであり、パラメータ数が違うのでその構成を変更したのみである。サンクでは何をしているのかというと、元々の引数を1つずつずらして第一引数にthisポインタをセットし、メンバ関数を呼び出す処理をしている。

アセンブラコードはXYBAKを使用している。このサンクはX64かつVCでしか動作しないと思う。ただこのコード、メンバ関数はcallで呼び出すようにしているけれどもjmpで転送するように書き換えても動作するのはないかなと思っている。その場合は前後のrspのadd/subは不要になると思う。

コンパイルを通すうえでハマったのがメンバ関数ポインタをvoidポインタにキャストする方法である。普通であればキャストできないのだが、メンバ関数ポインタのアドレスをvoid**にキャストすることはできるので、それを使って無理やりキャストしている。

仮想関数のアセンブラコードからの呼び出し

ファンクタはライブラリを使えばいいと割り切った。でもそこに行き着くまでに試行錯誤した中で技術的にもう少し深堀りしたいことがある。それは仮想関数のアセンブラコードからの呼び出し方法についてである。仮想関数はvtblを間接参照してよびだせばできそうなのであるが、いまだ具体的な方法に行き着いていない。これがわかればフリー関数・メンバ関数・仮想関数で特殊化したサンク・テンプレートを書けば私が求めている汎用度を満たすので、ひょっとするとファンクタをなくせるかもしれない。ってまだ割り切っていないね。。

こういうアセンブラ混じりのコードを書くのはなんか楽しい。X86/X64ニーモニックはほとんど知らない私が言うのもなんだが。