EternalWindows
DLL / 関数のエクスポート

今回から、DLLを実際に開発する方法について説明します。 この作業はコードの云々よりも、まずは作成するファイルがDLLであることを認識し、 プロジェクトの設定をDLLに対応させるところから始まります。 この設定の詳しい方法については、こちらのページを参照してください。

さて、肝心のコードですが、これはDLLの目的を考えてみると分かるはずです。 DLLは、EXEにエクスポートするための関数を実装しなければならないので、 当然ながら関数のコードを書くことになります。

BOOL Swap(int *lp1, int *lp2)
{
	int tmp;

	if (lp1 == NULL || lp2 == NULL)
		return FALSE;
	
	tmp  = *lp1;
	*lp1 = *lp2;
	*lp2 = tmp;

	return TRUE;
}

この関数は、第1引数と第2引数の値を入れ替えます。 このような関数があればEXEを作成する側にとっては、 数値を入れ替えたい場合はSwapを呼び出せばよいという考えが浮かびます。

しかし、実際には上記のようにDLLに関数コードを書いた場合、 その関数はEXEにエクスポートするための関数ではなく、 DLL内部で使われる関数として解釈されることになります。 関数をエクスポートするには、特別なキーワードを指定しなければならないのです。

__declspec(dllexport) BOOL Swap(int *lp1, int *lp2)
{
	・
	・
	・
}

この構文が付いた関数は、DLLのエクスポートセクションに関数名が書き込まれます。 エクスポートセクションに関数名が書き込まれるとは、 EXEが呼び出すことのできる関数の1つになったということです。 __declspecというキーワードの中のdllexportというキーワードが、 エクスポートという意味を持っています。

上記の構文を付けた関数のエクスポートには、 名前装飾という大きな問題が含まれています。 名前装飾とは実際の関数名に変更を加えることで、ビルド時に行われます。 これは、C++のオーバーロードの機能等を実現するために用いられています。 VC2003では、Swap関数の名前が以下のように変更されました。

?Swap@@YAPAHO@Z

名前装飾が行われた場合、実際にエクスポートセクションに書かれる関数名は、 名前装飾された関数名(この場合、?Swap@@YAPAHO@Z)となります。 これにより、起こりうる問題は主に2つあります。 たとえば、EXEがSwap関数を次のように呼び出したとします。

Swap(&a, &b);

ここで、リンカは実際にSwapという関数を探そうとしません。 Swapを名前装飾した関数を探そうとします。 ?Swap@@YAPAHO@Zと述べていないことに注意してください。 実は、名前装飾には規則というものが無く、 全てのコンパイラが固有の方法で名前を変更することが許されています。 ということは、DLLをコンパイルしたコンパイラと EXEをコンパイルしたコンパイラが異なっていた場合、 Sawpという名前装飾はDLLのコンパイラの名前装飾と異なるでしょうから、 リンクは失敗してしまうことになります。 つまり、ここにはコンパイラ依存が発生してしまっているのです。

第2の問題は、DLLの明示的リンクに起因するものです。

lpfnSwap = GetProcAddress(hmod, "Swap");

このコードはSawpという関数にリンクしようとしていますが、 GetProcAddressが探すのはあくまでSawpという名前の関数です。 このSawpという名前をDLLのエクスポートセクションから探そうとします。 しかし、実際にエクスポートセクションに書かれているのは、 ?Swap@@YAPAHO@Zという名前装飾された関数であるため、リンクは失敗してしまいます。 このような場合、GetProcAddressには名前装飾された関数名を指定するしかありませんが、 EXEから見れば名前装飾された関数名など知る由もありません。

このように名前装飾には数多くの問題がはらんでいるため、 extern "C"キーワードで名前装飾を無効にするべきです。

extern "C" __declspec(dllexport) BOOL Swap(int *lp1, int *lp2)
{
	・
	・
	・
}

これにより、Swapという実際の名前がエクスポートセクションに書き込まれるます (厳密には、呼び出し規約の指定によって名前装飾が行われることがあります)。 なお、名前装飾のように名前を変更することは、名前のマングリングとも呼ばれたりします。

extern "C"と__declspecキーワードで関数を正しくエクスポート できることは分かりましたが、エクスポートすべき全ての関数に このようなキーワードを指定するのは煩わしさを感じます。 そのため、一連のキーワードを#defineで定義するのが効果的でしょう。

#define DLLAPI extern "C" __declspec(dllexport)

このようにすれば、DLLAPIという定義を付けるだけで 関数をエクスポートすることができます。

さて、これまでの作業を行えば必要な関数はエクスポートできますが、 EXEの立場を考えた場合、まだ行うべきことがあります。 EXEは、DLLからエクスポートされている関数を呼び出すわけですから、 コードを組むにはその関数のプロトタイプが必要になるはずです。 また、その関数が独自の構造体を引数として要求するならば、 その構造体も把握していなければならないでしょう。 一般には、これらの情報をヘッダーファイルにて定義し、 EXEとDLLの両方がインクルードします。

#define DLLAPI extern "C" __declspec(dllexport)

BOOL Swap(int *lp1, int *lp2);

このようにすれば、EXEはヘッダーファイルをインクルードすることにより、 DLLの関数やその他の定義にアクセスすることができます。 しかしながら、__declspecキーワードのdllexportというのは、 関数をエクスポートするときに指定するべきものであり、 関数をインポートするEXEが指定してはなりません。 EXEは、dllimportキーワードを指定すべきなのです。 よって、ヘッダーファイルには、自身をインクルードしようとしているのが EXEなのかDLLなのかを把握するようなコードを書かなければなりません。

#ifndef DLLAPI
#define DLLAPI extern "C" __declspec(dllimport)
#endif

BOOL Swap(int *lp1, int *lp2);

ヘッダーファイルは、DLLAPI定数が定義されていないときには、 DLLAPIをdllimportとして定義します。 これでは、DLLもEXEもdllimportとしてインクルードされそうに思えますが、 DLL側ではヘッダーファイルをインクルードするときに次のようにします。

#define DLLAPI extern "C" __declspec(dllexport)

#include "mydll.h"

DLLは、ヘッダーファイルをインクルードする前にDLLAPIをdllexportとして定義します。 これで、ヘッダーファイルの#ifndefは実行されることはありません。

では、実際にDLLのプログラムを見ていくことにします。 まず、ヘッダーファイルを示し、その後にソースファイルを示します。 ヘッダーファイルの名前は、mydll.hとします。

#ifndef DLLAPI
#define DLLAPI extern "C" __declspec(dllimport)
#endif

DLLAPI BOOL Swap(int *lp1, int *lp2);

続いて、ソースファイルです。

#define DLLAPI extern "C" __declspec(dllexport)

#include <windows.h>
#include "mydll.h"

BOOL IsValidAddress(LPVOID lp1, LPVOID lp2);

DLLAPI BOOL Swap(int *lp1, int *lp2)
{
	int tmp;

	if (IsValidAddress(lp1, lp2))
		return FALSE;
	
	tmp  = *lp1;
	*lp1 = *lp2;
	*lp2 = tmp;

	return TRUE;
}

BOOL IsValidAddress(LPVOID lp1, LPVOID lp2)
{
	return lp1 == NULL || lp2 == NULL;
}

mydll.hをインクルードする前に、DLLAPIを定義しているところに注目してください。 これにより、ヘッダーファイルの#ifndefは実行されることはありません。 IsValidAddressという関数は、引数のポインタが無効でないかを調べるための関数であり、 これはEXE側にエクスポートするつもりはありません。 DLL内でのみ使用される関数ということで、DLLAPIという定義も付けていません。

今回のプログラムをビルドすると、プロジェクトのRelease(またはDebug)フォルダに以下のようなファイルが存在するはずです。

赤で囲ったファイルがDLLです。 青で囲ったファイルはインポートライブラリであり、 DLLがエクスポートしている関数の一覧が記録されています。 次節では、この2つのファイルとmydll.hを参照して、 DLLのSwap関数を呼び出すプログラムを作成します。


戻る