今回はDLLを作ってみましょう! いきなり色々作るのも大変なんで、今回は関数をエクスポートしたDLLを作ってみましょう。
|
関数を作る意味
DLLに関数を作る意味は、なんでしょう? はっきり言って意味はないです。今度説明する「DLLにクラスを作る方法」を使えば、関数を入れるためにDLLを作る必要はなくなるでしょう。 ただ、もちろんクラスとは別に関数をAPIのような形で置きたいという場合もあるでしょう。MFCにもAfxなんたらというような形で、普通の関数があります。そういう関数を起きたい場合には、必要な方法と言えるでしょう。 |
プロジェクトの作製
まずはプロジェクトから。プロジェクトは「MFC AppWizard (DLL)」を選んでください。DLLは、別にMFCを使わなくても作れますが、たぶん使った方がずっと楽ですので、今回は使うことにします。 プロジェクト名を決めて「OK」ボタンを押したら、1ページだけのウィザードダイアログが表示されます。ここで「MFC の拡張 DLL(MFC の共有 DLL 使用)」を選択して、「終了」ボタンを押してください。
MFCを使ったDLLには、ふたつのタイプがあります。 |
DllMain()
さて、プロジェクトができたら、同時に作製されたファイルを見てみましょう。おなじみのStdAfx.hの他に、リソースファイルがありますね。他には、プロジェクト名.cppと、見慣れないプロジェクト名.defがあると思います。 この内のプロジェクト名.cppを開いてみてください。その中にDllMain()という関数があると思います。
普通のExeファイルを作製するとき、必ずWinMain()という関数を作製します。ウィンドウズはExeファイルの中からこの関数を探しだし、呼び出すことでアプリケーションが始動します。
基本的に、この関数は「あればいい」だけで、プログラマー側で手を加えることは少ないでしょう。 |
ファイルの作製とコードの追加
では、実際に手を加えてみましょう。 「ファイル」−「新規作製」で「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に使用されています。
「修飾子」とは、コンパイル後の関数のタイプを決めるものです。コンピューターは当然内部では機械的に処理されるので、その処理にあった形にしなければならないというわけです。この「あった形」に処理された関数名を「装飾名」と呼びます。
この辺は、基本的には特に重要ではないので気にしないでください。「普通は__cdecl、DLLからエクスポートする関数は__stdcall」という風に憶えておけばいいでしょう。 |
関数のエクスポート
さて、ここからが大変です。 DLLの中の関数を他のDLLやExeから呼び出せるようにするには、エクスポートと呼ばれることをしなければなりません。 関数をエクスポートすると、中にあるはずの関数が外に出て、外から見えるようになります。実際に、適当なDLLをテキストエディタやバイナリエディタで開いてみてください(VCなら「用途」を「バイナリ」にして開けばOK)。中に関数名が文字列として書かれています。これが「見える」ということです。外から使えるようにするためには、このようにしなければならないというわけです。
エクスポートの方法はふたつあります。ひとつは定義ファイルを使う方法で、これは比較的簡単にできますが、関数が増えると面倒になります。もうひとつの方法は__declspec()を使う方法で、これは下準備が面倒ですが、一度してしまえば、関数を増やすのは楽になります。 |
定義ファイル(.def)を使ったエクスポート
「定義ファイル」とは、拡張子がdefのファイルのことです。プロジェクトを作製したときに、すでに存在していますね。 定義ファイルの中身は、次のようになっていると思います。 |
; TestDLL.def : DLL 用のモジュール パラメータ宣言
LIBRARY "TestDLL"
DESCRIPTION 'Test Windows Dynamic Link Library'
EXPORTS
; 明示的なエクスポートはここへ記述できます
定義ファイルはDLLの作製には欠かせないファイルです。特に重要なのはLIBRARYとEXPORTSの項目です。それ以外はとりあえず重要ではありません。ちなみに「;(セミコロン)」はコメントアウトです。
LIBRARYの項目に書かれた文字列は、生成されるDLLファイルの名前です。この名前は、「設定」ダイアログの「リンク」−「一般」ページの「出力ファイル名」のファイル名と一致している必要があります。
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_EXPORTをAFX_EXT_CLASSに置き換えれば以下のコードは同様の機能を持つことになります。詳しくはAFX_EXT_CLASSのリファレンスと、AFXV_DLL.hやAFXVER_.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.hのDllFunc()の行を次のように書き換えてください。
|
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のスイッチが入ります。
次にExe側で使用する場合を考えてみます。このとき、Exe側はDefs.hとFunc.hをインクルードします。つまりStdAfx.hをインクルードしません。 |
|
とまぁ、このように「DLLから」インクルードするときと「Exeから」インクルードするときとで関数のプロトタイプ宣言を変更するシステムを作るために、このような面倒な方法を採ったというわけです。 |
出力される装飾名について
エクスポートされた装飾名(コンパイル後の関数名)には、注意が必要です。 定義ファイルを用いてエクスポートしたとき、装飾名は関数名と一致します。つまり、前回説明した実行中ロードで::GetProcAddress()の第2引数にそのまま関数名(ここではDllFunc)を渡せるということです。
ところが、__declspec(dllexport)を用いたエクスポートを行うと?DllFunc@@YGHAAVCString@@PAVCWnd@@@Zといったえらいへんちくりんな装飾名になってしまいます(実際にDLLの中を見てみて、確認してみることをお奨めします)。
このような装飾名になってしまう理由のひとつが、C++です。C++は関数のオーバーロードという機能があり、同名で引数の違う関数を作製することができます。そのため、関数名だけの装飾名は作製されず、単純な関数があのようなへんちくりんな装飾名になってしまうのです。 |
// この下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()には、この装飾名を渡さなければなりません。 |
|
ただ、この辺は::GetProcAddress()を使う時のみに問題になる部分です。extern "C"などは、関数のプロトタイプ宣言が書かれたヘッダーファイルに付いているため、呼び出し側でもそういった装飾を行ってからリンカが検索します。ですから、基本的には気にする必要のない部分でしょう。
ただ、DLLのエクスポートの問題はたびたび話題に上りますし、また、これはVCでの話のため、他のコンパイラとはうまくいかない場合も出てくるので、そういった場合には色々と手を加える必要が出てきます。そういうときのためにも、こういった知識は持っておいて損はないでしょう。 |
出力されたファイル、そして……
さて、DLLが完成しました。でも、よく分からないファイルが結構出てきて、どれをどうしたらいいのか……という感じの方もいるでしょう。 そういったファイルの説明、そして自作DLLの使い方の詳細、デバッグ時の注意等を次回紹介します。 |
(C)KAB-studio 1998 ALL RIGHTS RESERVED. |