第V部〜マクロから呼び出すDLLの作り方 |
●目次
【0】 イントロダクション
【1】 用意するもの
【2】 マクロから呼び出せるDLL関数を作るには?
【3】 とりあえず "hello world"
【4】 ちょっと手を加えてみる
【5】 もう少し複雑なものを作ってみる
【6】 「ファイルを開く」ダイアログを出してみる
【7】 ウィンドウを表示してみる
【8】 作成したDLLのデバッグ
【9】 まとめ
【10】 リンク等
【11】 補足〜浮動小数点数版秀丸用のDLLについて
【0】 イントロダクション
このQ&A集でも至る所で紹介されている秀丸の強力なマクロ機能ですが、 その中でもある意味
もっともブッ飛んだ
ものが、今から解説する 「DLLの関数を利用する機能」でしょう。 DLLは「Dynamic Link Library」の略語、 日本語では「動的結合ライブラリ」となりますが、 これは関数やサブルーチンを必要がある時にメモリにロードして、 実行中のアプリケーションから呼び出せるようにするための仕組みです。 で、このDLLのロード&関数の呼出しをマクロ中からできるわけですが、 自分でDLLを作ってこの機能を利用すれば、 マクロの命令のここが気に入らないとかこんなのが欲しいとか、 あれこれ要望して実現されるのをひたすら待っていた(または諦めていた)
そんな後ろ向きな自分にサヨナラ(笑)
できるわけです。 もっとも、 秀丸のウィンドウや編集中のテキストの内容を直接いじれるわけではないですが、 それでも初期状態のマクロには用意されていない 高度なファイル操作や文字列操作、 自分で好きなようにデザインしたダイアログを表示したり… といったことが自由にできるようになるわけですから、
これはもうDLL作るしかない
と思わなきゃウソです(笑)。 まぁとはいえ、 思ったからすぐDLLが作れるほど世の中そう甘くはないわけですが、 やりもしないで諦めてしまうのはもったいない、 と言える程度に割とあっさりと作れてしまうのもまた事実です。 この解説では秀丸マクロ用のDLLを作る際に必要な環境と、 作成時に注意すべき点をいくつか挙げていますが、 それさえクリアすれば
貴方でも明日とはいわず今日からでも始められます!
ってなもんです(怪しげな英会話の教材のキャッチコピーみたいでアレですが(笑))。
【1】 用意するもの
まず、この解説はDLLの作成の話が中心なので、 使用する言語(ここでは C または C++ 言語)についての知識は 簡単なツールやサンプルアプリケーションを作れる程度に持っている として話を進めていきます。 したがって、
ポインタって何ですか?
とか
演算子のオーバーロードってどうやるんですか?
といったことで悩んでいる方は、 この解説より先にまず言語自体の参考書を見て勉強してください。次に、DLLを作るためにはもちろんDLLが作成可能な 開発環境(コンパイラetc.)が必要ですが、 売品なら Microsoft の Visual C++ や Borland の C++ Builder 等、 その他にもフリーソフトウェアのコンパイラがいくつかあります。 その中で Web 等から現在入手可能で比較的有名なものは以下の通りです。
- Cygwin 環境に含まれる GNU gcc コンパイラ (及び binutils に含まれる dlltool / dllwrap)
- MinGW (Minimalist GNU For Windows) 環境に含まれる GNU gcc コンパイラ (及び 同上)
- Borland C++ Compiler 5.5 (コンパイラその他のツールのみ)
これらの入手先についてはこちらをご覧ください。 また、これらは基本的にはコマンドライン上で使うツールなわけですが、 それらをウィンドウ環境で利用するための GUI ソフトウェア(いわゆる統合開発環境)も いくつか公開されているようです。 それらの環境の上でDLLを作成する方法については それぞれのマニュアルを参照してください。 ここでは、コマンドラインから直接DLLを作る時の コマンドラインオプションのみを説明します。
なお、いずれかの方法で作成したDLLの関数が アプリケーションからちゃんと呼び出せるかどうかは、 Windows に付属してくるクイックビューアを利用すると確認できます (エクスプローラ等でDLLファイルを右クリックしてでてくる コンテキストメニューの「クイックビューア」を選ぶ)。 その中の「Export Table」の下の方の表が関数の一覧ですが、 その最も右側の列がDLL関数を呼び出す時の名前です。 この「Export Table」が見つからない場合はDLLの作成に失敗しているので、 作成手順や DEF ファイルの内容を確認してからやり直してみて下さい。
- Cygwin gcc の場合:
この場合、作成するDLLが Cygwin 環境 (正確には
cygwin1.dll
) を必要とするかしないかを-mno-cygwin
オプションの有無で選択できます。 (-mno-cygwin
をつけるとcygwin1.dll
の代わりにmsvcrt.dll
を使うようになります。)まず、アプリケーション等から呼び出せるようにする(=エクスポートする) 関数に、(後に説明する)
__declspec(dllexport)
宣言子をつけておけば、 以下のように直接 gcc を使うのがもっとも簡単です。
gcc -shared -mwindows -o foo.dll foo.cpp
(Cygwin 環境下で動作)
gcc -shared -mwindows -mno-cygwin -o foo.dll foo.cpp
(Cygwin 環境は不要)
__declspec(dllexport)
宣言子を使わない場合は 以下のような DEF ファイルが別途必要になります。
; gcc の場合の DEF ファイルの例:
; foo(), bar(), baz() 関数をエクスポートする場合
EXPORTS
foo
bar
baz
このファイルを
foo.def
という名前で ソースファイルと同じディレクトリに置き、 以下のようなコマンドを実行します。
dllwrap -mwindows --def foo.def -o foo.dll foo.c
(cygwin 環境下で動作)
dllwrap --driver-flag='-shared' -mno-cygwin -mwindows --def foo.def -o foo.dll foo.c
(cygwin 環境は不要)
- MinGW gcc の場合:
この場合は Cygwin gcc とは異なり、 常に
msvcrt.dll
を利用するDLLができますが、__declspec(dllexport)
宣言子をつけても 無視されているようなので、 常に DEF ファイルが必要になります(内容は上の物と同じ)。
dllwrap -mwindows --def foo.def -o foo.dll foo.c
- Borland C++ Compiler の場合:
この場合、 DEF ファイルがないと 関数の名前に下線(アンダースコア)をつけたものが エクスポートされる関数の名前になるので、 下線がない名前でも呼び出せるようにするため 以下のような DEF ファイルを用意します (
__declspec(dllexport)
宣言子はあってもなくてもよい)。
; Borland C++ Compiler の DEF ファイルの例:
; foo(), bar(), baz() 関数をエクスポートする場合
EXPORTS
foo=_foo
bar=_bar
baz=_baz
コマンドラインは以下のようなシンプルなものでOKです。
BCC32 -WD -efoo.dll foo.cpp
(foo.def
は同じフォルダに置くだけでよい)
【2】 マクロから呼び出せるDLL関数を作るには?
前章でDLLを作成するための一般的な方法について説明しましたが、 秀丸マクロでは一般的なDLL全てを扱えるわけではありません。 マクロから呼び出せるDLL内の関数(以下「DLL関数」)は、 以下の性質を持つものだけです。
1番目と2番目の性質は、秀丸マクロで扱えるデータ型が 数値 (
- 引数の型は
void
(引数を持たない) か、int
※ (数値) とconst char*
(文字列) の組み合わせのみ- 戻り値の型は
int
※ (数値) またはconst char*
(文字列) のみ- 呼出し規約は
__cdecl
(通常のC関数) のみ
※ 浮動小数点数版秀丸の場合は int
ではなくdouble
になります。 浮動小数点数版秀丸についてはこちらをご覧ください。int
) と文字列 (char*
) しかないことから当然の事といえます。 なお、文字列型のconst
修飾子はなくても DLL関数の宣言としては支障ありませんが、 かといって、わざとconst
をつけずに 引数として渡された文字列の内容を書き換えてはいけません (というか書き換えても無意味)。最後の性質はちょっとわかり難いかもしれませんが、 Win32API でいうと
WINAPI
宣言子 (実際は__stdcall
の #define) がついていないもの、もしくはWINAPIV
宣言子 (同じく__cdecl
の #define)がつくものです。 とはいえ、__cdecl
や__stdcall
宣言子を つけなかった場合のデフォルトは__cdecl
なので、 特に何もつけなければOK!ということになります。以上の事を踏まえて、 秀丸マクロから使用可能なDLL関数のプロトタイプ宣言を書き下すと 以下のようになります。
C言語の場合
ここで、param_list や return_type は先に説明した 引数リストまたは戻り値を表します。 また、func_name は関数の名前ですが、 C++ 言語の場合は※の注にあるように「
__declspec(dllexport) return_type __cdecl func_name(param_list);
C++ 言語の場合
extern "C" __declspec(dllexport) return_type __cdecl func_name(param_list);
※「extern "C"
」は 関数名 func_name がマングルされるのを抑制するために必要。 「名前マングル」の意味については適当な C++ の教科書を参照してください。extern "C"
」を つけておかないと、 後からDLL関数を呼び出す時に func_name という ソースコード中での名前で参照できなくなるので注意してください。
__declspec(dllexport)
は DLL関数として呼び出せる関数につける宣言子ですが、 いちいちこんな長ったらしい宣言子を書くのも面倒なので、 以下の各章のサンプルのためにあらかじめ 次のようなマクロ(HIDEMARU_MACRO_DLL
)を定義しておきます。
…って、何か C 言語の場合は宣言子の方が短いような気がしますが、
#ifdef __cplusplus /* C++ 言語の場合 */ #define HIDEMARU_MACRO_DLL extern "C" __declspec(dllexport) #else /* __cplusplus */ /* C 言語の場合 */ #define HIDEMARU_MACRO_DLL __declspec(dllexport)#endif /* __cplusplus */
目の錯覚
なので気にしない様に(笑)。さて、DLLには上で説明したDLL関数や、 それらから呼ばれるだけのヘルパー関数の他に、 DLLエントリポイント関数というものを入れることができます (必須ではないことに注意!!)。 DLLエントリポイント関数は、 あるプロセスがDLLをロードした時に必要となるDLLの内部変数の初期化、 またはアンロードされた時の後始末を行う目的で使用されます。 デフォルトでは
DllMain
という名前の関数が エントリポイント関数になるので、 以下の説明でも一貫してDllMain
という名前を使うことにします。 そのDllMain
関数のプロトタイプ宣言と、DllMain()
が呼び出された時の状況に応じた処理を行う、 定型処理だけを書いたコードは以下の通りです。
上のサンプルにもコメントしてありますが、 C++ を使う場合はくれぐれも忘れずに
#include <windows.h> /* これを忘れると gcc でコンパイルした時に DllMain が実行されない */ #ifdef __cplusplus extern "C" { #endif /* * DLLエントリポイント関数の型と典型的なスケルトンコード */ BOOL APIENTRY /* int __stdcall */ DllMain(HINSTANCE hInstance, DWORD ul_reason_for_call, LPVOID pParam) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: /* ここにグローバル変数等の初期化コードを書く */ /* ※loaddll でDLLをロードした時はここが実行される */ break; case DLL_PROCESS_DETACH: /* ここにグローバル変数等の後始末コードを書く */ /* ※freedll でDLLをアンロードした時はここが実行される */ break; case DLL_THREAD_ATTACH: /* ここにスレッド毎に必要な変数等の初期化コードを書く */ break; case DLL_THREAD_DETACH: /* ここにスレッド毎に必要な変数等の後始末コードを書く */ break; } return TRUE; } #ifdef __cplusplus } #endifextern "C"
で 名前マングルされない様にして下さい(特に gcc を使う場合)。 エントリポイント関数は必須ではないため、 これを忘れてもコンパイラは「DllMain()
が見つからない」と 注意してくれませんので…。 え?なんでこんなにしつこく注意するかって? それはですねぇ…
僕もこの解説のサンプルを作ってる時にやってしまったからです(笑)。
…とまぁ余談はともかく、以下の章でも必要に応じて適宜
DllMain
関数を使っていますので、 具体的なサンプルコードについてはそちらをご覧ください。
【3】 とりあえず "hello world!"
この手の解説をする時にはもはや定番、というか
これをやらないと解説をした気になれない
というネタですが、関数としては最も簡単な "hello world!" という文字列を返すだけの関数を作ってみます。 第2章で説明した宣言子マクロやDllMain
関数は同じ物を使って、 その後に以下のようなコードを付け足します。
この関数を含んだDLLを
/* hello_world.c */ HIDEMARU_MACRO_DLL /* __declspec(dllexport) */ LPCSTR hello(void) /* LPCSTR は const char* の typedef */ { return "hello world!"; }hello_world.dll
という名前で作成し、 秀丸(hidemaru.exe
)と同じか パスの通ったフォルダに置きます。 そして、以下のようなマクロファイルを作ってDLLをテストしてみましょう。
DLLの作成方法その他に間違いがなければ、 以下のような "
// テスト用マクロ loaddll "hello_world.dll"; // 作成したDLLのロード message dllfuncstr("hello"); // hello() の呼出し freedll; // DLLのアンロードhello world!
" というテキストが入った メッセージボックスが表示されるはずです。
この例は確かに単純ですが、一つ重要なことがあります。 この関数のように文字列、したがって
LPCSTR (= const char*)
を返す場合、 その文字列はDLL内部で静的に(またはヒープ領域に)確保したもので なければならない、ということです。 この注意はもちろんDLL関数に限ったことではありませんが、 C または C++ でプログラムを書いていれば
一生に一度は必ず通る道(但し間違い(笑))
なので、ここでも取り上げておきます。
まずは戻り値である文字列へのポインタが スタック上に確保した文字配列を指している、という例です。 スタック領域は関数内で一時的に使用する変数(ローカル変数)を置くための場所で、 スタック上に確保した変数はその関数から抜けたあとは無効になります。 で、その無効な変数(文字配列)を指すポインタを 関数の戻り値として返すのはもちろん間違いです。
そんなことは百も承知
と言ってる側から(笑)、例えば以下のようなコードを書いてしまうわけです。
行数が少ないので何だかわざとらしい例になってしまいましたが、 エディタの1画面に収まらないような関数を書いていて 途中で色々
/* hello2.c */ HIDEMARU_MACRO_DLL LPCSTR hello(void) { char buf[13]; /* スタックに配列を確保 */ lstrcpy(buf,"hello world!"); /* スタック上の配列に文字列をコピー */ return buf; /* おっとっと… */ }buf[]
の内容をあれこれいじっている内に、 関数の終わりの方ではbuf[]
をスタック上に確保していた事を すっかり忘れてしまい… というパターンで思わずやってしまうので注意が必要です。 まぁさすがにここまであからさまな間違いはしないかもしれませんが、 スタック上の配列を指すポインタ変数を別に用意して そのポインタで配列を操作していたりすると、 最後にそのポインタの値をそのまま返してしまうというミスは 結構ありがちなことだと思います。さて、この例のような間違いを犯したあと、
同じ過ちは2度とすまいと心に誓い
気持ちも新たにコーディングを始めた
30秒後
に書いてしまいがちなのが以下のようなコードです。
今度は
/* hello3.c */ #include <stdlib.h> HIDEMARU_MACRO_DLL LPCSTR hello(void) { char* buf = malloc(13); /* ヒープ領域にメモリを確保 */ lstrcpy(buf,"hello world!"); /* 確保したメモリに文字列をコピー */ return buf; /* おいおい、誰が free(buf) を呼ぶんだ? */ }buf
が指す先のメモリはヒープ上にありますから、 関数を抜けてもメモリの内容が破壊される心配はありません。 がしかし、このコードにはmalloc()
の戻り値を チェックしていないという問題もありますが、それ以前にmalloc()
で確保されたメモリが開放されないという、 いわゆるメモリリークの問題があります。このような間違いを犯してしまう時に考えている事はおそらく
静的な配列では扱える文字列の長さに制限が生じてしまうので、 できれば動的にメモリを確保して 扱える文字列の長さの制限を無くしたい
という事だと思います(*1)。 この件については、上記の例のようなコードでは確かにまずいのですが、DllMain()
関数を使えば、 メモリリークの心配をしなくてもよい方法で動的に ヒープ領域に確保したメモリへのポインタを返すことができます。 これについては次章で見ることにしましょう。
(*1) あと考えられる事としては、 静的に確保した配列だと他のプロセスが同じDLLを呼び出した時にまずくないか? という事だと思いますが、DLL中で確保した静的変数はプロセス毎に コピーが作られるのでその心配は無用です。 ただし、秀丸マクロからだけでなく 一般にマルチスレッドなプロセスでも使用されるDLLを作る場合は、 スレッドセーフなDLLにするために静的変数の使用には注意が必要です (スレッドローカル変数領域(TLS)を利用する等)。
【4】 ちょっと手を加えてみる
前章で作成したDLL関数に少し手を加えて、 "hello " の後に引数で渡された文字列をつなげたものを返すようにしてみましょう。 単純に書けば
となりますが、 この場合渡した文字列の 74 バイト目以降は切れてしまいます。 静的に確保したバッファのサイズを増やせばもう少し制限を緩めることができますが、 根本的に制限があることには変わりありません。 そこで、前章の最後で考えたように、 メモリをヒープ領域に動的に確保する方法で書いてみます。
/* hello_what.c */ HIDEMARU_MACRO_DLL LPCSTR hello_what(LPCSTR what) { static char buf[80]; /* 静的にバッファを確保 */ lstrcpy(buf, "hello "); /* "hello " 部分をコピー */ lstrcpyn(buf + 6, what, 74); /* 渡された文字列をコピー */ return buf; }
ここで、初期メモリの確保(
/* hello_what2.c */ #include <stdlib.h> LPSTR pszBuffer; /* ヒープ領域に確保するメモリへのポインタ */ int nBufSize; /* 確保したメモリのサイズ */ #define BUF_SIZE 16 /* バッファの初期サイズ */ /* DLLエントリポイント関数 */ BOOL APIENTRY DllMain(HINSTANCE hInstance, DWORD ul_reason_for_call, LPVOID pParam) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: /* loaddll された時に実行される */ nBufSize = BUF_SIZE; pszBuffer = malloc(BUF_SIZE); /* 初期サイズでバッファを確保 */ break; case DLL_PROCESS_DETACH: /* freedll された時に実行される */ free(pszBuffer); /* ヒープ領域に確保したメモリを開放 */ break; } return TRUE; } #define HEADER_SIZE 6 /* "hello " の文字数 */ /* "hello " + what を返す(エラーの時は "" を返す) */ HIDEMARU_MACRO_DLL LPCSTR hello_what(LPCSTR what) { int len; if (IsBadStringPtr(what,1)) return ""; /* 引数が無効 */ len = lstrlen(what) + HEADER_SIZE + 1; /* 戻り値に必要なメモリサイズ */ if (len > nBufSize) { /* 現在のバッファではサイズが足らないのでメモリを(再)確保 */ free(pszBuffer); /* 古いメモリを開放 */ pszBuffer = malloc(len); /* 新たにヒープ領域にメモリを確保 */ if (pszBuffer == NULL) return ""; /* no memory */ nBufSize = len; /* 確保したバッファのサイズを記憶 */ lstrcpy(pszBuffer, "hello "); /* ヘッダをコピー */ } /* "hello " の後ろに what の内容をコピー */ lstrcpy(pszBuffer + HEADER_SIZE, what); return pszBuffer; }DLL_PROCESS_ATTACH
)と 開放の後始末(DLL_PROCESS_DETACH
)をDllMain()
関数の中で行っています。 その他、hello_what()
関数の中でも メモリの開放と再確保を行っている部分がありますが、 これは現在確保されているメモリの量が足りない場合にのみ実行されます。 実際にこのDLLを以下のようなマクロでテストしてみると、のようにメモリの(再)確保と開放が行われますが、 最終的には
// テスト用マクロ loaddll "hello_what.dll"; // ヒープ領域にメモリが確保される // 初回の呼出し…文字数が少ないので最初に確保したメモリがそのまま使われる message dllfuncstr("hello_what","world!!"); // 現在のバッファのサイズより文字数が多いので新たにメモリを確保 message dllfuncstr("hello_what","の訳は「ハロー」です"); freedll; // 最後に確保したメモリはここで開放されるDllMain()
関数の後始末処理でメモリが開放されるので、 前章の最後で述べたメモリリークの問題は発生しません。
【5】 もう少し複雑なものを作ってみる
秀丸マクロから呼び出せる関数は__cdecl
宣言子を持つもの、 すなわち通常のC関数の呼出し規約に従うものに限る、 という話は第2章でしましたが、 これは裏を返せば、 可変個の引数を取るDLL関数を作ることができる、 ということでもあります。 なぜそうなのかの説明は他書に譲って、 ここでは可変個の引数を扱うための C の標準マクロの使い方を説明します。 というわけで、今度の例はC言語のライブラリ関数sprintf
関数に似た引数を取り、 結果の文字列を返すDLL関数です。まずは Win32API の
wvsprintf()
をそのまま使うバージョンですが、 これは以下のように簡単にできます。
これはこれで例としては十分ですが、
/* sprintf.c */ #include <stdarg.h> /* マクロ va_???() の定義 */ #define MAX_BUF 1024 /* バッファサイズ(もう少し大きい方がいい?) */ char buf[MAX_BUF]; /* バッファは静的に確保 */ HIDEMARU_MACRO_DLL LPCSTR sprintf(LPCSTR fmt, ...) { buf[0] = '\0'; /* 不正な引数の場合は "" を返す */ if (!IsBadStringPtr(fmt,1)) { va_list args; /* 可変個の引数を扱うためのデータ型 */ va_start(args,fmt); /* args を初期化する */ wvsprintf(buf,fmt,args); /* API に丸投げ(^^; */ va_end(args); /* パラメータのスキャンを終了 */ } return buf; }va_arg()
マクロの使い方の説明も少しはしておかないと
看板に偽りあり
てなことを言われそうなので、 複雑な書式指定はあっさり捨てて、"%d"
(10進整数)、"%x"
(16進整数) と"%s"
(文字列)、 それ以外はそのまま出力する、というルールのみをサポートした バージョンを作ってみましょう。
ちなみに、この章で作ったDLL関数は以下のように呼び出します。
/* sprintf2.c */ #include <stdarg.h> #define MAX_BUF 1024 char buf[MAX_BUF]; /* 本来なら文字列長がバッファサイズを越えない様に チェックをする必要があるが、今回は簡潔さのためにばっさり省略してある */ HIDEMARU_MACRO_DLL LPCSTR sprintf2(LPCSTR fmt, ...) { va_list args; /* 可変個の引数を扱うためのデータ型 */ LPSTR ptr = buf; /* バッファを埋めるためのポインタ */ LPCSTR str; int len; if (IsBadStringPtr(fmt,1)) return ""; /* 不正な引数の場合 */ va_start(args,fmt); /* args を fmt の次を指すように初期化する */ while (*fmt != '\0') { if (IsDBCSLeadByte(*fmt)) { /* 2バイト文字の場合 */ *ptr++ = *fmt++; *ptr++ = *fmt++; } else if (*fmt == '%') { /* 書式指定文字の場合 */ /* va_arg() マクロの第1引数に args (va_list)、 第2引数に変数型を指定すると、 指定した型の値を返し、 args が次のパラメータを指すように変更される。 なお、文字列への変換は wsprintf() に丸投げ(^^; */ switch (*++fmt) { case 'd': /* 10進整数 */ len = wsprintf(ptr, "%d", va_arg(args, int)); if (len < 2) return ""; ptr += len; break; case 'x': /* 16進整数 */ len = wsprintf(ptr, "%x", va_arg(args, int)); if (len < 2) return ""; ptr += len; break; case 's': /* 文字列 */ str = va_arg(args, LPCSTR); /* ポインタの正当性チェック */ if (IsBadStringPtr(str,1)) return ""; len = wsprintf(ptr, "%s", str); if (len < 2) return ""; ptr += len; break; default: /* その他の場合はそのままコピー */ *ptr++ = *fmt; } fmt++; } else { /* 普通の文字の場合 */ *ptr++ = *fmt++; } } *ptr = '\0'; /* NULL 文字で終端させる */ va_end(args); /* args を NULL にしてパラメータのスキャンを終了 */ return buf; }
// テストマクロ loaddll "sprintf.dll"; // 現在の tickcount 値を保存 #n = tickcount; // wvsprintf() 丸投げ版 message dllfuncstr("sprintf","現在の tick = 0x%08x",#n); // 自作版 message dllfuncstr("sprintf2","%s の場合は %d です","10進数",#n); freedll;ところで、今回の例のような
sprintf()
系の関数は、 最初のフォーマット指定文字列によって間接的にその後に続く引数の個数を 関数に伝えていますが、 引数の個数を伝えるにはもちろん他の方法も色々と考えられます。 一番簡単なのは、最初の数値引数でその後に続く引数の数を指定する、 例えば
のような方法でしょう。 または対象となる文字列の形式とは明らかに異なる文字列を最後に指定する、 例えば
/* n1 〜 nN の数値の最大値を返す */
int max(int N, int n1, ...);
のような方法などがあり得ると思います。 いずれにしても
/* destfile に srcfile 以下を追加する。引数の最後は "" で終わること!! */
int cat(LPCSTR destfile, LPCSTR srcfile, ...);va_is_end(args)
のような パラメータ指定の終了を判定するマクロは用意されていない(*2)ので、 何らかの形で引数が何個あるかの情報をDLL関数に伝える必要があります。
(*2) 別にライブラリの作者が手抜きをしているわけではなくて、 一般的なスタックの構造では そもそもパラメータ指定の終了位置を判定できないからです。 引数の話題が出たついでに、というか
実はこの章の本題
ですが、ここで重要な注意です。 DLL関数が可変個の引数を取るかどうかに関わらず、 マクロ側(一般に動的にロードされたDLLの関数の呼出し側)では 任意個のパラメータを任意の順番で渡すことができてしまいます。 普通の関数(または静的にロードされたDLLの関数)なら コンパイル時に引数の個数及び型チェックが行われますから、 間違ったパラメータを渡してしまったことを コンパイル時にコンパイラが注意してくれます。 が、動的にロードされたDLLの関数の場合は、 間違ったパラメータを渡しても誰からも何も注意されない、まさに
今時のガキンチョ
のようですが(笑)、 間違ったパラメータを渡された関数は下手をすれば暴走状態に陥ってしまいます。 というわけで、 自作したDLLを公開する時はその呼出し規約を ちゃんとドキュメントに書くのはもちろんのこと、 渡された引数が正当なものかどうか(*3)のチェックは きちんとするようにしましょう。 ちなみに、
某田○DLLもつい最近まで 文字列引数の正当性チェックを殆どしてなかったことはナイショです(笑)。
(*3) 文字列型の場合は IsBadStringPtr()
API で ポインタの値が正当なものかどうかだけはチェックできます。 ポインタが指す先の文字列の内容、 または整数型の場合に値が不正かどうかの判断は 貴方(DLL関数)自身でやるしかないです。
【6】 「ファイルを開く」ダイアログを出してみる
さて、ここで話題をがらっと変えて、 DLL関数の中でダイアログやウィンドウを出す方法について 解説していくことにします。まず比較的簡単にできるダイアログの表示から行きますが、 例として「ファイルを開く」ダイアログを出すDLL関数を作ってみましょう。 このダイアログを出す関数は既に Win32API に
GetOpenFileName()
という名前で用意されているので、 ここでも素直にそれを使うことにしましょう。 この API で出てくるダイアログはいわゆるモーダルダイアログという奴で、 API の呼出しでダイアログが表示され、 ダイアログが表示されている間のユーザーからの入力等の処理は 全て API 内で行われ、 最後にダイアログが閉じられた後に API から復帰します。 というわけで、DLL関数内でやるべきことは、結局の所
この API を単に呼び出すだけ(=ただの関数呼出し)
ということになります (もちろん API に渡すパラメータの準備や API から復帰した後の処理は必要ですが)。以下がそのサンプルソースですが、 この章から C++ 言語を使っているので注意してください。
API に渡すパラメータの設定が少しややこしいですが、 この辺は適宜 Win32API のヘルプを参照してください。 で、このDLL関数を呼び出すサンプルマクロは以下のようになります。
// getfilename.cpp HIDEMARU_MACRO_DLL // extern "C" __declspec(dllexport) LPCSTR getfilename(int hWnd, LPCSTR inidir, LPCSTR inifile) { static char buf[MAX_PATH]; // 今回は静的にバッファを確保 buf[0] = '\0'; // パラメータの正当性チェックをお忘れなく if (::IsBadStringPtr(inidir, 1) || ::IsBadStringPtr(inifile, 1)) { return buf; } if (!::IsWindow(reinterpret_cast<HWND>(hWnd))) hWnd = 0; // 初期ファイル名 lstrcpy(buf,inifile); // GetOpenFileName() に渡す OPENFILENAME 構造体の設定 OPENFILENAME ofn; ::ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = reinterpret_cast<HWND>(hWnd); ofn.lpstrFilter = "全てのファイル (*.*)\0*.*\0"; ofn.nFilterIndex = 1; ofn.lpstrFile = buf; ofn.nMaxFile = MAX_PATH; ofn.Flags = OFN_HIDEREADONLY|OFN_EXPLORER|OFN_NOCHANGEDIR; ofn.lpstrInitialDir = *inidir ? inidir : 0; // ダイアログの表示(キャンセルの場合は "" を返す) if (!::GetOpenFileName(&ofn)) buf[0] = '\0'; return buf; }このマクロを実行すると、 (ダイアログの細かい形はOSのバージョンで違いますが) 以下のようないつもの「ファイルを開く」ダイアログが表示されるはずです。
loaddll "getfilename.dll"; // 第1パラメータにはオーナーウィンドウのウィンドウハンドルを指定する message dllfuncstr("getfilename",hidemaruhandle(0),hidemarudir,"foo.txt"); freedll;
「色の選択」等の他のコモンダイアログを表示する API も 概ね上の
GetOpenFileName()
と同じように モーダルダイアログを表示するだけなので、 特に何も考えずに API を呼び出すだけでダイアログを出すことができます。
【7】 ウィンドウを表示してみる
前章ではOSがあらかじめ用意しているコモンダイアログを 表示する例を紹介しましたが、 自分で作成したダイアログやウィンドウを表示するにはどうすればよいのでしょうか? ダイアログの場合は(::DialogBox()
等の API を使って) モーダルダイアログとして表示すれば、 前章の例とあまり変わらない手順で表示することができます。 ですが、同じダイアログでもモードレスダイアログとして表示したい時や、 普通のウィンドウを表示したい時は、 もう少し手順を踏む必要があります。まず、以下のようなコードを試してみましょう。
このサンプルで行っていることは、 ユーザー定義のウィンドウクラスの登録をする場所が
// show_window.cpp WNDPROC wc; // ウィンドウクラスの登録とインスタンスハンドルの保持に使用 // ウィンドウのコールバック関数 // 殆ど何もしてない(終了処理だけ) HRESULT CALLBACK MyWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CLOSE: ::DestroyWindow(hWnd); break; case WM_DESTROY: ::PostQuitMessage(0); // この API 呼出しでメッセージループが終了 break; default: return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; } // extern "C" を忘れると gcc の場合に DllMain が呼び出されなくなる extern "C" BOOL APIENTRY DllMain(HINSTANCE hInstance, DWORD ul_reason_for_call, LPVOID) { // DLL のインスタンスハンドルをここで取得する switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // ウィンドウクラスの登録 ::ZeroMemory(&wc, sizeof(wc)); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)MyWindowProc; wc.hInstance = hInstance; // DLL のインスタンスハンドルを指定する wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.lpszClassName = "HidemaruMacroDLL_Sample"; return ::RegisterClass(&wc) != 0; case DLL_PROCESS_DETACH: // ウィンドウクラスの登録を削除 ::UnregisterClass(wc.lpszClassName, wc.hInstance); return TRUE; default: break; } return TRUE; } // ウィンドウを表示するDLL関数 HIDEMARU_MACRO_DLL int show_window(LPCSTR title) { // この関数の中で行っていることは普通のウィンドウアプリケーションが // WinMain() で行っている処理とほぼ同じ // (ウィンドウクラスの登録は DllMain() 内で行っているため不要) // ウィンドウの表示 HWND hWnd = ::CreateWindow(wc.lpszClassName, title, WS_OVERLAPPEDWINDOW | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, wc.hInstance, // DLL のインスタンスハンドル NULL); if (hWnd == NULL) return -1; ::ShowWindow(hWnd, SW_SHOW); // メッセージループを回す…って勝手にそんなことしてもいいの?? MSG msg; while (::GetMessage(&msg, NULL, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } return msg.wParam; }DllMain()
関数である事を除けば ほぼ普通のアプリケーションがWinMain()
関数等で 行っているウィンドウ表示のコードそのままです。 なお、今回のウィンドウにはメニューもアイコンもつけなかったので 必要なかったのですが、 それら(一般にはリソース)が必要な場合は別途作成し(*4)、 DLL作成時にリンクする必要があるので、 各処理系のマニュアルを参照して必要な処理を行ってください。
(*4) gcc の場合は binutils の windres
、 Borland C++ Compiler の場合はbrcc32
を使います。さて、このコードは実際に動作するわけですが、 このコードには一つまずい点があります。 それはDLL関数内で勝手にメッセージループを回していることです。 …と詳しい事を書き始めるとダラダラと長くなるだけなので、 ここでは単に
人様のプロセス内で勝手にメッセージループを回すのは仁義に反する
とだけ述べておきます(*5)。
(*5) 上のサンプルコードのように単純なメッセージループしか持たないプロセスなら 問題はないですが、 メッセージループの中で特殊な処理を行っているようなプロセスの場合だと、 その特殊な処理が行われなくなるため問題が発生する場合があります。 ところで上のサンプルで表示したウィンドウですが、 よく考えると
show_window()
関数から戻る時には ウィンドウは既に閉じられているわけですから、 マクロ内で開いているウィンドウに対して何か手を加えたり、 ウィンドウに入力された文字などをウィンドウの表示中に取ってきたり、 といった処理を行うことはできません。 要するに前章のコモンダイアログと同じような モーダルなウィンドウを作ったのと同じことです。 というわけで、次のサンプルではモードレスなウィンドウを作ること、 そしてDLL関数の呼出し元プロセス(正確にはスレッド)で メッセージループを回さないこと、 の両方を同時に解決する方法を紹介します。直前に述べた2つの問題を解決するには、 DLL関数を抜けた後もウィンドウを開いておき、 さらにそのウィンドウのメッセージを処理するための メッセージループを回し続けることが必要なわけですが、 これは以下のようにDLL関数内で新しくスレッドを生成して、 その中でメッセージループを回すようにすれば両方とも実現できます。
// show_window2.cpp WNDCLASS wc; // 表示するウィンドウのウィンドウクラス構造体 HANDLE hThread; // スレッドハンドル HWND hWndUser; // ウィンドウハンドル // ウィンドウのコールバック関数 HRESULT CALLBACK MyWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CLOSE: ::DestroyWindow(hWnd); break; case WM_DESTROY: ::PostQuitMessage(0); // この API 呼出しでスレッドのメッセージループが終了 break; default: return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; } // DLL エントリポイント関数 extern "C" BOOL APIENTRY DllMain(HINSTANCE hInstance, DWORD ul_reason_for_call, LPVOID pParam) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // wc の初期化と登録作業 ::ZeroMemory(&wc, sizeof(wc)); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)MyWindowProc; wc.hInstance = hInstance; // DLL のインスタンスハンドルを指定する wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.lpszClassName = "HidemaruMacroDLL_Sample"; return ::RegisterClass(&wc) != 0; case DLL_PROCESS_DETACH: // 後始末 if (hWndUser != NULL) { ::SendMessage(hWndUser,WM_CLOSE,0,0); hWndUser = NULL; } if (hThread != NULL) { ::TerminateThread(hThread,-1); ::CloseHandle(hThread); hThread = NULL; } return ::UnregisterClass(wc.lpszClassName, wc.hInstance); default: break; } return TRUE; } // ウィンドウを表示するスレッド関数(::CreateThread() に渡す) DWORD WINAPI ShowWindowProc(LPVOID pParam) { // ウィンドウの作成 hWndUser = ::CreateWindow(wc.lpszClassName, (LPCSTR)pParam, // ウィンドウタイトルへのポインタ WS_OVERLAPPEDWINDOW | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, wc.hInstance, // DLL のインスタンスハンドル NULL); if (hWndUser == NULL) ::ExitThread(-1); ::ShowWindow(hWndUser, SW_SHOW); // 別スレッド内でメッセージループを回すのがミソ MSG msg; while (::GetMessage(&msg, NULL, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::ExitThread(msg.wParam); return -1; // 有り得ないはず } // ウィンドウを表示するDLL関数 // ウィンドウが閉じられたことは wait_close() を呼び出すことで判断する HIDEMARU_MACRO_DLL int show_window(LPCSTR title) { // 既にウィンドウを表示中の場合は関数は失敗する if (hThread != NULL || ::IsBadStringPtr(title, 1)) return -1; // ウィンドウの表示を行うスレッドを開始させる DWORD dwThreadID = 0; hThread = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ShowWindowProc, (LPVOID)title, // ShowWindowProc の引数 0, &dwThreadID); return hThread != NULL; } // スレッドの終了コードを文字列で返すDLL関数 // time ミリ秒待ってもスレッドが終了しない場合は "" を返す // 何らかのエラーの場合は "-1" を返す HIDEMARU_MACRO_DLL LPCSTR wait_close(int time) { static char buf[32]; // スレッドはまだ走っていない if (hThread == NULL) return "-1"; buf[0] = '\0'; if (::WaitForSingleObject(hThread, time) == WAIT_OBJECT_0) { // スレッドが終了した DWORD dwRetCode = -1; ::GetExitCodeThread(hThread, &dwRetCode); ::CloseHandle(hThread); hThread = NULL; hWndUser = NULL; wsprintf(buf, "%ld", (long)dwRetCode); } return buf; } // ウィンドウタイトルを動的に変更するDLL関数 HIDEMARU_MACRO_DLL int settitle(LPCSTR str) { if (::IsBadStringPtr(str, 1) || hWndUser == NULL) return 0; return ::SetWindowText(hWndUser, str); } // ウィンドウを表示し、閉じられた時のリターンコードを返すDLL関数 // 要するに前のサンプルと同じモーダルウィンドウだが、 // 呼出し側のプロセスを止めてしまうので背後の秀丸ウィンドウの描画がされなくなる HIDEMARU_MACRO_DLL int do_modal(LPCSTR title) { if (!show_window(title)) return -1; return atoi(wait_close(INFINITE)); }ちょっと長くなってしまいましたが、 前のサンプルの
show_window()
関数内で行っていた処理 (ウィンドウの作成とメッセージループ)を別スレッドの中で行うようにしたのと、 スレッドの終了(=ウィンドウを閉じた)後の後始末処理をするための DLL関数を追加しただけです。 なお、ウィンドウのタイトル文字列を変更するsettitle()
関数も追加していますが、 これはマクロ中でのウィンドウとの対話処理の例として使用します。 また、ウィンドウとの対話処理が必要なければ、 最後のdo_modal()
関数を呼び出せば モーダルなウィンドウの表示と同じことになります。以下のサンプルマクロがこのDLL関数の呼び出しの例ですが、 ウィンドウとの対話処理をどうやって行うかの例も含まれているので、 上のソースと見比べながら処理の流れがどうなっているのかを追ってみてください。
loaddll "win_test.dll"; // ウィンドウの表示テストその1 if (!dllfunc("show_window","ウィンドウのテスト1")) { message "ウィンドウの表示に失敗"; } else { // disablebreak; // 再描画も禁止されるのでコメントアウト $ret = ""; // wait_close() からの戻り値 ##sec = 0; // 経過秒数を保存する変数 ##start = tickcount; // 開始時刻 // ウィンドウの終了を wait_close() の戻り値で判断しながら、 // 1秒毎にウィンドウタイトルを書き換える処理 while ($ret == "") { // ウィンドウが閉じたら "" 以外が返る ##now = (tickcount - ##start) / 1000; // 経過秒数 if (##now > ##sec) { // ウィンドウのタイトルの書き換え ##sec = ##now; #n = dllfunc("settitle", "ウィンドウのテスト1(" + str(##sec) + " 秒経過)"); } // 1 ミリ秒だけウィンドウの終了を待つ $ret = dllfuncstr("wait_close", 1); } message "リターンコード = " + $ret; } // ウィンドウの表示テストその2 message "リターンコード = " + str(dllfunc("do_modal","ウィンドウのテスト2")); freedll;…とまぁ軽々しくスレッドなんちゅ〜もんを持ち出してきてしまいましたが、 スレッドを作成する際の一般的な注意として
等がありますから、 現実的なDLLを作成する際はくれぐれも注意してください。
- スレッドの中で標準Cライブラリ関数を使う時は、
CreateThread()
/ExitThread()
の代わりに_beginthread()
/_endthread()
を使い、 ライブラリを静的にリンクしている場合はLIBC.LIB
ではなくLIBCMT.LIB
(マルチスレッド対応Cライブラリ) を 代わりにリンクすること(MSVC++)- 同じリソースを複数のスレッドから使う場合は セマフォやミューテックス等の同期オブジェクトを使用して アクセス同期を取ること
【8】 作成したDLLのデバッグ
前章までで一応DLLの作成の例は終わりですが、 今までに紹介したソースコードはきちんとDLLが作れていれば ちゃんと動作するはずです。 ですが、いざ自分で作り始めると分かると思いますが、 書いたソースが一度でちゃんと動作することはまずあり得ません。 その原因の大部分は単なる凡ミスですから、 ソースをじっくり眺めれば修正箇所はすぐに明らかになりますが、 それでもどうしても原因が分からない異常な動作をする場合があります。 普通のアプリケーションならデバッガ上でそのアプリケーションを動作させて デバッグを行えばよいわけですが、 DLLは単独では実行可能でないので デバッガを使ったデバッグが少し面倒になります。まず、作成したDLLを呼び出す簡単なテストプログラムを別に作って、 それをデバッグすることでDLL自体のデバッグを行う、 という方法があります。 例えば第3章で作成したDLLをテストする場合は 以下のようなテストプログラムを作成します。
このテストプログラムが行っていることは、
/* test_hello.c */ #include <windows.h> #include <stdio.h> /* hello() 関数の型の typedef */ typedef LPCSTR (__cdecl *PFNFUNC)(void); int main(int argc, char** argv) { HINSTANCE hDll; FARPROC pfnHello; LPCSTR ret; /* DLLのロード */ if ((hDll = LoadLibrary("hello_world.dll")) == NULL) { fputs("DLL が見つかりません\n",stderr); return -1; } /* DLL関数へのポインタを取得 */ if ((pfnHello = GetProcAddress(hDll, "hello")) == NULL) { fputs("指定された関数が見つかりません\n",stderr); FreeLibrary(hDll); return -1; } /* 取得したポインタを使ってDLL関数を呼び出す */ ret = (*(PFNFUNC)pfnHello)(); /* 結果を表示 */ fputs(ret, stdout); /* DLLのアンロード */ FreeLibrary(hDll); return 0; }hello_world.dll
を動的にロードして、"hello"
という名前の関数へのポインタを取得し、 そのポインタを介してDLL関数を呼び出し、 その結果をコンソールに表示する、という処理です。 このDLL関数を呼び出す部分にブレークポイントを置けば、 あとはデバッガでDLL関数の内容を追っていけると思います。次に、テストプログラムを書くのが面倒なほどの 複雑な処理をマクロでしている場合は、 実際にマクロを動かしてDLLを使っている 秀丸自体をデバッグする必要があります。 この場合はマクロを動かす秀丸(プロセス)にアタッチして、 デバッガ側でDLL関数の内部にブレークポイントを置いたあとに アタッチした秀丸でマクロを実行します(*6)。
(*6) Cygwin/MinGW gdb では上で書いたように単純にプロセスにアタッチしても うまく行きません(涙)。 動的にロードされたDLLがリロケートされた場合に gdb が追いついてないみたいですが、 gcc もしくは gdb の設定を変更すれば何とかなるかもしれません (が僕には分からないですm(_ _)m)。
【9】 まとめ
以上、非常に大雑把にDLLの作成について説明してきましたが、要は 開発環境を揃えて第2章で説明したことさえ守れば、 あとは基本的なこと(スタック変数へのポインタを返すetc.)に気をつけて 書いていけばいいだけです。最後に、作ったDLLはたとえ小さいものであっても、 例えば「秀まるおのホームページ」のマクロライブラリなどで 公開することをお勧めします。 どんなものでもそれを求める人はいるだろうし、 そうなれば複数の人が使う事で得られるメリット、 例えば(あまり出て欲しくはないですが)バグが早く見つかるとか 本人が思いもつかなかった機能のアイデアが出てきたりとか、 そういったことがほんの少しの手間で手に入るわけですから、
これはもう公開しない手はない
ですよね(笑)。
【10】 リンク等
第1章 で紹介したフリーソフトウェアとして入手可能なコンパイラに関する情報は 以下の Web サイトから得ることができます。Cygwin gcc
MinGW gcc
Borland C++ Compiler 5.5
GUI 開発環境
- …検索してみたら結構たくさんあるんですね(^^;。 で、いちいち書くのも面倒なので Vector の Web サイトの ソフトウェアライブラリ / Windows95/98/Me / プログラミング / C 言語 等から探してみてくださいm(_ _)m。
【11】 補足〜浮動小数点数版秀丸用のDLLについて
この解説では普通の秀丸用のDLLの作り方を紹介しましたが、 マクロの数値変数が浮動小数点数のバージョン、 いわゆる浮動小数点数版秀丸用のDLLを作る場合について ここで補足したいと思います。浮動小数点数版秀丸のマクロから呼び出すDLLは、 整数版秀丸(=普通の秀丸)のそれに対して以下の点が異なります。
なお、浮動小数点数版秀丸用のDLLは整数版秀丸では使えませんが、 整数版秀丸用のDLLは浮動小数点数版秀丸でも利用することができます。
- 引数または戻り値の数値は
int
ではなくdouble
にする- 浮動小数点数版秀丸用のDLLであることを示すため、
void FLOATMACRO(void) { }
(C++ の場合はextern "C"
を宣言の前につける)
という関数をエクスポートする必要がある