thunkを使いOSからダイレクトにメンバ関数を呼び出させる。

公開:2011-10-30 20:29
更新:2020-02-15 04:36
カテゴリ:c++,dawもどきの作成,windows,audio

今までウィンドウハンドル(HWND)とC++インスタンスを結びつけるのをWTL/ATLで採用されているthunkのアイデアを使って実装していた。thunk(といっていいのかなこれって)は下記のような小さなアセンブラソースである。64bit環境ではインラインアセンブラは利用できないので、私はXBYAKを使っている。


   // thisとhwndをつなぐthunkクラス
   struct hwnd_this_thunk : public Xbyak::CodeGenerator {
     hwnd_this_thunk(base_win32_window* impl,ProcType proc)
     {
       // rcxにhwndが格納されているので、それをimpl->hwndに保存
       mov(qword[&(impl->hwnd_)],rcx);
       // 代わりにthisのアドレスをrcxに格納
       mov(rcx,(LONG_PTR)impl);
       // r10にproc(Window プロシージャ)へのアドレスを格納
       mov(r10,(LONG_PTR)proc);
       // Window プロシージャへへジャンプ
       jmp(r10);
     }
   };

ProcTypeはWNDPROCの別名だと思ってもらいたい。このクラスをインスタンス化する際、コンストラクタの引数としてC++クラスへのインスタンスポインタ、ウィンドウ・プロシージャ(WNDPROC)へのポインタをセットする。そしてGetCode()関数を呼び出すと、アセンブルした機械語へのポインタを取得できる。それをCreateWindowで渡すウィンドウ・プロシージャ引数にセットする。OSはこのthunkコードをWNDPROCだと思って呼び出す。するとこの関数によって本来第一引数にセットされるHWNDの値がC++クラスインスタンスへのポインタに書き換えられて、ウィンドウプロシージャが呼び出される。

このthunkによって呼び出されるウィンドウ・プロシージャは下記のようなコードとなる。内容であるがHWNDにインスタンスへのポインタが格納しているのでキャストして取り出し、そのポインタを使ってメンバ関数を呼び出す。


static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  base_win32_window* ptr = reinterpret_cast<base_win32_window*>(hwnd);
  hwnd = ptr->hwnd_;
  return ptr->window_proc(hwnd,message,wParam,lParam);
};

私はこうしてHWNDとC++クラスインスタンスの結びつけを行なっている。

この方法ってやる意味あるの?

こんな危なそうなコードを書いて得られるメリットはあまりない。Set/GetWindowLongPtrにインスタンスへのポインタを格納するのに比べて呼び出しが若干高速に行われそうかな?という程度であり、HWNDとインスタンスを結びつけるのにstaticメソッドもしくはフリー関数を経由させなければならない点では同じである。コードを書く手間もあまり変わらない。私がなんでthunkを使っているかというと単なる趣味である。普通はSet/GetWindowLongPtrを使う方法をおすすめする。

thunkの改良

herumiさんの「Cとの連係 その2C++のメンバ関数の呼び出し方法を読んでいたら、仮想関数でなければ第一引数にthisポインタをセットすればメンバ関数を呼び出せると書いてあった。うまく行けばthunkでstatic/フリー関数を経由せずにダイレクトに呼び出しができるかもしれないと思ってthunkコードを書きなおしてみた。


// thisとhwndをつなぐthunkクラス
// メンバー関数を直接呼び出す。
struct hwnd_this_thunk2 : public Xbyak::CodeGenerator {
  hwnd_this_thunk2(LONG_PTR this_addr,const void * proc)
  {
    // 引数の位置をひとつ後ろにずらす
    mov(r10,r9);
    mov(r9,r8);
    mov(r8,rdx);
    mov(rdx,rcx);
    // thisのアドレスをrcxに格納する
    mov(rcx,(LONG_PTR)this_addr);
    // 第5引数をスタックに格納
    push(r10);
    sub(rsp,32);
    mov(r10,(LONG_PTR)proc);
    // メンバ関数呼び出し
    call(r10);
    add(rsp,40);
    ret(0);
  }
};

このコードを使うと第一引数にthisをセットしてprocを呼び出すことができる。第一引数にthisをセットするために本来のWNDPROCの引数を1つ後ろにずらしている。第5引数は引数が4個以上になるとスタックに格納する必要があるのでそうしている。後はcallでprocを呼び出すだけだ。

メンバ関数をvoidにキャストする方法

しかし問題は残る。メンバ関数へのポインタはvoid にキャストできないのである。void にしないで引数をメンバ関数型にすればよさそうだけれど、レジスタに格納するところでキャストしないといけないので。どうしたものかと考えていたら、先ほどの「C++のメンバ関数の呼び出し方法」にヒントが書いてあった。

(void*)&p = (void)func2;

上記をヒントにキャストを行ったところ無事メンバ関数をvoid*にキャストし、格納することができた。でもこの方法はかなりグレーな方法らしい。。 ちなみに下記が実際のコンストラクタを呼び出すコードである。


thunk((LONG_PTR)this,*(void**)(&(&impl::info_dlgproc)));

結果、無事仮想関数以外のメンバ関数をthunkから直接呼び出すことに成功した。これでthunkを使うメリットが出来た。

thunkの関数ポインタのタイプをテンプレート化すればよさそう。できるかな。。いや、結局thunkコード内でキャストしないといけないからやっぱりできなさそう。。。

しかし、、、

ちょっと怪しいかな。やっぱり。書いたけどあんまりおすすめできないコードですな。。またコードはVC2010でのみ動作するものなのであしからず。。