コールバック の変更点



 #setlinebreak(on)
 #topicpath
 #contents
 ** このページは何か? &size(3){(by [[Tino]])}; [#n331a79d]
 コールバックとメンバ関数の関係は複雑なので、整理してみます。
 
 // ↓適宜コメントアウトして使いましょう
 **経緯 [#vbb1188e]
 Monaでスレッドに引数を渡せるようになりましたが、[[SUNEO/007.まだ投げない]]で[[EDS1275]]さんより、ラッパーを挟まずにC++と絡めることができるかどうかという問題提起がありました。
 
 結論から言うと、コールバック関数の呼び出し規約が、
 - cdeclであれば可能。
 - fastcallでは不可能。
 
 **目的 [#j8d1af8f]
 コールバックとメンバ関数の仕組みを分析します。
 
 //**概要
 //**アクション
 //**未解決な問題
 **関連ページ [#nf761f49]
 - [[SUNEO/007.まだ投げない]]
 - [[MonaForms/delegate]]
 
 //#ls2
 
 ** メンバ関数の正体 [#t776a142]
 次のようなソースを用意します。(test1.cpp)
  class Test {
    public: void test();
  };
  void Test::test() {}
  void call() {
    Test t;
    t.test();
  }
 
 Intel形式でアセンブリを出力します。
  $ g++ -masm=intel -S test1.cpp
 
 出力されたtest1.sより、call()中のt.test();を抜き出してみます。
  lea     eax, [ebp-1]
  mov     DWORD PTR [esp], eax
  call    __ZN4Test4testEv
 
 ebp-1というのは&tを表しています。lea eax, [ebp-1]というのはeax=ebp-1という意味です。つまりtのインスタンスのポインタをeaxに入れています。
 
 [esp]にeaxを入れていますが、スタックの先頭に入れるということは、直前でpushするのと同様の効果があります。callの前にスタックに積むということは、つまり引数として扱われていると見なせます。
 
 __ZN4Test4testEvというのはTest::test()のマングリング名です。(最初のアンダーバーはプレフィックスなので落とします)
  $ c++filt _ZN4Test4testEv
  Test::test()
 
 このことから、void Test::test()というメンバ関数は、void _ZN4Test4testEv(Test* t)という関数と等価であるということが分かります。
 
 *** メンバ関数の強制呼び出し [#j320c6b7]
 そのことを実際に確認するのが次のコードです。(test2.cpp)
  #include <stdio.h>
  class Test {
    int a;
  public:
    Test(int a):a(a){}
    void test();
  };
  void Test::test(){printf("%d\n",a);}
  extern "C" void _ZN4Test4testEv(Test* t);
  int main() {
    Test t(1234);
    // t.test();
    _ZN4Test4testEv(&t);
    return 0;
  }
 
 実行してみます。
  $ g++ -o test2 test2.cpp
  $ ./test2
  1234
 
 このことから_ZN4Test4testEv(&t);がt.test();と等価であることが分かります。
 
 メンバ関数というのはthisを隠された第一引数として渡す関数なのです。
 
 ** C言語でオブジェクト指向 [#p0bdbc2e]
 マングリング名を自前解決して呼び出すのは分かりにくいので、純粋にC言語だけで同じ意味のコードを書いてみます。(test3.c)
  #include <stdio.h>
  typedef struct { int a; } Test;
  void Test_test(Test* this) {
    printf("%d\n", this->a);
  }
  int main() {
    Test t = { 1234 };
    Test_test(&t);
    return 0;
  }
 
 これはthisを予約語としてではなく単なる変数名として扱っているので、必ずC言語として保存する必要があります。C++として扱うとエラーになります。
 
 実行してみます。
  $ gcc -o test3 test3.c
  $ ./test3
  1234
 
 このことから、クラスというのは、構造体と関数をセットにしたシンタックスシュガーだと考えられます。
 
 参考までに、このような方針を推し進めて一般化した規約があります。
 - [[C 言語によるオブジェクト記述法 COOL ver.2>http://www.sage-p.com/process/cool.htm]]
 
 ** コールバックさせてみる [#n1484d46]
 以上から、Test::test()はTest_test(Test*)と等価だと見なすことができます。
 それではTest::test()を強制的にvoid(*)(void*)としてコールバックできるでしょうか?
 
 実際にやってみます。(test4.cpp)
  #include <stdio.h>
  class Test {
    int a;
  public:
    Test(int a):a(a){}
    void test(){printf("%d\n",a);}
  };
  void call(void(*f)(void*), void* p) {
    (*f)(p);
  }
  int main() {
    Test t(1234);
    void(Test::*f)() = &Test::test;
    void** p = (void**)&f;
    call((void(*)(void*))(*p), &t);
    return 0;
  }
 
 実行してみます。
  $ g++ -o test4 test4.cpp
  $ ./test4
  1234
 
 Test::test()を強制的にvoid(*)(void*)としてコールバックさせることに成功しました。
 
 *** 強引なキャスト [#xfd56f14]
 ここでのポイントは次の部分です。
    void(Test::*f)() = &Test::test;
    void** p = (void**)&f;
    call((void(*)(void*))(*p), &t);
 
 まずfにTest::test()のアドレスを入れています。これは[[MonaForms/delegate]]で解説したメンバ関数のコールバックの正規の書式です。
    void(Test::*f)() = &Test::test;
 
 fをvoid(*)(void*)にキャストすれば良さそうですが、fのキャストは拒否されます。
 - &color(red){''【エラー】''}; void* p = (void*)f;
 - &color(red){''【エラー】''}; void(*p)(void*) = ( (void(*)(void*) )f;
 
 仕方ないのでfのポインタを取ります。
   void** p = (void**)&f;
 
 先ほど(void*)fとして得たかったポインタが*pとして手に入りました!
 
 これは一見意味不明ですが、fは関数のアドレスが入っている整数値なので、以下と原理的に同じことをやっています。
  int a = 1;
  printf("[%p] %d\n", &a, a);
  void** p = (void**)&a;
  printf("[%p] %d\n", p, *p);
 
 あとは*pをキャストしてコールバックとして押し込むというわけです。
    call((void(*)(void*))(*p), &t);
 
 ** 呼び出し規約 [#hae1ffe7]
 このようにthisを明示的な第一引数と扱うことで、メンバ関数をCの関数にキャストしてコールバックさせることは可能です。
 
 しかしこれはgcc-3.4のthiscallの規約がそうなっているため可能な技で、コンパイラによっては使うことができません。具体的にはthisをレジスタで渡すコンパイラがありますが、そのようなコンパイラではこの技は使えません。環境依存の邪道なテクニックで、汎用性はありません。
 
 syscall_mthread_create_with_arg()はfastcallのため、引数はスタックではなくレジスタで渡されます。そのためこれも直接キャストして渡すことはできません。
 
 以上をまとめたのが、冒頭にも書いた結論です。
 
 *** 結論 [#v2f1cd18]
 コールバック関数の呼び出し規約が、
 - cdeclであれば可能。
 - fastcallでは不可能。
 
 ** 関数テンプレートによる汎用的な変換 [#q072a9f7]
 コールバックはメンバ関数と相性が悪いのでラッパーで諦めるべきでしょうか?それが簡単な方法ではあります。
 
 しかしどうしても諦めきれない場合は、thiscallをfastcallに変換する関数テンプレートを使ってみるのも良いかもしれません。
  template <class T, void(T::*func)()>
  void __fastcall this2fast(void* p) {
    (((T*)p)->*func)();
  }
 
 これはコールバックを破壊するキャストをしていないため、fastcallをサポートするコンパイラであれば汎用的に使えるはずです。ただし(T*)pの部分は型保証されない危険なキャストですが、今回は型保証のためにテンプレートを使っているわけではないので、目をつむります。。。
 
 *** 使用例 [#p26f1e04]
 関数テンプレートだけでは意味不明なので使ってみます。(this2fast.cpp)
  #include <stdio.h>
  
  class Test {
    const char* s;
  public:
    Test(const char* s):s(s){}
    void test(){printf("%s\n", s);}
  };
  
  template <class T, void(T::*func)()>
  void __fastcall this2fast(void* p) {
    (((T*)p)->*func)();
  }
  
  void call(void __fastcall(*f)(void*), void* p) {
    (*f)(p);
  }
  
  extern "C" void _ZN4Test4testEv(Test* t);
  
  int main() {
    Test t("mona");
    t.test();
    _ZN4Test4testEv(&t);
    this2fast<Test, &Test::test>(&t);
    call(this2fast<Test, &Test::test>, &t);
    return 0;
  }
 
 実行してみます。
  $ g++ -o this2fast this2fast.cpp
  $ ./this2fast
  mona
  mona
  mona
  mona
 
 *** Monaでスレッド生成 [#o6d10021]
 これをMonaで使うとこんな感じになるでしょう。ラッパーを汎用化して自動生成しているイメージですね。
  // Test t のとき t.test() でスレッドを開始したい
  syscall_mthread_create_with_arg(this2fast<Test, &Test::test>, &t);
 
 MonaFormsで使った委譲と同種のテクニックではあります。
 
 以上です。
 
 ** コメント [#z99a6a57]
 #pcomment(,1000,below,reply)

リロード   新規 編集 差分 添付 複製 改名   トップ 一覧 検索 最終更新 バックアップ   ヘルプ   最終更新のRSS

Modified by mona
PukiWiki 1.4.6 Copyright © 2001-2005 PukiWiki Developers Team. License is GPL.
Based on "PukiWiki" 1.3 by yu-ji
Powered by PHP 5.2.17
HTML convert time to 0.036 sec.