DLLを作ろう!(クラス編)

 前々回はDLLに関数を作製しました。今回はクラスを作製します。
 で、クラスを作るだけだとかなり簡単なので、デバッグバージョンとリリースバージョンについての解説もしたいと思います。

プロジェクトの作製
 前々回と同じく、「MFC AppWizard (DLL)」を作製し、ウィザードダイアログで「MFC の拡張 DLL(MFC の共有 DLL 使用)」を選択して、「終了」ボタンを押せばOK。ってゆーか、今回は前々回のをそのまま使いますので、まだ見てない方はそちらからご覧ください。

クラスの作製
 最初に、エクスポートするクラスを作製します。
 ワークスペースの「Class View」を表示し、一番上の「* クラス」(*はプロジェクト名)を右クリックし、「クラスの新規作製」を選んでください。
 今回はテストなので、基本的にはどんなクラスでもOKです(実際のクラスの作製方法についてはクラスを作ろう!を参考にしてください)。とりあえず「クラスの種類」を「Generic クラス」にして、クラス名を「CTestCls」とでもしてください。
 作製したら、テスト用にメンバ関数を追加しておきます。「* クラス」のすぐ下の「CTestCls」の上で右クリック、「メンバ関数の追加」を選び、「関数の型」をint、「関数の宣言」をDllFunc( CString &p_rcStr, CWnd *p_pcWnd )としてください。「アクセス制御」はそのままpublicです。
 そして、今作った関数に次のコードを書き込んでください。


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

 関数そのものは前々回のものと同じなので解説はいいですね。単にメッセージボックスを表示するものです。
 さて、準備はできました。では今作った関数をエクスポートしてみましょう。

クラスのエクスポート
 まずクラスのソースファイル(ここではTestCls.cpp)で次のようにDefs.hをインクルードしてください。


#include "stdafx.h"
#include "Defs.h"	//ここを追加。
#include "TestCls.h"
	

 順番がヒジョーに重要なことに注意してください。Defs.hに書かれてるDLL_EXPORTは、TestCls.hで使用するので、TestCls.hをインクルードする前にDefs.hをインクルードする必要があるからです。
 次に、クラスの定義が書かれているファイル(ここではTestCls.h)を次のように書き換えてください。


// この下を書き換えてください。
class DLL_EXPORT CTestCls  
{
public:
// 以下略。

	

 と、以上のようにclass「クラス名」の間に__declspec(dllexport)を挟み込む形にします。これで、クラスそのものがエクスポートされますので、メンバ関数をひとつずつエクスポートする必要はありません。
 で、クラスのエクスポートはこれで終了。ビルドしてみてください。

 あと、前回の「標準インクルードファイル」にも次のように書き加えておいてください。


// ヘッダーファイルを読み込みます。
#include <Defs.h>
#include <Func.h>
#include <TestCls.h>
	

 これでDLL側の準備はひとまず終了しました。今度はExe側に回って、使ってみましょう。

自作クラスを使う
 Exe側は、これも前回のものを流用してください。流用しているのなら、「標準インクルードファイル」はインクルードしてあるはずなので、特に何かする必要はありません。また、「検索ディレクトリ」もそのままなら、大丈夫でしょう。
 あとは、先ほど作製したDLLをExeにコピーして準備はOK。コピーの自動化もしてあるのなら、これすら必要ないでしょう。

 では、実際に使ってみましょう。といっても、例えばダイアログクラスの中で次のようなコードを追加するだけです。



// IDC_BTN_DLL_CLS のボタンを押したときのメンバ関数。

void CMy01Dlg::OnBtnDllCls() 
{
	CTestCls	cTest;	//DLLからエクスポートされたクラスです。
	CString	cStr = "てすと2";

	cTest.DllFunc( cStr, this );	//メッセージボックスの表示。

}
	

 このように、ごく普通に使用できるわけですね。ま、MFCなんかはそうなんですから、できて当然といったところでしょうか。

 さて、再びDLLの方へと戻って、もう少し突っ込んだ内容に入ってみましょう。

__declspec(dllexport)を使う理由
 前々回、「エクスポートの方法はふたつある」という解説をしましたが、今回は有無を言わさず__declspec(dllexport)を使ったエクスポートを紹介しました。というのも、実は定義ファイルではクラスのエクスポートが難しいという問題があるからです。

 まず、基本的に「エクスポート」はクラス単位ではできません。エクスポートされるのは関数や変数としてです。クラスというのは、一種の「変数の型」のようなものなので、エクスポートの対象とはならないのです。
 そのため、メンバ関数が膨大になった場合、そのすべてを定義ファイルに書き込まなければなりません。もちろんそれは普通の関数でも同じことなのですが、実際には普通の関数よりもずっと難しくなります。それは、「装飾名」の問題です。

 定義ファイルを使用してエクスポートする場合、普通の関数なら関数名と同じで良かったのですが、クラスのメンバ関数の場合にはC++の装飾名となってしまうため、「へんちくりん」な装飾名となってしまうのです。
 例えば、今回作製したCTestCls::DllFunc()の場合には?DllFunc@CTestCls@@QAEHAAVCString@@PAVCWnd@@@Zというへんちくりんなものになります。
 これはExpファイルに書き込まれていますが、これをすべて調べて定義ファイルに書き込むというのは相当大変な仕事になります(もちろん、プログラムで自動化すればいいんですけどね。そういうツールもあります)。

 こういった問題を、__declspec(dllexport)は簡単に解決してくれます。なんと言っても、クラスを丸ごとエクスポートしてくれるのですから。というわけで、こちらだけを紹介したというわけです。

エクスポートとインポート
 これまでは「エクスポート」を中心に説明してきましたが、今度は「インポート」について見てみましょう。
 DLL_EXPORTを用いたエクスポートの場合、フラグによってふたつの文字に替えられることは説明しましたね。

 DLLの場合、StdAfx.hの中でDLL_EXPORT_DOのフラグを立てているため、クラスの宣言はclass __declspec(dllexport) CTestClsとなります。これによって、クラスはエクスポートされます。
 これをExe側で使用する場合、DLLのStdAfx.hは読み込みませんので、フラグは立ちません。その結果、クラス宣言はclass __declspec(dllimport) CTestClsとなります。これが、「インポート」と呼ばれるものです。

 通常、DLLの関数やクラスは、そのヘッダーファイルを読み込んで、実体が見つからない場合にライブラリファイル等を検索するという順序を取ります。
 このDLLや関数に明示的にインポートすると、すぐさまライブラリファイルを検索します。Exe内での検索を行わずに済むというわけです。

 実は、通常のAPIも、このようにインポートを行っています。Winbase.h等を見ると、APIのプロトタイプ宣言の頭にWINBASEAPIという単語が付いています。そう、これが__declspec(dllimport)に替わるのです。
 APIは定義ファイルを使用してエクスポートを行っているため、__declspec(dllexport)は使用しませんが、インポートはちゃんと__declspec(dllimport)を使用しているのです。
 

デバッグ版とリリース版
 さて話は変わって、MFC42.dllMFC42d.dllのように、デバッグ版とリリース版のふたつのDLLを作製してみます。そのために、このふたつについて確認しておきましょう。

 VCでプロジェクトを作ると、「デバッグ版」と「リリース版」のふたつの「コンフィグ」がデフォルトで作られます。
 「コンフィグ」は「コンフィグレーション(Configuration)」の略です。VCの日本語リファレンスでは「構成」という訳を使用していますが、なんか感じが掴めないので英語原文の単語を使用します。
 コンフィグは「プロジェクトの設定」のスイッチ設定のことです。スイッチ設定の違うアプリケーションをいくつか作りたいときに、新しいコンフィグを作り(「ビルド」−「構成」)、それぞれ望みのスイッチにし、ビルドを行うコンフィグを変更(「ビルド」−「アクティブな構成の設定」)してからビルドすれば、それぞれのコンフィグでアプリケーションが作製されます。
 で、「デバッグ版」と「リリース版」は単なるデフォルトのコンフィグで、スイッチもそれぞれ「デバッグに適切だと思われるスイッチ」「リリースに適切だと思われるスイッチ」がセットされているというだけです。だから、例えばコンフィグが「リリース版」でも、「設定」ダイアログの「C/C++」−「一般」ページ「デバッグ情報」「プログラムデータベースを使用」にし、さらに「リンク」−「デバッグ」ページ「デバッグ情報」のスイッチを入れることで、プログラムデータベースが作製されデバッグができるようになります。
 同様のスイッチが「デバッグ版」にもあること、そして「デバッグ版」でこのスイッチを解除するとデバッグができなくなることを確認してください。

 「デバッグ版」と「リリース版」の違いは、一見しただけでは分かりませんが、実際には大きく違います。
 すべての基本は「C/C++」−「一般」ページ「プリプロセッサの定義」_DEBUGスイッチです。この欄に書かれたスイッチは、プリプロセッサによって#defineを用いて定義したとみなされます。それによって、多くの文字列が置き換えられます。
 例えば、newdeleteは、このスイッチが入っていると、メモリリーク等を検出するための機構が働いたり例外が発生しにくいよう余分に領域を取ったりといった特別な処理を実行します。
 また、外部DLLの中の関数にはデバッグにしか使用しないものもあります。この場合、ヘッダーファイルやDLL、ライブラリファイルが違う場合があります。
 これらが、すべて_DEBUGのスイッチによって制御されます。

 通常、デバッグ版の関数は余分な処理を行っているため、動作が幾分遅くなります。また、デバッグ版の関数が入っているDLLは、一般のウィンドウズにはインストールされていません。こういった事情から、基本的に「デバッグ版」は配布しないことになっています。
 で、自作DLLを作製する場合、デバッグ版とリリース版のDLLが同じ名前だと、管理が難しくなります。そこで、DLLのファイル名をふたつで変えてみましょう。

ファイル名を変えるための変更
 まず例によって「設定」ダイアログの「リンク」ページを開いてください。次に、ダイアログ左上の「設定の対象」を、デバッグ版のコンフィグ名(デフォルトでは「Win32 Debug」)にしてください。

 次に、「プロジェクトオプション」の欄を書き換えます。
 「C/C++」のページもそうですが、「プロジェクトオプション」より上の設定は、「プロジェクトオプション」を解りやすく設定するためのものです。「コンパイル」はCL.exe、「リンク」はLINK.exeというコンソールベースのアプリケーションが処理します。これらのアプリケーションに渡すパラメーターが、「プロジェクトオプション」です。
 「プロジェクトオプション」より上の設定はあくまでパラメーターを解りやすく見せているだけで、中にはプロジェクトオプションでしか設定できないスイッチもあります。これから説明するいくつかはそういったものです。

 前置きが長くなりました。まず「一般」ページの出力ファイル名を、デバッグ版のものに変えてください。例えばデフォルトのファイル名がTestDLL.dllなら、これをTestDLLd.dllにしてください。
 そうしたら、「プロジェクトオプション」をクリックしてみてください。パラメーターが書き換えられたでしょう。/out:/implib:、そして/pdb:のファイル名が、上で設定したものに替わっていると思います。
 でもまだ十分ではありません。/def:".\TestDLL.def"の欄を、/def:".\TestDLLd.def"に書き換えてください。これは見ての通り、関数のエクスポートに使用する「定義ファイル」の設定です。

デバッグ版用の定義ファイル
 前々回の定義ファイル(拡張子がdefのファイル)を思い出してください。LIBRARYパラメーターでDLLのファイル名を指定する必要があります。デバッグ版とリリース版ではファイル名が違うわけですから、デバッグ版とリリース版のふたつ必要ということになります。リリース版はデフォルトのものを使えばいいので、デバッグ版を新たに作製しましょう。

 まずフォルダ等で定義ファイル(今回の例ではTestDLL.def)を複製して、TestDLLd.defというファイル名にしてください(前述の「プロジェクトオプション」に指定したファイル名です)。
 作製したら、「プロジェクト」−「プロジェクトへ追加」−「ファイル」ダイアログを開き、「ファイルの種類」で「定義ファイル(.def)」を選び、先ほど作製したTestDLLd.defを追加してください(警告が出るかもしれませんが、気にしないでください)。

 追加したら、「プロジェクトワークスペース」の「ファイルビュー」にこのファイルがあると思うので、ダブルクリックして開き、次のように書き換えてください。


LIBRARY      "TestDLLd"	;最後の"d"を書き加えてね
	

 こうすることで、この定義ファイルは、エクスポートされた関数がTestDLLd.dllにあるということをライブラリファイルに書き込みます。

 でも、まだあと一仕事あります。先ほど警告が出てきた人は「定義ファイルはひとつの構成でひとつしか使えません」と書かれていたことでしょう。出力するDLLはひとつのコンフィグで一種類なので、定義ファイルもひとつだけでなければなりません。そこで、コンフィグごとに定義ファイルを変更してみましょう
 例によって「プロジェクト」−「設定」ダイアログを開いてください。今回はページは関係ありません。今回はダイアログの左半分を使用します。
 この部分の「プロジェクトに組み込まれたファイル一覧」のSource fileの中に、ふたつの定義ファイル(ここではTestDLL.defTestDLLd.def)の両方があると思います。でも、おそらくアイコンが違うでしょう。非コンパイルのアイコンコンパイルのアイコンのどちらかがあると付いているでしょう。
 これは、非コンパイルのアイコンは「このコンフィグではコンパイルしないファイルです」という意味です。
 また、コンパイルのアイコンは「このコンフィグではコンパイルするファイルです」という意味です。

 ひとつのコンフィグではひとつの定義ファイルしか使用できないわけですから、例えばデバッグ版ではTestDLLd.defコンパイルし、TestDLL.defコンパイルしないことにすればいいわけです。
 この「コンパイルする、しない」の設定は、変更するファイルを左クリックしたあと、右側のページを「一般」にして、「このファイルをコンパイルしない」のチェックを付ける、消すことで変更できます。このチェックボックスを使って、使用する定義ファイルを選んでください。まとめると、次のようになります。

デバッグモード 非コンパイルのアイコンTestDLL.def コンパイルのアイコンTestDLLd.def
リリースモード コンパイルのアイコンTestDLL.def 非コンパイルのアイコンTestDLLd.def

 以上のようになるように、コンフィグを設定してください。
 で、ここまでくれば終わりです。DLLをビルドしてください。デバッグ版はデバッグ版のDLLとライブラリファイルが、リリース版はリリース版のDLLとライブラリファイルが作製されるはずです。

 デバッグ版とリリース版を分ける方法についてまとめてみましょう。
 コンフィグのファイル名を変える:DLLとライブラリファイルは簡単に変えられますが、定義ファイルは「プロジェクトオプション」を直接変える必要がありましたね。
 デバッグ用の定義ファイルを複製する:これは複製コピーして、プロジェクトに追加するだけでした。
 定義ファイルの"LIBRARY"を変える:DLLのファイル名が、コンフィグと一致しなければならないからでした。
 コンフィグごとにコンパイルする定義ファイルを変える:アイコンに注意してください。

 うまくいってなくても必ずコンパイルエラーが出るので、そうしたら手順を追って調べてみてください。

「標準インクルードファイル」の変更
 以上で「DLL」側は終わったのですが、最後に使用する側について書いておきます。
 デバッグ版とリリース版のDLLを作製した場合、Exe側でも変更する必要があります。それは、ライブラリファイルの検索リストです。
 DLLをExeから使用するときに必要なのは、DLLではなくライブラリファイルです。で、今回ライブラリファイルは2種類あるわけですから、Exeがデバッグ版ならデバッグ版の、リリース版ならリリース版のライブラリファイルを検索リストに加える必要があります。この使い分けを行うことで、実行時のDLLも自動的に使い分けられるというわけです。

 コンフィグを使用して検索リストに加えている場合には、やっぱり「プロジェクト」−「設定」ダイアログの「リンク」−「一般」ページのオブジェクト/ライブラリモジュール」で、コンフィグに合わせてライブラリファイルを変更してください。
 また、これまでの例のように「標準インクルードファイル」を使用している場合には、そのライブラリファイルの検索リストを次のように書き換えてください。


// ライブラリファイルを読み込みます。
#ifdef _DEBUG
	#pragma comment(lib, "TestDLLd.lib")
#else
	#pragma comment(lib, "TestDLL.lib")
#endif
	

 今回の「デバッグ版とリリース版」の部分で説明したように、デバッグ版のコンフィグでは_DEBUGというスイッチが入るように設定されています。これはExeも同じです。このスイッチが入っているかどうかによって、検索するライブラリファイルを変更するというわけです。
 これで使用する側もOKです。DLLのコピーや、「検索ディレクトリ」の設定等は忘れずに。

DLLのまとめ
 さて、今回を持ちましてひとまずDLLの話は終了します。
 最後の今回は、コンフィグなど比較的解りにくい部分を解説してみました。そのためにちょっと全体的には分かりにくくなってしまったかもしれません。KCL9542がサンプルになると思うので、そちらも試してみてください。

 で、次回からは「フック」という技術について見ていこうと思います。フックからシステムフック、そしてサブクラス化と、きっとこちらの方が興味深い内容になると思います。とりあえず、期待しててください。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.