2013年12月17日火曜日

main() のすり替え in C++

[2013/12/18] ptr_umain 経由の呼び出しで引数を付けていなかった点を修正。

ある種のライブラリの場合、main() の呼び出し前に処理をしておきたい場合があります。ものすごく真っ当な方法で言うならスタートアップルーチンを書くべきところではありますが、その場合、ライブラリを使う側でもオプション指定が必要になったりしてちょっと面倒になります。こういう時に、ライブラリ側で main() を定義してしまいユーザーコード側では

// main.h
#define main _umain
#ifdef __cplusplus
extern "C" int _umain(int argc, char **argv);
#else
extern int _umain(); // No specification for parameters in C
#endif

// main.c
#include "main.h"
int main(int argc, char **argv) { /* ... */ } // replaced with _umain

のようにヘッダを読み込ませることによって main() をすり替えてしまい、ライブラリ側の main() からすり替えた _umain() を呼び出す、といったことが行われる場合があります。例えば SDL (http://www.libsdl.org/) なんかはこれを使っているようです。

が、問題が一つ。main() は引数が固定されていません。実用的には、以下の3種類くらいあると考えてよいでしょう。

int main(void);
int main(int argc, char **argv);
int main(int argc, char **argv, char **envp);

C の場合、これでもそんなに問題になりません。すり替え用マクロが有効であるとして、上記コードのように extern int _umain(); としておけば、C 言語では引数に関しては指定がない扱いであり、かつ可変引数関数を実現する C の呼び出し規約上、上記いずれの定義であっても _umain(argc, argc, envp); と呼び出してしまえば、普通の処理系ならまず問題なく動作するはずです。

が、C++ の場合、プロトタイプ宣言が必須であり、また、関数の型チェックが必ず行われます。つまり main() がどの形式で定義されていたかによって呼び分ける必要が出てくるわけです。前述の SDL では main() の型を int main(int, char**) に限定することで回避しているようです。最初から特定ライブラリを使う前提であればそれでもいいのですが、今作成中のライブラリ(UTF-8 Win32 API)は後付けの形での利用を想定しているので限定はちょっとつらいです。

要はユーザーがどの形式で main() を定義したか判別する方法があればいいわけです。そこで(処理系依存ですが) GCC の weak symbol と alias を使用してみました。

int _umain_stub()
{
 return 0;
}

// for C
extern "C" int _umain(...) __attribute__((weak, alias("_Z11_umain_stubv")));
// for C++
extern int _umain() __attribute__((weak, alias("_Z11_umain_stubv")));
extern int _umain(int argc) __attribute__((weak, alias("_Z11_umain_stubv")));
extern int _umain(int argc, char ** argv) __attribute__((weak, alias("_Z11_umain_stubv")));
extern int _umain(int argc, char ** argv, char **envp) __attribute__((weak, alias("_Z11_umain_stubv")));

static int(*ptr_umain)(...)                   = static_cast<int(*)(...)>(_umain);
static int(*ptr_umain0)()                     = static_cast<int(*)()>(_umain);
static int(*ptr_umain1)(int)                  = static_cast<int(*)(int)>(_umain);
static int(*ptr_umain2)(int, char**)          = static_cast<int(*)(int, char**)>(_umain);
static int(*ptr_umain3)(int, char**, char**)  = static_cast<int(*)(int, char**, char**)>(_umain);

template<typename T>
static inline bool is_target(T t)
{
 return t != reinterpret_cast<T>(_umain_stub);
}

int main(int argc, char **argv, char **envp)
{
 if(is_target(ptr_umain))  return ptr_umain(argc, argv, envp);
 if(is_target(ptr_umain0)) return ptr_umain0();
 if(is_target(ptr_umain1)) return ptr_umain1(argc);
 if(is_target(ptr_umain2)) return ptr_umain2(argc, argv);
 if(is_target(ptr_umain3)) return ptr_umain3(argc, argv, envp);
 return -1; // Not reached if main() is user-defined
}

通常、同名の関数定義がある場合、多重定義エラーになります。weak symbol を使うと、他に同名の定義がない場合だけ有効になる定義を作ることができます。デフォルト実装の定義を作っておいて、場合によってはより高速な実装に差し替えることも可能、みたいな使い方が典型的なユースケースです。alias はある関数を別の関数の別名として定義できる機能です。上記コードでは C/C++ の _umain() 系を全て _umain_stub(void) の別名かつ weak symbol としています。ユーザーがある形式の main() を定義した場合、マクロで _umain() にすり替えられていずれかの weak symbol が上書きされます。結果として _umain_stub(void) と値(アドレス)が変わるため、それを is_target() で判別して呼び分けているわけです。

で、話が済んでいれば幸せだったのですが、なぜか StrawberryPerl 5.18.1.1 32bit 中の GCC を使って、↑ main() のあるソースファイルで #include <iostream> や #include <cstdio> すると関数ポインタの値がずれます。Cygwin の i686-w64-ming32-g++ だと問題ありません。両方とも GCC-4.7.3 なんですが。例えば実際のアドレスが、
__Z7_umain_v (ユーザー定義の _umain(void)) -> 0x401560
__Z11_umain_stubv (_umain_stub(void)) -> 0x40195e
である場合に、バイナリ中で参照されるアドレスがそれぞれ、0x40117e, 0x40157c だったりします。両方とも 0x3e2 だけずれています。バグなのか設定がおかしいのかちょっと分かりませんがとりあえず以下のようなコードで無理やり補正すると一応動作はします。ユーザー定義の _umain() と _umain_stub(void) の 2 種類があるだけで、かつ、ユーザー定義は 1 つだけ存在するはず、という前提で多数派が実際には _umain_stub(void) であるとみなしてその差を補正しています。

template<typename T>
static inline unsigned long get_offset(T t)
{
 return reinterpret_cast<unsigned long>(_umain_stub) - reinterpret_cast<unsigned long>(t);
}

template<typename T>
static inline void add_offset(T& t, unsigned long offset)
{
 t = reinterpret_cast<T>(reinterpret_cast<unsigned long>(t) + offset);
}

void fix_fptr()
{
 std::map<unsigned long, int> counter;
 ++counter[get_offset(ptr_umain)];
 ++counter[get_offset(ptr_umain0)];
 ++counter[get_offset(ptr_umain1)];
 ++counter[get_offset(ptr_umain2)];
 ++counter[get_offset(ptr_umain3)];
// assert(counter.size() == 2);
 unsigned long offset = counter.begin()->second > (++(counter.begin()))->second ? counter.begin()->first : (++(counter.begin()))->first;
 add_offset(ptr_umain, offset);
 add_offset(ptr_umain0, offset);
 add_offset(ptr_umain1, offset);
 add_offset(ptr_umain2, offset);
 add_offset(ptr_umain3, offset);
}

ちょっとどうだかなという感じではあるので、とりあえず、#include <iostream> や #include <cstdio> を外して様子見な感じですが謎な状態です。

0 件のコメント:

コメントを投稿