dllやsoから本体側の関数を呼ぶ方法

[注: この記事はわかったところまでを忘れないうちに書き留める目的のもので、soとdylibの違いとかELFとMach-Oの違いとかがいまいちわかってない人間が書いているので要注意です!]

プラグインの側から本体が提供している関数(API)を呼んでどうこうしたいってケースの話。たとえば下の例ではプラグインの側から本体で定義されているaddって関数を呼ぼうとしている。しかし単に本体側の関数のプロトタイプ宣言を持ってきただけではシンボルが見つからない旨のエラーになる。

#include <stdio.h>
int add(int, int);

extern "C" {
void foo(int x){
  printf("%d\n", add(x, 42));
}
}
$ g++ -shared -o so.so so.cpp
Undefined symbols:
  "add(int, int)", referenced from:
      _foo in ccyB73dJ.o
ld: symbol(s) not found
collect2: ld returned 1 exit status

コンパイラオプションに「-Wl,-undefined,dynamic_lookup」を付けると、見つからないシンボルは無視をして実行時に動的にルックアップするようになる。so.soのビルドができたので次はそれを呼び出すコードを書いてみよう。もちろんaddの定義も。

// base.cpp
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int add(int x, int y){
  return x + y;
}

int main(){
  void* handle = dlopen("so.so", RTLD_LAZY); /* soを開く */
  char *error;
  if (!handle) {
    fprintf (stderr, "%s\n", dlerror());
    exit(1);
  }
  dlerror();    /* Clear any existing error */

  typedef void (*Foo)(int); /* (1) */
  Foo foo = (Foo)dlsym(handle, "foo"); /* (2) */
  if ((error = dlerror()) != NULL)  {
    fprintf (stderr, "%s\n", error);
    exit(1);
  }

  foo(42); /* (3) */
  dlclose(handle);
}

(1)ではintを取ってvoidを返す関数ポインタ型Fooを作っている。そして(2)で"foo"という名前の関数へのポインタを取得してそれをFooにキャストして、fooという名前の変数に入れている。そうすると(3)のように普通の関数みたいに呼ぶことが出来る。(see http://www.kouno.jp/home/c_faq/c4.html#12)

これを実行すると期待通りのaddが呼ばれた結果が返るので「本体側の関数を呼ぶことは簡単にできる」ということがわかった。筆者はこれを「POSIXでは簡単にできる」「ELFでは簡単にできる」「gccでは簡単にできる」のどれにするのが適切かわからない。ELFらしいけど。(moriyoshi先生談)

さてここからが本題で、これと同じことをWindowsでやりたい場合にどうすればいいか。色々調べて、とあるオプション(忘れてしまった)を付けると未定義のシンボルをエラーにせずに無視してビルドすることが出来ることがわかった。

しかしこれは罠で、実行時に動的ルックアップはしてくれないらしい!

それに気づかずに例外0xC0000005とか出たり、調べてみたらこれはアクセスバイオレーションだとわかったり、「あれー、デバッグビルドしてるのにどこでアクセスバイオレーションになってるのかソースが見れないぞー」ってなったり、アクセスバイオレーションが発生する最小限のコードにしてみたら最初のAPIコールで死んでいることがわかったのでその関数にブレークポイントをつけてみたらテスト実行までに何度も呼ばれてたり、エラーの直前に呼ばれるときの引数などが特におかしく見えなかったり、エラーの直前に呼ばれてからの処理をステップ実行していたらスレッドがたくさんあってあっちゃこっちゃ行って意味不明だったり、………という盛大な遠回りをしてしまったがようするにAPIをコールした後死んでいるのでも、APIをコールする前に死んでいるのでもなくAPIの関数ポインタが不正な値なので呼んだ瞬間に死んでいるというオチだった。変なところに飛んで死ぬのでソースが見れず、「死ぬ直前に呼ばれた問題のAPIの処理」を追っかけても単なるハウスキーピングだから意味がなく、スレッドが切り替わってプラグイン内でその呼び出しが呼ばれたタイミングで死ぬので死のタイミングが変わりうる、と。

さて、罠と遠回りはさておき、じゃあWindowsではどうすればいいのか?方法は二つあるようだ。1つは関数ポインタを構造体などに詰め合わせにして引数などでDLL側に渡してやる方法(thanks 光成さん)、もうひとつは本体側の公開したい関数に__declspec(dllexport)をつけて、GetModuleHandleExで本体exeのハンドルを取得する方法(thanks id:moriyoshi)

If this parameter is NULL, the function returns a handle to the file used to create the calling process (.exe file).

で、前者のサンプルを作ったり、後者のサンプルをmoriyoshi先生にもらったりしたのだけども、これからプログラミングシンポジウムが始まるので続きはまた今度で。