Hidemaru Q and A

第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を作る際に必要な環境と、 作成時に注意すべき点をいくつか挙げていますが、 それさえクリアすれば

ってなもんです(怪しげな英会話の教材のキャッチコピーみたいでアレですが(笑))。

【1】 用意するもの

まず、この解説はDLLの作成の話が中心なので、 使用する言語(ここでは C または C++ 言語)についての知識は 簡単なツールやサンプルアプリケーションを作れる程度に持っている として話を進めていきます。 したがって、

とか

といったことで悩んでいる方は、 この解説より先にまず言語自体の参考書を見て勉強してください。

次に、DLLを作るためにはもちろんDLLが作成可能な 開発環境(コンパイラetc.)が必要ですが、 売品なら Microsoft の Visual C++ や Borland の C++ Builder 等、 その他にもフリーソフトウェアのコンパイラがいくつかあります。 その中で Web 等から現在入手可能で比較的有名なものは以下の通りです。

これらの入手先についてはこちらをご覧ください。 また、これらは基本的にはコマンドライン上で使うツールなわけですが、 それらをウィンドウ環境で利用するための GUI ソフトウェア(いわゆる統合開発環境)も いくつか公開されているようです。 それらの環境の上でDLLを作成する方法については それぞれのマニュアルを参照してください。 ここでは、コマンドラインから直接DLLを作る時の コマンドラインオプションのみを説明します。

なお、いずれかの方法で作成したDLLの関数が アプリケーションからちゃんと呼び出せるかどうかは、 Windows に付属してくるクイックビューアを利用すると確認できます (エクスプローラ等でDLLファイルを右クリックしてでてくる コンテキストメニューの「クイックビューア」を選ぶ)。 その中の「Export Table」の下の方の表が関数の一覧ですが、 その最も右側の列がDLL関数を呼び出す時の名前です。 この「Export Table」が見つからない場合はDLLの作成に失敗しているので、 作成手順や DEF ファイルの内容を確認してからやり直してみて下さい。

【2】 マクロから呼び出せるDLL関数を作るには?

前章でDLLを作成するための一般的な方法について説明しましたが、 秀丸マクロでは一般的なDLL全てを扱えるわけではありません。 マクロから呼び出せるDLL内の関数(以下「DLL関数」)は、 以下の性質を持つものだけです。

1番目と2番目の性質は、秀丸マクロで扱えるデータ型が 数値 (int) と文字列 (char*) しかないことから当然の事といえます。 なお、文字列型の const 修飾子はなくても DLL関数の宣言としては支障ありませんが、 かといって、わざと const をつけずに 引数として渡された文字列の内容を書き換えてはいけません (というか書き換えても無意味)。

最後の性質はちょっとわかり難いかもしれませんが、 Win32API でいうと WINAPI 宣言子 (実際は __stdcall の #define) がついていないもの、もしくは WINAPIV 宣言子 (同じく __cdecl の #define)がつくものです。 とはいえ、__cdecl__stdcall 宣言子を つけなかった場合のデフォルトは __cdecl なので、 特に何もつけなければOK!ということになります。

以上の事を踏まえて、 秀丸マクロから使用可能なDLL関数のプロトタイプ宣言を書き下すと 以下のようになります。

ここで、param_listreturn_type は先に説明した 引数リストまたは戻り値を表します。 また、func_name は関数の名前ですが、 C++ 言語の場合は※の注にあるように「extern "C"」を つけておかないと、 後からDLL関数を呼び出す時に func_name という ソースコード中での名前で参照できなくなるので注意してください。

__declspec(dllexport) は DLL関数として呼び出せる関数につける宣言子ですが、 いちいちこんな長ったらしい宣言子を書くのも面倒なので、 以下の各章のサンプルのためにあらかじめ 次のようなマクロ(HIDEMARU_MACRO_DLL)を定義しておきます。

…って、何か C 言語の場合は宣言子の方が短いような気がしますが、

なので気にしない様に(笑)。

さて、DLLには上で説明したDLL関数や、 それらから呼ばれるだけのヘルパー関数の他に、 DLLエントリポイント関数というものを入れることができます (必須ではないことに注意!!)。 DLLエントリポイント関数は、 あるプロセスがDLLをロードした時に必要となるDLLの内部変数の初期化、 またはアンロードされた時の後始末を行う目的で使用されます。 デフォルトでは DllMain という名前の関数が エントリポイント関数になるので、 以下の説明でも一貫して DllMain という名前を使うことにします。 その DllMain 関数のプロトタイプ宣言と、 DllMain() が呼び出された時の状況に応じた処理を行う、 定型処理だけを書いたコードは以下の通りです。

上のサンプルにもコメントしてありますが、 C++ を使う場合はくれぐれも忘れずに extern "C" で 名前マングルされない様にして下さい(特に gcc を使う場合)。 エントリポイント関数は必須ではないため、 これを忘れてもコンパイラは「DllMain() が見つからない」と 注意してくれませんので…。 え?なんでこんなにしつこく注意するかって? それはですねぇ…

…とまぁ余談はともかく、以下の章でも必要に応じて適宜 DllMain 関数を使っていますので、 具体的なサンプルコードについてはそちらをご覧ください。


【3】 とりあえず "hello world!"

この手の解説をする時にはもはや定番、というか

というネタですが、関数としては最も簡単な "hello world!" という文字列を返すだけの関数を作ってみます。 第2章で説明した宣言子マクロや DllMain 関数は同じ物を使って、 その後に以下のようなコードを付け足します。

この関数を含んだDLLを hello_world.dll という名前で作成し、 秀丸(hidemaru.exe)と同じか パスの通ったフォルダに置きます。 そして、以下のようなマクロファイルを作ってDLLをテストしてみましょう。

DLLの作成方法その他に間違いがなければ、 以下のような "hello world!" というテキストが入った メッセージボックスが表示されるはずです。

この例は確かに単純ですが、一つ重要なことがあります。 この関数のように文字列、したがって LPCSTR (= const char*) を返す場合、 その文字列はDLL内部で静的に(またはヒープ領域に)確保したもので なければならない、ということです。 この注意はもちろんDLL関数に限ったことではありませんが、 C または C++ でプログラムを書いていれば

なので、ここでも取り上げておきます。

まずは戻り値である文字列へのポインタが スタック上に確保した文字配列を指している、という例です。 スタック領域は関数内で一時的に使用する変数(ローカル変数)を置くための場所で、 スタック上に確保した変数はその関数から抜けたあとは無効になります。 で、その無効な変数(文字配列)を指すポインタを 関数の戻り値として返すのはもちろん間違いです。

と言ってる側から(笑)、例えば以下のようなコードを書いてしまうわけです。

行数が少ないので何だかわざとらしい例になってしまいましたが、 エディタの1画面に収まらないような関数を書いていて 途中で色々 buf[] の内容をあれこれいじっている内に、 関数の終わりの方では buf[] をスタック上に確保していた事を すっかり忘れてしまい… というパターンで思わずやってしまうので注意が必要です。 まぁさすがにここまであからさまな間違いはしないかもしれませんが、 スタック上の配列を指すポインタ変数を別に用意して そのポインタで配列を操作していたりすると、 最後にそのポインタの値をそのまま返してしまうというミスは 結構ありがちなことだと思います。

さて、この例のような間違いを犯したあと、

気持ちも新たにコーディングを始めた

に書いてしまいがちなのが以下のようなコードです。

今度は buf が指す先のメモリはヒープ上にありますから、 関数を抜けてもメモリの内容が破壊される心配はありません。 がしかし、このコードには malloc() の戻り値を チェックしていないという問題もありますが、それ以前に malloc() で確保されたメモリが開放されないという、 いわゆるメモリリークの問題があります。

このような間違いを犯してしまう時に考えている事はおそらく

という事だと思います(*1)。 この件については、上記の例のようなコードでは確かにまずいのですが、 DllMain() 関数を使えば、 メモリリークの心配をしなくてもよい方法で動的に ヒープ領域に確保したメモリへのポインタを返すことができます。 これについては次章で見ることにしましょう。

(*1) あと考えられる事としては、 静的に確保した配列だと他のプロセスが同じDLLを呼び出した時にまずくないか? という事だと思いますが、DLL中で確保した静的変数はプロセス毎に コピーが作られるのでその心配は無用です。 ただし、秀丸マクロからだけでなく 一般にマルチスレッドなプロセスでも使用されるDLLを作る場合は、 スレッドセーフなDLLにするために静的変数の使用には注意が必要です (スレッドローカル変数領域(TLS)を利用する等)。


【4】 ちょっと手を加えてみる

前章で作成したDLL関数に少し手を加えて、 "hello " の後に引数で渡された文字列をつなげたものを返すようにしてみましょう。 単純に書けば

となりますが、 この場合渡した文字列の 74 バイト目以降は切れてしまいます。 静的に確保したバッファのサイズを増やせばもう少し制限を緩めることができますが、 根本的に制限があることには変わりありません。 そこで、前章の最後で考えたように、 メモリをヒープ領域に動的に確保する方法で書いてみます。

ここで、初期メモリの確保(DLL_PROCESS_ATTACH)と 開放の後始末(DLL_PROCESS_DETACH)を DllMain() 関数の中で行っています。 その他、hello_what() 関数の中でも メモリの開放と再確保を行っている部分がありますが、 これは現在確保されているメモリの量が足りない場合にのみ実行されます。 実際にこのDLLを以下のようなマクロでテストしてみると、 のようにメモリの(再)確保と開放が行われますが、 最終的には DllMain() 関数の後始末処理でメモリが開放されるので、 前章の最後で述べたメモリリークの問題は発生しません。

【5】 もう少し複雑なものを作ってみる

秀丸マクロから呼び出せる関数は __cdecl 宣言子を持つもの、 すなわち通常のC関数の呼出し規約に従うものに限る、 という話は第2章でしましたが、 これは裏を返せば、 可変個の引数を取るDLL関数を作ることができる、 ということでもあります。 なぜそうなのかの説明は他書に譲って、 ここでは可変個の引数を扱うための C の標準マクロの使い方を説明します。 というわけで、今度の例はC言語のライブラリ関数 sprintf 関数に似た引数を取り、 結果の文字列を返すDLL関数です。

まずは Win32API の wvsprintf() をそのまま使うバージョンですが、 これは以下のように簡単にできます。

これはこれで例としては十分ですが、 va_arg() マクロの使い方の説明も少しはしておかないと

てなことを言われそうなので、 複雑な書式指定はあっさり捨てて、 "%d"(10進整数)、 "%x"(16進整数) と "%s"(文字列)、 それ以外はそのまま出力する、というルールのみをサポートした バージョンを作ってみましょう。

ちなみに、この章で作ったDLL関数は以下のように呼び出します。

ところで、今回の例のような sprintf() 系の関数は、 最初のフォーマット指定文字列によって間接的にその後に続く引数の個数を 関数に伝えていますが、 引数の個数を伝えるにはもちろん他の方法も色々と考えられます。 一番簡単なのは、最初の数値引数でその後に続く引数の数を指定する、 例えば

のような方法でしょう。 または対象となる文字列の形式とは明らかに異なる文字列を最後に指定する、 例えば

のような方法などがあり得ると思います。 いずれにしても va_is_end(args) のような パラメータ指定の終了を判定するマクロは用意されていない(*2)ので、 何らかの形で引数が何個あるかの情報をDLL関数に伝える必要があります。

(*2) 別にライブラリの作者が手抜きをしているわけではなくて、 一般的なスタックの構造では そもそもパラメータ指定の終了位置を判定できないからです。

引数の話題が出たついでに、というか

ですが、ここで重要な注意です。 DLL関数が可変個の引数を取るかどうかに関わらず、 マクロ側(一般に動的にロードされたDLLの関数の呼出し側)では 任意個のパラメータを任意の順番で渡すことができてしまいます。 普通の関数(または静的にロードされたDLLの関数)なら コンパイル時に引数の個数及び型チェックが行われますから、 間違ったパラメータを渡してしまったことを コンパイル時にコンパイラが注意してくれます。 が、動的にロードされたDLLの関数の場合は、 間違ったパラメータを渡しても誰からも何も注意されない、まさに

のようですが(笑)、 間違ったパラメータを渡された関数は下手をすれば暴走状態に陥ってしまいます。 というわけで、 自作したDLLを公開する時はその呼出し規約を ちゃんとドキュメントに書くのはもちろんのこと、 渡された引数が正当なものかどうか(*3)のチェックは きちんとするようにしましょう。 ちなみに、

(*3) 文字列型の場合は IsBadStringPtr() API で ポインタの値が正当なものかどうかだけはチェックできます。 ポインタが指す先の文字列の内容、 または整数型の場合に値が不正かどうかの判断は 貴方(DLL関数)自身でやるしかないです。


【6】 「ファイルを開く」ダイアログを出してみる

さて、ここで話題をがらっと変えて、 DLL関数の中でダイアログやウィンドウを出す方法について 解説していくことにします。

まず比較的簡単にできるダイアログの表示から行きますが、 例として「ファイルを開く」ダイアログを出すDLL関数を作ってみましょう。 このダイアログを出す関数は既に Win32API に GetOpenFileName() という名前で用意されているので、 ここでも素直にそれを使うことにしましょう。 この API で出てくるダイアログはいわゆるモーダルダイアログという奴で、 API の呼出しでダイアログが表示され、 ダイアログが表示されている間のユーザーからの入力等の処理は 全て API 内で行われ、 最後にダイアログが閉じられた後に API から復帰します。 というわけで、DLL関数内でやるべきことは、結局の所

ということになります (もちろん API に渡すパラメータの準備や API から復帰した後の処理は必要ですが)。

以下がそのサンプルソースですが、 この章から C++ 言語を使っているので注意してください。

API に渡すパラメータの設定が少しややこしいですが、 この辺は適宜 Win32API のヘルプを参照してください。 で、このDLL関数を呼び出すサンプルマクロは以下のようになります。 このマクロを実行すると、 (ダイアログの細かい形はOSのバージョンで違いますが) 以下のようないつもの「ファイルを開く」ダイアログが表示されるはずです。

「色の選択」等の他のコモンダイアログを表示する API も 概ね上の GetOpenFileName() と同じように モーダルダイアログを表示するだけなので、 特に何も考えずに API を呼び出すだけでダイアログを出すことができます。

【7】 ウィンドウを表示してみる

前章ではOSがあらかじめ用意しているコモンダイアログを 表示する例を紹介しましたが、 自分で作成したダイアログやウィンドウを表示するにはどうすればよいのでしょうか? ダイアログの場合は(::DialogBox() 等の API を使って) モーダルダイアログとして表示すれば、 前章の例とあまり変わらない手順で表示することができます。 ですが、同じダイアログでもモードレスダイアログとして表示したい時や、 普通のウィンドウを表示したい時は、 もう少し手順を踏む必要があります。

まず、以下のようなコードを試してみましょう。

このサンプルで行っていることは、 ユーザー定義のウィンドウクラスの登録をする場所が DllMain() 関数である事を除けば ほぼ普通のアプリケーションが WinMain() 関数等で 行っているウィンドウ表示のコードそのままです。 なお、今回のウィンドウにはメニューもアイコンもつけなかったので 必要なかったのですが、 それら(一般にはリソース)が必要な場合は別途作成し(*4)、 DLL作成時にリンクする必要があるので、 各処理系のマニュアルを参照して必要な処理を行ってください。

(*4) gcc の場合は binutils の windres、 Borland C++ Compiler の場合は brcc32 を使います。

さて、このコードは実際に動作するわけですが、 このコードには一つまずい点があります。 それはDLL関数内で勝手にメッセージループを回していることです。 …と詳しい事を書き始めるとダラダラと長くなるだけなので、 ここでは単に

とだけ述べておきます(*5)。

(*5) 上のサンプルコードのように単純なメッセージループしか持たないプロセスなら 問題はないですが、 メッセージループの中で特殊な処理を行っているようなプロセスの場合だと、 その特殊な処理が行われなくなるため問題が発生する場合があります。

ところで上のサンプルで表示したウィンドウですが、 よく考えると show_window() 関数から戻る時には ウィンドウは既に閉じられているわけですから、 マクロ内で開いているウィンドウに対して何か手を加えたり、 ウィンドウに入力された文字などをウィンドウの表示中に取ってきたり、 といった処理を行うことはできません。 要するに前章のコモンダイアログと同じような モーダルなウィンドウを作ったのと同じことです。 というわけで、次のサンプルではモードレスなウィンドウを作ること、 そしてDLL関数の呼出し元プロセス(正確にはスレッド)で メッセージループを回さないこと、 の両方を同時に解決する方法を紹介します。

直前に述べた2つの問題を解決するには、 DLL関数を抜けた後もウィンドウを開いておき、 さらにそのウィンドウのメッセージを処理するための メッセージループを回し続けることが必要なわけですが、 これは以下のようにDLL関数内で新しくスレッドを生成して、 その中でメッセージループを回すようにすれば両方とも実現できます。

ちょっと長くなってしまいましたが、 前のサンプルの show_window() 関数内で行っていた処理 (ウィンドウの作成とメッセージループ)を別スレッドの中で行うようにしたのと、 スレッドの終了(=ウィンドウを閉じた)後の後始末処理をするための DLL関数を追加しただけです。 なお、ウィンドウのタイトル文字列を変更する settitle() 関数も追加していますが、 これはマクロ中でのウィンドウとの対話処理の例として使用します。 また、ウィンドウとの対話処理が必要なければ、 最後の do_modal() 関数を呼び出せば モーダルなウィンドウの表示と同じことになります。

以下のサンプルマクロがこのDLL関数の呼び出しの例ですが、 ウィンドウとの対話処理をどうやって行うかの例も含まれているので、 上のソースと見比べながら処理の流れがどうなっているのかを追ってみてください。

…とまぁ軽々しくスレッドなんちゅ〜もんを持ち出してきてしまいましたが、 スレッドを作成する際の一般的な注意として

等がありますから、 現実的なDLLを作成する際はくれぐれも注意してください。

【8】 作成したDLLのデバッグ

前章までで一応DLLの作成の例は終わりですが、 今までに紹介したソースコードはきちんとDLLが作れていれば ちゃんと動作するはずです。 ですが、いざ自分で作り始めると分かると思いますが、 書いたソースが一度でちゃんと動作することはまずあり得ません。 その原因の大部分は単なる凡ミスですから、 ソースをじっくり眺めれば修正箇所はすぐに明らかになりますが、 それでもどうしても原因が分からない異常な動作をする場合があります。 普通のアプリケーションならデバッガ上でそのアプリケーションを動作させて デバッグを行えばよいわけですが、 DLLは単独では実行可能でないので デバッガを使ったデバッグが少し面倒になります。

まず、作成したDLLを呼び出す簡単なテストプログラムを別に作って、 それをデバッグすることでDLL自体のデバッグを行う、 という方法があります。 例えば第3章で作成したDLLをテストする場合は 以下のようなテストプログラムを作成します。

このテストプログラムが行っていることは、 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 開発環境


【11】 補足〜浮動小数点数版秀丸用のDLLについて

この解説では普通の秀丸用のDLLの作り方を紹介しましたが、 マクロの数値変数が浮動小数点数のバージョン、 いわゆる浮動小数点数版秀丸用のDLLを作る場合について ここで補足したいと思います。

浮動小数点数版秀丸のマクロから呼び出すDLLは、 整数版秀丸(=普通の秀丸)のそれに対して以下の点が異なります。

なお、浮動小数点数版秀丸用のDLLは整数版秀丸では使えませんが、 整数版秀丸用のDLLは浮動小数点数版秀丸でも利用することができます。

目次に戻る