はじめに
MySQLのストレージエンジンは、MySQLが提供する関数を使用します。そのため、通常はMySQLのストレージエンジンをWindows上でmysqldのコアにリンクすることが必要です。これは、mysqldが.dllではなく.exeであり、必要な関数がすべてエクスポートされないことによります。したがって、自作のプラグインを作成するときに、必要な関数をすべてインポートすることは不可能です...少なくとも理論上は。
しかしありがたいことに、MySQLでは、リリースの配布物の中に.mapファイルが付属しています。この中には、必要なインポート関数のアドレスがすべて示されています。そこで私は、次のようなアイデアをひらめきました。
Microsoft Visual C バージョン6のリンカーには、遅延読み込みの機能があります。必要なDLL関数を実行中に読み込むことができる機能です。その場合、リンカーは、IATのポインタを、特別なインポートコードに対応付けます。簡単に言うと、必要な関数への最初のアクセス時にLoadLibrary()とGetProcAddress()を呼び出して、IATのエントリを正しいアドレスで上書きするというコードです。遅延読み込みの詳細については、http://www.microsoft.com/msj/1298/hood/hood1298.aspxを参照してください。
幸い、遅延読み込みのコードは、リンカー自身が生成するものではありません。Microsoft Visual Studio 6以降に同梱されているdelayimp.lib内の、あらかじめ定義された関数を呼び出しているだけです。さらに幸いなことに、遅延インポートローダーのソースコードも同梱されています(VC98¥Include¥DELAYHLP.CPP)。ここまで言えば、私のアイデアがおわかりになるかもしれません。
そのアイデアとは、遅延インポートローダーを自分で作成するというものです。この遅延インポートローダーは、要求された関数へのエントリポイントを.mapファイルに基づいて解決し、その正しい関数ポインタをIATに書き込みます。本稿のプラグインは、すべてのコードが含まれたmysqld.exeプロセスのコンテキストで動作するので、必要なコードのfarポインタさえわかればOKです。もちろん、.exeファイルが再配置されていないかどうかをチェックして、最終的に.mapファイルのアドレスを再配置する必要はあります。しかしそれは大きな問題ではありません。必要な読み込みアドレスも.mapファイルに含まれているからです(アドレス - 必要な読み込みアドレス + 実際の読み込みアドレス)。モジュールのモジュールハンドルは読み込みアドレスとまったく同じなので、これは簡単です。このようなdelayimp.libを作成して、自作のプラグインにリンクし、すべてのインポートを実行時に動的に解決するという方法で行くことにします。ただし仕様上、リンカーには、関数宣言を解決するインポートライブラリが必要です。
では、始めることにしましょう。
まずは、一連の作業を行うためのビルド環境をセットアップする必要があります。MySQLのソースディレクトリにwin¥delayloadディレクトリを作成してください。
インポートライブラリの作成
1. 静的インポートライブラリを作成する
リンカーの要件を満たすために、mysqldのすべてのオブジェクトを基にしたライブラリファイルを作成する必要があります。必要なオブジェクトのリストを取得するために、私は簡単なバッチスクリプトを作成しました。mysqldのビルド処理の際にcmakeによって作成されるbuild.makeファイルから取得を行います。それには、コンパイル済みオブジェクトとbuild.makeファイルが必要なので、mysqldをコンパイルしていない場合、まずはコンパイルしてください。具体的には、MySQLのソースのルートディレクトリで、次のように入力します。
cmake.exe" . -G "NMake Makefiles"
バッチスクリプトmake_lib.cmdを作業ディレクトリ(win¥delayload)に置きます。
このディレクトリでシェルを開き、VC環境をセットアップします(vcvars32.batを実行)。スクリプトを正しく実行するには、パスの通った場所にlib.exeツールが必要だからです。そして、次のようにスクリプトを実行します。
これで、すべてのオブジェクトを取り込んだ新しい静的ライブラリmysqld-test-static.libができあがります。
実は、この時点でプラグインを使用することも不可能ではありません。この静的ライブラリファイルには必要な関数がすべて取り込まれているので、これにリンクすれば事足りるからです。しかしそれでは、.dllファイルのサイズがmysqld.exeと同じかそれ以上になってしまい、ナンセンスです。そこで、次の手順でこれをDLLに変換し、リンカー用のインポートライブラリを生成して、遅延読み込みを指定できるようにします。.dllファイルを作成することはできませんが、インポートライブラリを作成すればうまくいくはずであり、それで十分です。
2. 静的ライブラリをDLLに変換する
次は、静的ライブラリをDLLに変換する作業です。これはかなり込み入った作業です。MSVCのデフォルトのツール群では、すべてのシンボルをエクスポートするDLLを自動的に作成するのは不可能です。そこで、再びちょっとしたヘルパースクリプトの力を借りる必要があります。このスクリプトは、Grigore Stefan氏が作成した変換スクリプトを一部借用しています。氏のオリジナルのスクリプトはhttp://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=496762&SiteID=1にあります。
ただし、MySQLのインポートライブラリ作成にあたっては、スクリプトに修正が必要でした。元のスクリプトは非常に速度が遅かったうえ、データエクスポート用に有効な.defファイルが作成されませんでした。マークが行われず、エクスポートされた各シンボルがすべてインポートライブラリ用の関数となっていたのですが、それでは誤りです。
エクスポート用の正しい.defファイルを作成するには、2つのGPLプログラムの力を借りる必要があります。1つはMinGWのdlltool、もう1つはGNUのストリームエディタsedです。dlltoolでは、先に作成した静的ライブラリのすべてのエクスポートを含む有効な.defファイルを作成できます。またsedでは、作成される.defファイルに含まれるいくつかのエラーを修正できます。詳細については、バッチスクリプトを参照してください。このdlltool.exeとsed.exeを、パスの通った場所に置きます。これらのツールをお持ちでない場合は、ダウンロードのうえ、作業ディレクトリに展開しておいてください。Visual Cのビルド環境を既にセットアップ済みのシェルで、次のように実行します。
これにはかなりの時間がかかることがありますが、すべてうまくいった場合には、インポートライブラリmysqld.libができあがります。
プラグインのセットアップ
3. プラグインに遅延ローダーを追加しリンカーに動的リンクを指定する
次に、ストレージエンジンプラグインの遅延ローダーのコードで、存在しないmysqld.dllを遅延読み込みできるようにします。そのためには、mysqld.dllを使うようリンカーに指示する必要があります。MySQLではcmakeビルドシステムを使用するので、CMakeLists.txtファイルを適切に編集します。以下の行を追加し、適宜編集を加えます。
SET(DELAY_LDR_DIR ../../win/delayload)
SET(DELAY_LOADER ${DELAY_LDR_DIR}/mysqld ${DELAY_LDR_DIR}/mapdelayldr)
SET_TARGET_PROPERTIES _
(your_storage_engine PROPERTIES LINK_FLAGS "/DELAYLOAD:mysqld.dll")
TARGET_LINK_LIBRARIES(your_storage_engine ${DELAY_LOADER})
遅延ローダーについて若干補足しておきます。
遅延ローダーは、ホストプロセス(この例ではmysqld.exe)のディレクトリの.mapファイルをハッシュテーブルに読み込みます。そして、このテーブルを使用してアドレスを解決します。しかし、遅延ローダーのモジュールには、ちょっとした問題点があります。
実は、厄介なことに、VC6のリンカーとVC7以降のリンカーでは、遅延ローダーに違いがあるのです。というのも、64ビットアーキテクチャが登場したときに、遅延ローダーのデザイナが、ImgDelayDescrでポインタではなくRVAを使うべきだったことに気付いたからです。したがって、適切なリンクのコードを使用して、両方のバージョンのリンカーに対応する必要があります。Visual Studioのバージョンによって、ヘッダーファイルは異なります。遅延ローダーのコードは、この点に対応して、正しいバージョンでコンパイルされるようになっています。この遅延ローダーはVC6のリンカーでのみテストしました。新しいバージョンの遅延ローダーで動作する保証はありませんが、理屈上は正しく動くはずです。
4. リンクする
次に、cmakeでmakefileを作成し、リンクしてみましょう。私はその作業にnmakeを使用しているので、MySQLのメインディレクトリでcmake . -G "NMake Makefiles"を実行し、その後で、nmakeコマンドでプラグインをビルドします。でも、cmakeのターゲットが異なる場合でも、うまくいくはずです。
プラグインによっては、未解決の外部シンボルが原因でリンカーエラーが生じることがあります。これは通常、プラグインで使用しているグローバルデータ変数を示しています。たとえば、私が最初にプラグインをリンクしたときには、次のような未解決シンボルのエラーが表示されました。
ha_storage.obj : error LNK2001: Nichtaufgeloestes externes Symbol _
"class Bitmap<64> const key_map_empty" _
(?key_map_empty@@3V?$Bitmap@CODE_REPLACEMENT 4EA@@@B)
ha_storage.obj : error LNK2001: Nichtaufgeloestes externes Symbol _
_my_charset_bin
ha_storage.obj : error LNK2001: Nichtaufgeloestes externes Symbol _
"struct charset_
info_st * system_charset_info" _
(?system_charset_info@@3PAUcharset_info_st@@A)
storage.dll : fatal error LNK1120: 3 unaufgeloeste externe Verweise
5. 外部データシンボルを手動で解決する
残念ながら、このリンカーが遅延読み込みできるのは関数だけで、データシンボルは読み込めません。そこで、ちょっとした裏技が必要になります。
どうにかして、遅延ローダーが解決できるようなポインタを用意し、これらのポインタがプラグインで逆参照されるようにしなければなりません。基本的には、シンボルをポインタに変換することが必要で、そのポインタを使用するときには必ずポインタを逆参照して正しいメモリ位置を参照するようにします。このようなシンボルはMySQLのヘッダーファイルで定義されることもあるので、一連の処理は透過的な方法で行わなければなりません。幸い、このからくりは、プリプロセッサを使用して実現できます。方法は次のとおりです。
まずは、オブジェクトへのすべての参照をポインタに変換し、逆参照する必要があります。そこで、そのための#defineをソースの冒頭部分に記述します(これはMySQLのヘッダーをインクルードする前に行います)。私の例の場合、次のような記述になります。
#ifdef WIN32
#define system_charset_info *ppsystem_charset_info
#define my_charset_bin *pmy_charset_bin
#endif
次に、必要なMySQLのヘッダーをインクルードします。
次に、不足しているシンボルの宣言をMySQLのソースコード内で探し、プラグインファイルに記述します(この時点でプリプロセッサの処理は完了します)。
#ifdef WIN32
const key_map key_map_empty(0);
CHARSET_INFO *system_charset_info;
CHARSET_INFO my_charset_bin;
#endif
key_map_emptyは常に、ここで宣言したように宣言されるので、元のシンボルを参照する必要はありません。しかし、他のエクスポートについては、これを同様に行います。この時点で、関数ポインタの宣言は済んでおり、使用時に逆参照されます。もちろん、この段階ではポインタはまだ無効です。したがって、モジュールの初期化時にポインタを初期化する必要があります。そのためには、遅延ローダーの動作について知っている必要があります。ここでは、そのための関数を2つ宣言します。1つは_LoadMyMapfileで、もう1つはGetMapProcAddressです。
_LoadMyMapfileが行うのは、mysqld.mapファイルを調査および解析し、すべての関数および対応するアドレスを、高速に検索できる内部ハッシュテーブルに追加するという処理です。この関数の戻り値は、モジュールハンドルを示すHMODULEです。GetMapProcAddressで個別の関数の正しいアドレスを取得するときには、そのハンドルを使います。_LoadMyMapfileの戻り値が0の場合は、エラーが発生した(.mapファイルが見つからなかった)という意味です。HMODULEは常にモジュールの読み込みアドレスで、取得するハンドルはmysqld.exeのハンドルなので、個別のインポート関数について、正しく再配置されたアドレスを判断できます。
簡単にまとめると、まず必要なのは、遅延ローダーで2つの関数を次のように宣言することです。
#ifdef WIN32
extern "C" HMODULE _LoadMyMapfile(void);
extern "C" FARPROC GetMapProcAddress (HANDLE hModule, _
const char *szImport);
#endif
次に、プラグインのinit_funcで、リンカーでエラーとなったすべての外部シンボルを解決します。前述のエラーメッセージでは、妙な記号の付いた名前が示されていました。その名前をここで使います。今回の例では、プラグインの初期化関数に次のコードを追加します。
#ifdef WIN32
HMODULE hMod = _LoadMyMapfile();
if (!hMod) DBUG_RETURN(1);>
pmy_charset_bin = (CHARSET_INFO*)GetMapProcAddress(hMod, _
"my_charset_bin");
ppsystem_charset_info = (CHARSET_INFO**)GetMapProcAddress(hMod, _
"?system_charset_info@@3PAUcharset_info_st@@A");
#endif
注
ここでは、前述の#defineで使用した関数名が必要です。プリプロセッサが書き込む逆参照バージョンは必要ないからです。また、シンボルの型のポインタにキャストする必要がある点にも注意してください(ポインタを追加)。
以上の面倒な手順が完了したら、ストレージエンジンを再度リンクしてみてください。今度はきちんとリンクできるはずです。
おつかれさまでした。これで、動的読み込みが可能なストレージエンジンプラグインの完成です。
補足
この記事で紹介した手法は、MySQLがWin32用のプラグインAPIに修正を加えるまでの回避策にすぎません。MySQLは、MySQLdのすべての機能をDLLに組み込んで、mysqld.exeはその単なるローダーにしてくれればよいのになあと思います。そうすれば、プラグインはそのDLLを使えるようになって、この記事の回避策は無用になります。
8歳でプログラミングを始める。最初はCommodore 64のBASICで、後にIBM PCに移行。アセンブラとCを愛好し、システムレベルプログラミングへの造詣を深めた。現在は、オーストリアの医療ソフトウェア企業でUNIXのCプログラマとして勤務。システムプログラミング全般に深い興味を持ち、特にIntel x86プラットフォーム上のDOS/Win32/UNIXを専門とする。卒業論文のテーマはWin32でのルートキットの検出と駆除。.NETやJavaのようにリソースを大きく消費するソフトウェア開発ツールはあまり好きではなく、C++よりCを好む。システムリソースは無駄にせず大事にするべきだというのが持論。