コールバック


Top / コールバック

このページは何か? (by Tino)

コールバックとメンバ関数の関係は複雑なので、整理してみます。

経緯

Monaでスレッドに引数を渡せるようになりましたが、SUNEO/007.まだ投げないEDS1275さんより、ラッパーを挟まずにC++と絡めることができるかどうかという問題提起がありました。

結論から言うと、コールバック関数の呼び出し規約が、

目的

コールバックとメンバ関数の仕組みを分析します。

関連ページ

メンバ関数の正体

次のようなソースを用意します。(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)という関数と等価であるということが分かります。

メンバ関数の強制呼び出し

そのことを実際に確認するのが次のコードです。(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言語でオブジェクト指向

マングリング名を自前解決して呼び出すのは分かりにくいので、純粋に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

このことから、クラスというのは、構造体と関数をセットにしたシンタックスシュガーだと考えられます。

参考までに、このような方針を推し進めて一般化した規約があります。

コールバックさせてみる

以上から、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*)としてコールバックさせることに成功しました。

強引なキャスト

ここでのポイントは次の部分です。

  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のキャストは拒否されます。

仕方ないので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);

呼び出し規約

このようにthisを明示的な第一引数と扱うことで、メンバ関数をCの関数にキャストしてコールバックさせることは可能です。

しかしこれはgcc-3.4のthiscallの規約がそうなっているため可能な技で、コンパイラによっては使うことができません。具体的にはthisをレジスタで渡すコンパイラがありますが、そのようなコンパイラではこの技は使えません。環境依存の邪道なテクニックで、汎用性はありません。

syscall_mthread_create_with_arg()はfastcallのため、引数はスタックではなくレジスタで渡されます。そのためこれも直接キャストして渡すことはできません。

以上をまとめたのが、冒頭にも書いた結論です。

結論

コールバック関数の呼び出し規約が、

関数テンプレートによる汎用的な変換

コールバックはメンバ関数と相性が悪いのでラッパーで諦めるべきでしょうか?それが簡単な方法ではあります。

しかしどうしても諦めきれない場合は、thiscallをfastcallに変換する関数テンプレートを使ってみるのも良いかもしれません。

template <class T, void(T::*func)()>
void __fastcall this2fast(void* p) {
  (((T*)p)->*func)();
}

これはコールバックを破壊するキャストをしていないため、fastcallをサポートするコンパイラであれば汎用的に使えるはずです。ただし(T*)pの部分は型保証されない危険なキャストですが、今回は型保証のためにテンプレートを使っているわけではないので、目をつむります。。。

使用例

関数テンプレートだけでは意味不明なので使ってみます。(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でスレッド生成

これをMonaで使うとこんな感じになるでしょう。ラッパーを汎用化して自動生成しているイメージですね。

// Test t のとき t.test() でスレッドを開始したい
syscall_mthread_create_with_arg(this2fast<Test, &Test::test>, &t);

MonaFormsで使った委譲と同種のテクニックではあります。

以上です。

コメント

最新の1000件を表示しています。 コメントページを参照

お名前:
  • ちなみにclではthisがECXで渡されるということを利用すると、gccとは逆に、コールバックをfastcallにすればキャストできます。test4をcl用に修正すると以下のようになります。これはgccでは動きません。 -- Tino 2006-10-25 (水) 09:08:34
    #include <stdio.h>
    class Test {
      int a;
    public:
      Test(int a):a(a){}
      void test(){printf("%d\n",a);}
    };
    void call(void(__fastcall *f)(void*), void* p) {
      (*f)(p);
    }
    int main() {
      Test t(1234);
      void(Test::*f)() = &Test::test;
      void** p = (void**)&f;
      call((void(__fastcall *)(void*))(*p), &t);
      return 0;
    }
  • 早く帰れたので、すこし実験してみました。-- EDS1275 2006-10-24 (火) 19:00:07
    • 引数がレジスタ渡しであるSolaris@amd64だとだめかと思って試してみましたが
      test4が動作することが確認できました。thiscallと標準呼び出し規約の違いが第一引数にthisが来ることだけであることが重要ということが理解できました。
      • i386の場合はデフォルトがスタック渡しのため、レジスタ渡しが特別扱い(fastcall明示)になっていて互換性がないということです。レジスタ渡しがデフォルトの環境ではこの限りではありません。 -- Tino 2006-10-24 (火) 22:09:34
      • cl(VCのコンパイラ)ではthisがECX渡しのため、test4は正常動作しません。これは実際に確認しました。thiscallの第一引数のthisが、普通の引数とは違う扱いを受けている例です。 -- Tino 2006-10-24 (火) 22:14:58
      • 関数テンプレートはABIに影響されないため、this2fast()がclで正常動作することを確認しました。関数テンプレートの方が安全です。ただしthis2fast.cppは関数テンプレート以外の部分で引っ掛かりました。1. _ZN4Test4testEv()はgccのマングリングに依存するため動作しない。 2. 関数ポインタでの__fastcallの位置が違う⇒void call(void (__fastcall *f)(void*), void* p); -- Tino 2006-10-24 (火) 22:25:09
      • gccでもvoid call(void (__fastcall *f)(void*), void* p);はコンパイルが通るため、こちらが正規の記述方法のようです。 -- Tino 2006-10-24 (火) 22:27:23
    • そして、眺めているうちに、次のようなプログラムができちゃうことに気がつきました。
      #include <stdio.h>
      class Test {
      private:
       Test();
      public:
       void test(){printf("%x called \n",this);}
      };
      void call(void(*f)()) { (*f)(); }
      int main() {
       void(Test::*f)() = &Test::test;
       void** p = (void**)&f;
       call((void(*)())(*p));
       return 0;
      }
      • このコードはclでも動きます。以下のようにcallでECXに代入すると、thisがECXとして渡されていることが確認できます。 -- Tino 2006-10-25 (水) 08:59:39
        void call(void(*f)()) {
          __asm { mov ecx, 0x1234 }
          (*f)();
        }
    • Privateコンストラクタを持つクラスの非スタティックメンバ関数が
      インスタンスなしで呼べてしまいます。おもしろい!
      • これはフィールド(メンバ変数)にアクセスしない限り、deleteしたインスタンスやNULLに対してメンバ変数を呼び出しても落ちないのと同じ理屈です。 -- Tino 2006-10-24 (火) 22:31:29
        #include <stdio.h>
        class Test {
          public: void test(){printf("this=%p\n",this);}
        };
        int main() {
          Test* t = new Test;
          t->test();
          delete t;
          t->test();
          t = NULL;
          t->test();
          return 0;
        }
      • あー、だからsizeof(Test)は小さい(メンバ変数分しかない)のか! -- EDS1275 2006-10-24 (火) 22:50:45
      • それが「C言語でオブジェクト指向」というになります。クラスは構造体を拡張したものです。そのためclassキーワードをstructに置き換えても、デフォルトのスコープ以外はすべて同じ扱いになります。なお、仮想関数が絡んでくるとvtableが必要になるので、sizeofはメンバ変数だけでなくvtableも含むサイズになります。 -- Tino 2006-10-25 (水) 08:55:15
    • 汎用性や安全性を考えるとテンプレートが優れた方法であることは理解できました。しかし魔法のようなキャストがあまりにも印象的で琴線に触れるものがあります。昨夜はfの直接キャストがなぜ出来ないのか考えていたのですが、わかりませんでした。 -- EDS1275 2006-10-25 (水) 07:37:35
      • C++の規約で禁止されているからでしょう。test4がclで動かなかったように、動作がABIに左右されてしまうような記述が禁止されていると思われます。 -- Tino 2006-10-25 (水) 08:51:50
  • すばらしい説明をありがとうございます。fが直接キャストできないところをあっさり回避してしまうところに超えられない壁を感じました。 -- EDS1275 2006-10-24 (火) 01:21:50
    • 恐縮です。何かの参考になれば幸いです。今後ともよろしくお願いします。 -- Tino 2006-10-24 (火) 01:31:57
  • お。このまとめ面白い。extern "C" void _ZN4Test4testEv(Test* t);。期待。 -- ひげぽん 2006-10-24 (火) 00:09:00
    • Monaに関わるまではコンパイラの吐いたアセンブリを眺めたりする人じゃなかったんですが。。。 -- Tino 2006-10-24 (火) 01:17:34

MENU

now: 1

リンク


最新の20件
2018-05-03 2017-09-29 2017-04-25 2017-01-10 2016-12-11 2016-10-04 2016-08-14 2016-06-05 2016-05-29 2016-04-15 2015-12-28 2013-02-25 2013-02-21 2013-02-20 2013-02-12 2013-02-11 2013-02-10
最新の20件
2010-02-01 2010-01-31 2010-01-30 2010-01-29 2010-01-16

Counter: 9562, today: 1, yesterday: 0

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

Last-modified: 2008-03-28 (金) 15:48:01 (3709d);  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.048 sec.