> Home > その他 > 備忘録的メモ > その他の事 > 「Digital Mars C/C++ で小さいDLLを作る、又は関数の呼び出し規約に縛られないラッパーDLLを作る」

何で必要になったか

なんだってこんな変な物を作ることにしたかと言いますと。
このサイトで公開している「日本語インクリメンタルサーチするマクロ」という物がありまして、これは香り屋さんの「C/Migemo」というDLLを使っています。
このDLLは1.2系まで各関数を "__cdecl" でエクスポートしていたのですが、1.3系から標準の "__stdcall" を使うようになりました。
でも配布条件や辞書ファイルのサイズの問題でDLLを同梱していないため、こちら側からはバージョンの指定が出来ませんでした。

そこで仕方がなく複数のバージョンのDLLで動作するよう改造することにしました。
以下はそのメモです。
※作る過程で"Digital Mars C/C++ で小さいDLLを作る"方法も習得しました。

どうすれば呼び出し規約の違うDLLを差し替えて使えるようになるか

いきなり詰まってしまいました。
真っ先に思いついたのはDLLから GetProcAddress する前にそのDLLのバージョンを確認して、そのバージョンにあった呼び出し規約を切り替える方法です。
が、このバージョン確認が曲者です。
    方法
  • DLLファイルのリソースからバージョンを取得
  • DLLファイルのファイルサイズ/CRCなどとバージョンの対応表を作る
  • ユーザに指定してもらう
が考えられますが、一つめはリソースが埋まっていない場合があり断念。
二つめは数が膨大で面倒な上に1.3系は 05/07/25 現在ソースコードのみで提供されていてユーザが各自でコンパイルするようになっているため、コンパイラ、オプションなどの違いで対応表は不完全な物しかできない。
最後はかなり有望です。実際後で分かりましたが、同じようなマクロを作っていらっしゃるBouzmanさんのマクロはそうしているようです。
でもユーザが指定を間違えると……。
こういったバージョンを判定するタイプでは判定を間違えると秀丸ごと違反で落ちるので出来れば避けることにしました。
かといって「DLLファイルを解析して呼び出し規約を調べる」(爆)なんて私の力量では無理&これしきのことに持ちだすような方法ではない(笑)ですので こんなキワモノを含め残った選択肢(私の考えついた)は...
    実際にどっちの呼び出し規約を使用しているのか調べる……
  • DLLファイルを解析して呼び出し規約を調べる
  • 特攻する別exeを作って生き残った方で呼び出す(笑)
    呼び出し規約の違いでスタックが破壊されるなら……
  • インラインアセンブラで自前でスタック調整
  • スタックが狂っても大丈夫なようにvolatileな自動変数を置いて緩衝材にする
まぁ上二つはあまりに変態的で却下ですね!
そんなわけでスタックの調整にまわることにしました。
で、どちらがましか、ってことになりますが……。まぁ普通ならアセンブラを選択しますよね。
こちら(VC++超初心者のホームページさんの所のの掲示板です)で相談したところ雛形まで示していただきました。
その節は、大変お世話になりました。 m(_ _)m というわけで
unsigned long reg_esp;
asm { mov reg_esp, esp }
// 関数呼び出し
asm { mov esp, reg_esp }
て雰囲気で行ってみることになりました。

実装?

上記の掲示板で教えていただいたのですが、インラインアセンブラで毎回スタックを書き戻す方法だとコンパイルオプションなどによっては、最適化されてしまってまずい事になる可能性があるようです。
そこで、困りました。
  • 将来にわたってコンパイルオプションに気を配り続ける
  • コンパイルするごとにディスアセンブルして確認する
  • フルアセンブラ
  • 問題のある部分をDLLとして切り分ける
はい。迷わず最後を選びました。
そんなわけでmigemo.dllを呼び出す、「呼び出し規約」に左右されないラッパーDLLを作ることになりました。

実装!

コンセプトとしては今まで migemo.dll を直接呼び出していたソースを極力変更しないようにすることでしたので、今まで migemo.dll で使用していた関数を "__cdecl" で同じ名称/形式で エクスポートすることにしました。

で、実際のコードです。
#include <windows.h>
FARPROC funcs[6] ={NULL,NULL,NULL,NULL,NULL,NULL};
typedef int (*MIGEMO_PROC_INT2CHAR)(unsigned intunsigned char*);
typedef struct _migemo migemo;

typedef migemo *(*M_Func_Open)(char*);
typedef int (*M_Func_Load)(migemo*,int,const char*);
typedef unsigned char*(*M_Func_Query)(migemo*,const unsigned char*);
typedef void (*M_Func_Release)(migemo*,const unsigned char*);
typedef void (*M_Func_Close)(migemo*);
typedef void (*M_Func_Setproc_Int2Char)(migemo* object, MIGEMO_PROC_INT2CHAR proc);
HINSTANCE hInstMigemo = NULL;
extern "C" volatile migemo* migemo_open(char* dict)
{
    migemo* ret;
    unsigned long reg_esp;
    asm { mov reg_esp, esp }
    ret = ((M_Func_Open)funcs[0])(dict);
    asm { mov esp, reg_esp }
    return ret;
}
extern "C" volatile int migemo_load(migemo* obj, int dict_id, char* dict_file)
{
    int ret;
    unsigned long reg_esp;
    asm { mov reg_esp, esp }
    ret = ((M_Func_Load)funcs[1])(obj ,dict_id , dict_file);
    asm { mov esp, reg_esp }
    return ret;
}
extern "C" volatile unsigned char* migemo_query(migemo* object, unsigned char* query)
{
    unsigned char* ret;
    unsigned long reg_esp;
    asm { mov reg_esp, esp }
    ret = ((M_Func_Query)funcs[2])(object,query);
    asm { mov esp, reg_esp }
    return ret;
}
extern "C" volatile void migemo_release(migemo* object, unsigned char* string)
{
    unsigned long reg_esp;
    asm { mov reg_esp, esp }
    ((M_Func_Release)funcs[3])(object,string);
    asm { mov esp, reg_esp }
}
extern "C" volatile void migemo_close(migemo* object)
{
    unsigned long reg_esp;
    asm { mov reg_esp, esp }
    ((M_Func_Close)funcs[4])(object);
    asm { mov esp, reg_esp }
}
extern "C" volatile void migemo_setproc_int2char(migemo* object, MIGEMO_PROC_INT2CHAR proc)
{
    unsigned long reg_esp;
    asm { mov reg_esp, esp }
    ((M_Func_Setproc_Int2Char)funcs[5])(object , proc);
    asm { mov esp, reg_esp }
}

extern "C" int GetFuncs(const char* dllpath)
{
    if(hInstMigemo!=NULL)return false;
    if((hInstMigemo = LoadLibrary(dllpath)) == NULL){
        return false;
    }
    funcs[0] = GetProcAddress(hInstMigemo,"migemo_open");
    funcs[1] = GetProcAddress(hInstMigemo,"migemo_load");
    funcs[2] = GetProcAddress(hInstMigemo,"migemo_query");
    funcs[3] = GetProcAddress(hInstMigemo,"migemo_release");
    funcs[4] = GetProcAddress(hInstMigemo,"migemo_close");
    funcs[5] = GetProcAddress(hInstMigemo,"migemo_setproc_int2char");
    for(int i=0;i<=5;++i)if(funcs[i] == NULL)return false;
    return true;
}
int ReleaseFuncs()
{
    FreeLibrary(hInstMigemo);
    hInstMigemo=NULL;
    return true;
}


extern "C" BOOL APIENTRY DllEntryPoint(HINSTANCE, DWORD reason, LPVOID)
{
    if(reason == DLL_PROCESS_DETACH)ReleaseFuncs();
    return true;
}

いろいろと手抜きをしていますが、早い話が、 GetFuncs って関数に migemo.dll のパスを渡して LoadLibrary 下直後に呼び出してやれば、後は migemo.dll と同じにすればいいようになっています。
DLLを作ったことのある方はお気づきでしょうが、普通あるはずの「DllMain」関数がなく同じような形式で、「DllEntryPoint」なんて関数が定義されています。
これは私の使ったコンパイラのせいですのでとりあえずこの場は同じ物と考えてください。

DLLのサイズを小さく小さく


このDLLはコンセプトからしてインラインアセンブラを使います。
しかし、私が普段使っているBorland C++ Compilerは 無償版のため、インラインアセンブラを使用するために必要な Turbo Assembler が、付属していません(有償です)。
そこでもう一つ使うことのある、インラインアセンブラを使えるDigital Marsのコンパイラを使うことにしました。
(BCC+NASMってのも考えたのですが、無理みたいでしたので)
そんなわけで DMC を使うことになりましたが、普通に作ってコンパイルしたところ、25K位になってしまいました。
まぁそんなに気にすることではなかったのですが、単なるラッパDLLのくせにサイズがでかいな~……小さくしてしまえ!
て事で小さくすることにしました。(そう何度も作り直す物ではない予定ですし)

最適化オプションいじったり、構造化例外処理省いたりといつもの行動に出ましたが、いつも通り大差なし(笑)
よく考えたらC標準関数を使う必要ないので標準ライブラリを省けるなぁ。
有名どころの
かずぼんのホームページ実行ファイルのサイズを小さくする
VC++の使い方軽い実行可能ファイルの作り方
Hiro Software FactoryVisual C++の小技
辺りを参考にやってみるべ~かな?となりました。
上の方々の対象にされているのはVC++で、一部BCCのEXEでした。
さて、 DMC です。デフォルトライブラリを使用しないようにするには……。と考えて先ずはコマンドラインオプションを調べました。
DMC に引数付けないで実行するとコンソールにオプション一覧を吐き出します。
さて……。
-NL no default library
だそうで、さっそく「-NL」オプションを付けてみました。
Error 42: Symbol Undefined __acrtused_dll
あぎゃ……。
とりあえず空の関数を定義……ダメ?。
調べてみる……。
分かった。「DllMain」って関数があると自動的に上記関数をリンクしたがる見たい。
そこに書いてあった衝撃の事実……C++でWindows用にコンパイルするなら「-NL」オプションは使うな。
なんですと!!
悔しいので無視してみる(笑)
__acrtused_dll をシンボルとして持ってればいいだよな。。.defファイルのEXPORTS に
__acrtused_dll = DllMain
なんて書いてみる
リンクできた……。テストしてみよう……、何だか動いてる……。
……はっ!! FreeLibrary できてねぇ!!__acrtused_dllはエントリーポイントでないのか!
しかもDLLとして__acrtused_dllを解放している。DllMainでなくても危ないし不格好だ。
DllMain関数があると自動的にアウトなんだから、思い切って DllMain なくしちゃえ。
FreeLibrary はそれ用の関数を別に作って、解放前にそれを必ず呼ぶようにしよう。
リンクできた……。テストしてみよう……、何だか動いてる……。
くっ……!不格好過ぎる!!
しかもエントリーポイントあるのかないのか不明で恐いなぁ……。
あ、エントリーポイントを指定できればいいのか。
さっきのDMCのオプション一覧を見てみる。
ない……。
ん?まてよ、リンカか?
bin フォルダからリンカを探す。
「link.exe」ヘルプ探すの億劫だから引数付けないで実行。
げ、プロンプトモード(?)になっちゃった。
しょうがないので、-h
いっぱい出てくる。
探す
EN[try]
これか?
不親切だなぁ……。
DMC の方からリンカへオプション渡すには -L か。
DllMain を分かりやすいように改名「DllEntryPoint」にする。
-L/EN:DllEntryPointを渡す
おお!ばっちりか?
テスト……OK。
ちゃんと「DLL_PROCESS_DETACH」時に「DllEntryPoint」に来るかテスト。
ちゃんと来た!!
完成です!!
サイズ……3.52Kバイト
ちっちゃい!!!ちっちゃい!!!ちっちゃい!!!
ちっちゃい!!!ちっちゃい!!!
ちっちゃい!!!
……
…………
……ふぅ……。冷静に考えると、今時大したこっちゃないなぁ……。
まぁ楽しかったのでよしとしましょう。

最後に

まぁそんなこんなで
  • Digital Mars C/C++ Compiler で小さいDLLを作る
  • 関数の呼び出し規約に縛られないラッパーDLLを作る
の二つをそれぞれ、
  • 最適化オプションとデフォルトライブラリ(ランタイムライブラリ)をリンクしない
  • インラインアセンブラでスタックを調整する
で達成しました。