DLLを作ろう!(関数編)

 今回はDLLを作ってみましょう! いきなり色々作るのも大変なんで、今回は関数をエクスポートしたDLLを作ってみましょう。

関数を作る意味
 DLLに関数を作る意味は、なんでしょう?
 はっきり言って意味はないです。今度説明する「DLLにクラスを作る方法」を使えば、関数を入れるためにDLLを作る必要はなくなるでしょう。
 ただ、もちろんクラスとは別に関数をAPIのような形で置きたいという場合もあるでしょう。MFCにもAfxなんたらというような形で、普通の関数があります。そういう関数を起きたい場合には、必要な方法と言えるでしょう。

プロジェクトの作製
 まずはプロジェクトから。プロジェクトは「MFC AppWizard (DLL)」を選んでください。DLLは、別にMFCを使わなくても作れますが、たぶん使った方がずっと楽ですので、今回は使うことにします。
 プロジェクト名を決めて「OK」ボタンを押したら、1ページだけのウィザードダイアログが表示されます。ここで「MFC の拡張 DLL(MFC の共有 DLL 使用)」を選択して、「終了」ボタンを押してください。

 MFCを使ったDLLには、ふたつのタイプがあります。
 ひとつは「拡張DLL」で、上の設定をしたときにこのDLLが作製されます。この「拡張DLL」は、DLLを使用するExeやDLLもMFCを使う時にのみ使うことができます。
 もうひとつは「レギュラーDLL」で、「MFC の共有 DLL を使用」を選ぶことで作製できます。これは内部的にMFCそのものを持っているため、DLLを使用するExeやDLLがMFCを使わなくても使用できます。
 ここでは、DLLにリンクするExeファイルはMFCを使うので「拡張DLL」を作製します。

DllMain()
 さて、プロジェクトができたら、同時に作製されたファイルを見てみましょう。おなじみのStdAfx.hの他に、リソースファイルがありますね。他には、プロジェクト名.cppと、見慣れないプロジェクト名.defがあると思います。
 この内のプロジェクト名.cppを開いてみてください。その中にDllMain()という関数があると思います。

 普通のExeファイルを作製するとき、必ずWinMain()という関数を作製します。ウィンドウズはExeファイルの中からこの関数を探しだし、呼び出すことでアプリケーションが始動します。
 このWinMain()のDLL版がDllMain()です。WinMain()と同様、SDKにちゃんと書かれているのでどういう関数なのか知りたい場合にはそちらを見てください(ただしSDKでの名前はDllEntryPoint()です)。

 基本的に、この関数は「あればいい」だけで、プログラマー側で手を加えることは少ないでしょう。

ファイルの作製とコードの追加
 では、実際に手を加えてみましょう。
 「ファイル」−「新規作製」で「C/C++ ヘッダー ファイル」を選び、ファイル名をFunc.hとしてください。「OK」を押せば、同名のファイルが作製され、ファイルの中身が表示されるはずです。おそらく何も書いてありませんが。そこに、次のコードを書き込んでください。


#ifndef __FUNC_H__
#define __FUNC_H__

int WINAPI DllFunc( CString &p_rcStr, CWnd *p_pcWnd );

#endif	//__FUNC_H__	

 同様に、「ファイル」−「新規作製」で今度は「C++ ソース ファイル」を選び、ファイル名をFunc.cppとして作製して、次のコードを書き込んでください。


#include "StdAfx.h"
// 「__declspec」を使う時は、ここにあとで追加。
#include "Func.h"

int WINAPI DllFunc( CString &p_rcStr, CWnd *p_pcWnd )
{
	// 単純にメッセージボックスの表示。
	return p_pcWnd->MessageBox( p_rcStr );
}
	

 関数そのものは単純なものなので大丈夫でしょう。とりあえずビルドして、通ることを確認してください。

装飾名について
 ちょっと脇に逸れて、「装飾名」というものを見てみることにしましょう。基本的に「DLLを作ること」には関係ないことなので、読み飛ばしてもOKです。

 関数の前にWINAPIというものが置かれています。これは、Win32SDKのほとんどのAPIに使用されています。
 また、MFCで使われているAfxなんたら()という関数は、AFXAPIを使っています。
 これはどちらも__stdcallとして定義されています。これは「修飾子」と呼ばれるもののひとつです。

 「修飾子」とは、コンパイル後の関数のタイプを決めるものです。コンピューターは当然内部では機械的に処理されるので、その処理にあった形にしなければならないというわけです。この「あった形」に処理された関数名を「装飾名」と呼びます。
 装飾名には主に__cdecl__stdcallのふたつがあります。
 デフォルトは__cdeclで、特に指定されていない関数は、常にこのタイプの装飾名に変換されます(この「デフォルト」は、「プロジェクト」−「設定」ダイアログの「C/C++」−「コード生成」ページの「呼び出し規約」で変更できます)。
 SDKの取り決めで、DLLからエクスポートされる関数は、__stdcallのタイプの装飾名でなければならないとされています。DLLとExe、違うファイルが関数を呼び出すのですから、関数のタイプも決めておかなければならないというわけです。

 この辺は、基本的には特に重要ではないので気にしないでください。「普通は__cdecl、DLLからエクスポートする関数は__stdcall」という風に憶えておけばいいでしょう。
(ところで、__fastcallのメリットってなんなんでしょうか。呼び出しが速そうに見えますが、実際はどうなんでしょう。C++Builderはほとんどすべての関数で使っていますが……)

関数のエクスポート
 さて、ここからが大変です。
 DLLの中の関数を他のDLLやExeから呼び出せるようにするには、エクスポートと呼ばれることをしなければなりません。
 関数をエクスポートすると、中にあるはずの関数が外に出て、外から見えるようになります。実際に、適当なDLLをテキストエディタやバイナリエディタで開いてみてください(VCなら「用途」を「バイナリ」にして開けばOK)。中に関数名が文字列として書かれています。これが「見える」ということです。外から使えるようにするためには、このようにしなければならないというわけです。

 エクスポートの方法はふたつあります。ひとつは定義ファイルを使う方法で、これは比較的簡単にできますが、関数が増えると面倒になります。もうひとつの方法は__declspec()を使う方法で、これは下準備が面倒ですが、一度してしまえば、関数を増やすのは楽になります。
 このふたつの方法のうち、どちらかひとつを使用します。

定義ファイル(.def)を使ったエクスポート
 「定義ファイル」とは、拡張子がdefのファイルのことです。プロジェクトを作製したときに、すでに存在していますね。
 定義ファイルの中身は、次のようになっていると思います。


; TestDLL.def : DLL 用のモジュール パラメータ宣言

LIBRARY      "TestDLL"
DESCRIPTION  'Test Windows Dynamic Link Library'

EXPORTS
    ; 明示的なエクスポートはここへ記述できます
	

 定義ファイルはDLLの作製には欠かせないファイルです。特に重要なのはLIBRARYEXPORTSの項目です。それ以外はとりあえず重要ではありません。ちなみに「;(セミコロン)」はコメントアウトです。

 LIBRARYの項目に書かれた文字列は、生成されるDLLファイルの名前です。この名前は、「設定」ダイアログの「リンク」−「一般」ページの「出力ファイル名」のファイル名と一致している必要があります。
 以前説明したように、Exe側の中に見つからない関数があると、ライブラリファイルを検索して、その中に見つかったら、その関数がどのDLLにあるのか書き留めておく、という手順を取ります。この「どのDLL」ということを示すのが、LIBRARYの項目ということです。
 また、決してこれはライブラリファイルの指定ではありません。出力するライブラリファイルのファイル名は、「設定」ダイアログの同ページ下の「プロジェクトオプション」の/implib:で指定された文字列です。この設定を変更して出力するライブラリファイル名を変えても、問題はありません。

 EXPORTSは、エクスポートする関数を列挙する部分です。ここに、次のように書き加えてください。


EXPORTS
	DllFunc		@1
	

 「DllFunc」は、エクスポートする関数の名前です。戻り値や引数は書きません。
 そのあとの「@」と数字は、序数値と呼ばれるものです。エクスポートされる関数は、関数名と同時に一意の整数が割り当てられます。この数字は関数ごとに変えなければならないので、関数が増えていくと次のようになります。


EXPORTS
	DllFunc			@1
	TestFunc		@2
	TempFunc		@3
	FunyoFunc		@4
	

 これが、定義ファイルを用いてのエクスポートのめんどくささです。エクスポートする関数が増えるごとに、定義ファイルに書き込んで、序数値を増やしていかなければなりません。
 ただ、実はこれでDLLは完成です。つまり、定義ファイルにちょっと書き加えるだけで、関数のエクスポートができてしまうのです。

 さて、ではもうひとつの方法を見てみましょう。今紹介した「定義ファイルを用いた方法」は、最初は楽でもあとで大変になってきます。次に紹介するのは「最初は大変だけどあとは楽」というものです。

__declspec()を使ったエクスポート
 もうひとつの方法は、__declspec()というキーワードを使うものです。このキーワードは、ウィンドウズ特有の拡張機能を関数やクラスに持たせたいときに使うものです(Declare Specialの略?)。
 何度も言っているように、この方法は下準備がちょっと面倒です。

 まず、「ファイル」−「新規作製」で、Defs.hというヘッダーファイルを作製して、そこに次のように書き込んでください。


#ifndef __DEFS_H__
#define __DEFS_H__

#ifdef DLL_EXPORT_DO		//これは StdAfx.h で定義されています。
   #define DLL_EXPORT			__declspec(dllexport)
#else
   #define DLL_EXPORT			__declspec(dllimport)
#endif

#endif	//__DEFS_H__
	

(注:このファイルは必要ない場合があります。同様の定義がMFCでもされていて、DLL_EXPORTAFX_EXT_CLASSに置き換えれば以下のコードは同様の機能を持つことになります。詳しくはAFX_EXT_CLASSのリファレンスと、AFXV_DLL.hAFXVER_.HといったMFCのヘッダーファイルを見てください。
 「エクスポートされるフラグ」は、_AFXDLL_AFXEXTの両方が定義されているとき、となっています。この定義は「拡張DLL」の場合、「設定」ダイアログの「C/C++」−「一般」ページの「プリプロセッサの定義」に書かれています。
 問題なのは、拡張DLL1を拡張DLL2から使用する場合、DLL2でもDLL1の関数がエクスポートされてしまうということです。DLL2でも、このふたつの定義は行われているのですから。
 今回の場合にはAFX_EXT_CLASSを使っても問題はないのですが、仕組みを憶えてもらうため、また、将来独自クラスをDLLに入れるかもしれないことを考えて、このようなファイルを作製して、ちゃんと説明することにしたというわけです)
 次に、StdAfx.hの中に次のコードを書き加えてください。


#define DLL_EXPORT_DO	//クラスをエクスポートします。

// この辺でいーでしょ。

//{{AFX_INSERT_LOCATION}}
// Microsoft Developer Studio は前行の直前に追加の宣言を挿入します。
	

 さらに、Func.hDllFunc()の行を次のように書き換えてください。


DLL_EXPORT int WINAPI DllFunc( CString &p_rcStr, CWnd *p_pcWnd );
	

 とどめに、Func.cppの最初の方に、次の行を加えてください。


#include "StdAfx.h"
#include "Defs.h"	//この行を加えてください。
#include "Func.h"
// 順番チョー大事!! 変えないようにね。
	

 ふう。これで終わりです。ビルドすれば、DllFunc()のエクスポートされたDLLが作製されるでしょう。

 ホントは、関数のエクスポートそのものはFunc.hの変更の部分だけで済んでしまいます。つまり、__declspec()を使ったエクスポートの場合、次の変更だけでできてしまうのです。


__declspec(dllexport) int WINAPI DllFunc( CString &p_rcStr, CWnd *p_pcWnd );
	

 つまり、関数のプロトタイプ宣言の頭に__declspec(dllexport)を付けるだけで、エクスポートはできてしまうのです。では、なんでこんな面倒なことをしているかというと、それは「2重エクスポート」を避けるためです。

2重エクスポートを避ける
 Exe側でいざ使おうという時に、__declspec(dllexport)を直接書き込んだFunc.hをインクルードしたら、どうなるでしょうか。そう! Exeからもエクスポートされてしまうのです。
 一応、__declspec(dllexport)を消したヘッダーファイルも別に用意しておけばいいんですが、そんな面倒な方法は採りたくありません。そこで、プリプロセッサを使って、自動的に処理させようということで、面倒な方法を採ったということです。

 DLL作成時には、StdAfx.hをインクルードします。このときDLL_EXPORT_DOのスイッチが入ります。
 このスイッチが入っているとき、DLL_EXPORTという単語を__declspec(dllexport)に置き換えます。
 その結果、__declspec(dllexport) int WINAPI DllFunc()というプロトタイプ宣言となり、関数はエクスポートされるというわけです。

 次にExe側で使用する場合を考えてみます。このとき、Exe側はDefs.hFunc.hをインクルードします。つまりStdAfx.hインクルードしません
 当然DLL_EXPORT_DOのスイッチは入いらず、そのためDLL_EXPORT__declspec(dllimport)に置き換えられます。
 結果、__declspec(dllimport) int WINAPI DllFunc()というプロトタイプ宣言となり、関数はエクスポートされないというわけです。

  StdAfx.h DLL_EXPORTの置換
DLLの作製時 インクルード __declspec(dllexport)
Exeの作成時 使用せず __declspec(dllimport)

 とまぁ、このように「DLLから」インクルードするときと「Exeから」インクルードするときとで関数のプロトタイプ宣言を変更するシステムを作るために、このような面倒な方法を採ったというわけです。
 でもここからは簡単。作った関数のプロトタイプ宣言の頭にDLL_EXPORTを付けていくだけで、その関数はエクスポートされます。

出力される装飾名について
 エクスポートされた装飾名(コンパイル後の関数名)には、注意が必要です。
 定義ファイルを用いてエクスポートしたとき、装飾名は関数名と一致します。つまり、前回説明した実行中ロード::GetProcAddress()の第2引数にそのまま関数名(ここではDllFunc)を渡せるということです。

 ところが、__declspec(dllexport)を用いたエクスポートを行うと?DllFunc@@YGHAAVCString@@PAVCWnd@@@Zといったえらいへんちくりんな装飾名になってしまいます(実際にDLLの中を見てみて、確認してみることをお奨めします)。
 このため、::GetProcAddress()には、このへんちくりんを渡さなければなりません。これでは、どのような装飾名になっているか確認してから呼び出す必要が出てきます。

 このような装飾名になってしまう理由のひとつが、C++です。C++は関数のオーバーロードという機能があり、同名で引数の違う関数を作製することができます。そのため、関数名だけの装飾名は作製されず、単純な関数があのようなへんちくりんな装飾名になってしまうのです。
 そこで、「C++のプログラムですが、C言語の関数です。オーバーロードなんかしません」とはっきりと示すキーワード、extern "C"を使ってみましょう。
 Func.hで、DllFunc()のプロトタイプ宣言の前後に、次のコードを追加してください。


// この下4行を追加。
#ifdef __cplusplus
  extern "C"
  {
#endif

DLL_EXPORT int WINAPI DllFunc( CString &p_rcStr, CWnd *p_pcWnd );

// この下3行を追加。
#ifdef __cplusplus
  }
#endif
	

 さらに、Func.cppを次のように書き換えてください。


extern "C" int WINAPI DllFunc( CString &p_rcStr, CWnd *p_pcWnd )
{
	// 以下略。
	

 このようにしてextern "C"を付けることで、C++の中で明示的にC言語の関数を作製することができます。

 が、このようにしても、装飾名は_DllFunc@8のようになってしまいます。::GetProcAddress()には、この装飾名を渡さなければなりません。

エクスポートの方法 int DllFunc( CString &p_rcStr, CWnd *p_pcWnd )の装飾名
(.def) DllFunc
__declspec(dllexport) ?DllFunc@@YGHAAVCString@@PAVCWnd@@@Z
extern "C"
__declspec(dllexport)
_DllFunc@8

 ただ、この辺は::GetProcAddress()を使う時のみに問題になる部分です。extern "C"などは、関数のプロトタイプ宣言が書かれたヘッダーファイルに付いているため、呼び出し側でもそういった装飾を行ってからリンカが検索します。ですから、基本的には気にする必要のない部分でしょう。
 ただ、DLLのエクスポートの問題はたびたび話題に上りますし、また、これはVCでの話のため、他のコンパイラとはうまくいかない場合も出てくるので、そういった場合には色々と手を加える必要が出てきます。そういうときのためにも、こういった知識は持っておいて損はないでしょう。

出力されたファイル、そして……
 さて、DLLが完成しました。でも、よく分からないファイルが結構出てきて、どれをどうしたらいいのか……という感じの方もいるでしょう。
 そういったファイルの説明、そして自作DLLの使い方の詳細、デバッグ時の注意等を次回紹介します。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.