S-JIS[2006-08-22/2007-09-28] 変更履歴

DLLの補足

DLL作成の指針やDLLに関連するファイルについて。


暗黙的リンクと明示的リンク

DLLの呼び出し方法には、暗黙的(静的)リンク明示的(動的)リンクがある。

暗黙的リンクと明示的リンクの特徴(メリット・デメリット)
  暗黙的(静的)リンク 明示的(動的)リンク
関数の宣言 DLLの関数を__declspecl(dllimport)を付けて宣言する。
(付けなくてもいいらしいが、付けた方が多少効率がいいらしい)
DLLの関数を宣言しない。
代わりにtypedefしておく(のが便利)。
リンカー リンカーでlibファイルをリンクする必要がある。 リンカーでのリンクは不要。
DLLのロード 実行の準備段階でDLLがロードされる。
見つからない場合は実行されない。
LoadLibrarydllファイルをロードする。
見つからない場合はエラーが返る。
関数との紐付け libファイルの中で、dllファイル内で定義されている関数名(や序数)が保持されている。
この関数名(あるいは序数)が一致しないと実行時エラーとなり、実行されない。
GetProcAddressで、dllファイル内で定義されている関数名(あるいは序数)を指定する。
この関数名(または序数)が一致しないとエラーが返る。
暗黙・明示とは プログラマーが意識しないレベル(実行の準備段階)でDLLが勝手にロードされるので、『暗黙的』。 プログラマーがLoadLibraryを呼び出してやらないとDLLをロードできないので、『明示的』。
静的・動的とは リンカーでリンクした時点で使用するDLLの種類や関数が決まってしまい、変更できないので『静的』。 LoadLibraryGetProcAddressで、使用するDLLの種類や関数を自由に変更できるので『動的』。

暗黙的リンクの場合、関数宣言のヘッダーファイルをDLLの定義側と共有することが簡単に出来る。

#ifdef DLL_DEFINE	//DLLの作成側では、このマクロを定義しておく
#define	DLL_EXTERN	__descspec(dllexport)
#else
#pragma comment(lib, "test.lib");
#define	DLL_EXTERN	__deslspec(dllimport)
#endif

DLL_EXTERN int __stdcall test(int a);

DLLの使用側に作成側からヘッダーファイルを提供してもらえば、関数呼び出しに関する間違いはコンパイラーが指摘してくれるので、堅実になる。
ただしlibファイルも同時に提供してもらう必要がある。libファイルが新しくなるということは再リンクが必要になるということなので、ちょっと嫌かも。

暗黙的リンクと明示的リンクのどちらにすべきか
 
DLLの呼出側とDLLが密接に関係している。
(そのDLLが無いと実行できないとか)
オプション的なDLLであり、DLLが存在しなくても実行したい。 ×
DLLファイル名を定義ファイル等に書いておき、それをロードしたい。 ×
特定のディレクトリに存在するDLLを検索してロードしたい。 ×

明示的リンクでなければ出来ない事が多々あるので、その場合は明示的リンクでないと仕方ない。
が、暗黙的リンクで出来るのであれば、そちらの方がプログラミング的には楽。

MSDNの『エクスポート方式の使い分け』


ソースファイル(関数定義・宣言)

DLL作成側で関数を定義するには、以下の方法がある。

また、関数定義(および宣言)には以下のような識別子を付加することが多い。


defファイルを使用しない場合dllファイルlibファイルには、装飾された関数名で関数が保持される。序数値は適当に採番される。

ソースファイルでtestという関数を定義した場合、識別子の種類に応じて以下のように装飾が変わる。

関数宣言・定義 装飾つき関数名 備考
int __cdecl test(int a) ?test@@YAHH@Z この装飾はコンパイラのバージョンによって異なる。
int __stdcall test(int a) ?test@@YGHH@Z
extern "C" int __cdecl test(int a) test  
extern "C" int __stdcall test(int a) _test@4  

C++においては 関数のオーバーロードが許される(同名の関数でも引数の型が異なっていれば別の関数として定義できる)ので、それらを区別する為にごちゃごちゃと装飾される。
「extern "C"」を付けるとC言語扱いになる(オーバーロードなんて機能は無い)ので、装飾がシンプルになる。

「__stdcall」は、アセンブラレベルでの動作を規定する。
引数はアセンブラレベルではスタック上に確保されるが、この解放を__stdcallは呼ばれた関数側で行い、__cdeclは呼出元で行う。
(VC++のデフォルトは__cdecl。余談だが、可変長引数(va_arg)は__cdeclでしか使えないらしい(使用したスタックサイズが呼出元でしか分からない為だろう)
__stdcallはWINAPI(Win32 APIの修飾子)でも定義されているもので、他言語からもこの関数を呼び出したい場合は__stdcallにする必要がある。
__stdcallで 修飾された関数名の「@」以降の数字は、引数に使用するスタックサイズだそうだ。


defファイル

defファイルを使用すると、dllファイルに保持される関数名や序数値をプログラマーが定義することが出来る。

defファイルは 新規プロジェクトの種類によっては自動的に雛形が生成されるが、そうでない場合は手動で作成してプロジェクトに追加する
いずれにしても自分でdefファイルの中身を書く必要がある。

;DLL名(DLLのファイル名(拡張子は除く)と一致していないと、ビルド時に警告が出る)
LIBRARY test

;説明(有っても無くてもよい)
DESCRIPTION "DLL test"

;エクスポートする関数
EXPORTS
	test @1
	foo  @2

;DLLのバージョン(有っても無くてもよい)(→プログラムから取得する方法
VERSION 1.0

EXPORTS』にエクスポートする関数を書く。以下のように、色々なオプションを付加できる。

test
関数名のみ指定。
testという関数がtestという名前でdllファイルに定義される。
序数値は適当に採番される。
test @1
関数名と序数を指定。
testという関数がtestという名前でdllファイルに定義される。
序数値は「@」の後ろに書かれた番号になる。
hoge=test @1
関数の別名を指定。
testという関数がhogeという名前でdllファイルに定義される。
test @1 NONAME
関数名を削除する指定。
testという関数はdllファイル内に序数しか定義されない。 すなわち、関数名を使用した呼び出しが出来なくなる。

EXPORTSで指定する関数が__declspec(dllexport)を宣言している必要は無い。
が、逆に__declspec(dllexport)を宣言している関数がEXPORTSに書かれなかった場合は、その関数はdefファイルを指定しなかった場合と同様に扱われてエクスポートされる。(序数値は重複しない番号が自動的に割り当てられる)


libファイル

DLLの呼び出し側が暗黙的(静的)リンクの方式を採る場合、DLLの関数は「外部の関数」として宣言することになる。

一般的にリンカーでのリンク時には外部の関数が全て必要となり、それはライブラリに書かれているので、リンカーのオプションとしてライブラリファイルを指定する。
DLLの場合はDLL生成時に同時に作られるlibファイルを指定する。

通常のライブラリはプログラムの実体がバイナリになっているわけだが、DLL用のlibファイルはDLL本体へのエントリー(入口)が定義されているだけらしい。

libファイルをリンクした実行プログラム(exeファイル)は、実行する際にlibファイル内に指定されたDLLを探してロードする。
また、使用している関数(や、その序数)が一致しているかどうかチェックし、必要な関数がDLL内に存在しない場合はエラーを出して終了する(実行が中断する)。


expファイル

エクスポートするものを記録しているファイルらしい
__declspec(dllexport)宣言やdefファイルから生成され、dllファイルを作る際に使用される。


dllファイル

実行時に利用されるファイル。プログラムの実体が入っている。

関数名や序数でエントリー(呼び出し口)を保持しており、呼び出す側はそれに完全に一致する関数名か序数を使用して呼び出す。
(暗黙的(静的)リンクの場合はlibファイルがそれを保持しており、実行時の準備段階でチェックされる)
(明示的(動的)リンクの場合はGetProcAddressでそれを指定する)

この関数名は装飾された関数名であり、ソースファイルに書かれている関数名とは一致しないことが多い。
(といいつつ、defファイルでエクスポートしている場合は一致していることが多いだろう)

コマンドプロンプトから実行できるdumpbin.exe(VC++をインストールすると入っている)や『Dependency Walker』(VC++6.0をインストールすると入っていた)でdllファイルを見ると、これらの関数名や序数値が分かる。

dumpbin

C:\temp>dumpbin /exports test.dll
〜
            ordinal hint   name

                  1    0   test  (00001000)
〜

「ordinal」が序数で、「name」が関数名。

Dependency Walker

『Dependency Walker』を実行し、dllファイルを開く。

「Ordinal」が序数で「Function」が関数名。
ちなみに「Imager Ver」がバージョン


序数

dllファイルは、関数を序数と呼ばれる値で保持しているらしい。
なので、序数は重複しない番号にする必要がある。(特にdefファイルを手動で書く場合)

序数はdllファイル内でテーブル化されているので、跳び跳びの番号を使うと空きが出来て無駄となる。
また、1から始まらなくてもちゃんと動作する。MSDNには「1から」と書いてあるけど、他の断定的な記述も嘘が混じってる気がするから、これも大丈夫かな?(爆))

序数はlibファイルにも書かれているようで、一致している番号がDLLに存在しないと実行時にエラーとなる可能性がある。
(「序数が見つかりません」というメッセージボックスで、「序数 n がダイナミック ライブラリ test.dllから見つかりませんでした。」というようなメッセージが出る)
同様にGetProcAddressにおいて序数を指定した場合に DLL内にその番号が見つからないとGetLastError()=182(「このオペレーティング システムでは %1 は実行されません。」)のエラーとなる。%1って何だ?

なので、libファイルを使っている場合やGetProcAddressで序数を指定している場合は、defファイルの序数を変えたり、(defファイルを使わず__declspec(dllexport)することによって自動的に序数を採番させている場合に)新しい関数をエクスポートしたり(序数が変わる可能性がある)したときには、DLL呼び出し側のプログラムの修正(や再リンク)が必要となる。
(逆に言えば、後から関数を拡張するようなことが想定されるなら、defファイルをきちんと作って序数をちゃんと書いておくべき。)

※しかし、呼び出し側で使っていない序数なら、いくら変わっても無関係。


DLL実装方法へ戻る / VC++ページへ戻る / 技術メモへ戻る
メールの送信先:ひしだま