Japanese Only.

VC++.NET2003で作る/使うDLL

DLL製作しているとデバッグが非常に面倒・・・、なんだけど、さすがの統合環境、DLL製作がメチャ簡単(ま、覚えるまではメンドー)。そんなこんなで、まとめておきます。
#すまねぇっ。画像載せるほどホームページスペースがないの。全部で5MB、あぅ。
#あと、この方法がベストだとか、一般的かどうかは知らないので「アシカラズ」(言いたい放題宣言)

ソリューションとプロジェクト

まずは、いつも通り「新しいプロジェクト」からウィザードを使ってプロジェクトを作成していくんですが、最初は「Visual Studio ソリューション」から「空のソリューション」を選びます。今回はサンプルなんで、プロジェクト名は「Solution1」です。ソリューション エクスプローラに「ソリューション 'Solution1' (0プロジェクト)」とでてきます

次にDLL用のプロジェクトを作ります。ソリューション エクスプローラを右クリックして「追加」→「新しいプロジェクト」を選び(プロジェクト名は「mydll」)、テンプレートを「Win32 プロジェクト」または「Win32 コンソール プロジェクト」にします。「OK」してから「アプリケーション ウィザード」の「アプリケーションの設定」から「DLL」を選び、「シンボルのエクスポート」にチェックを入れます。で、完了。とりあえず、これでDLLプロジェクトは完成です。

いま作ったDLL用のテストプロジェクトを作成します。今回は「Win32コンソールプロジェクト」で作成しています。特に設定はいらないので、プロジェクトをウィザードをさっさと終わらせます。あ、プロジェクト名は「mydlltest」です(捻り無し)。

ちょっとした設定

このままでもビルド成功しますが、DLLのテストをしたいので、テストプロジェクトに、ちょっとコードを足します。

 1 : // mydlltest.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
 2 : //
 3 : 
 4 : #include "stdafx.h"
 5 : 
 6 : int _tmain(int argc, _TCHAR* argv[])
 7 : {
 8 :   Cmydll hoge;
 9 :   return 0;
10 : }
11 : 

追加したのは8行目の Cmydll hoge; だけです。この「Cmydll」は、mydllプロジェクトの「mydll.h」内に書かれているクラスです。このクラスは、mydllプロジェクトを作ったときにウィザードが勝手に作ったものです。

このままビルドしたいですが、「\Solution1\mydlltest\mydlltest.cpp(8): error C2065: 'Cmydll' : 定義されていない識別子です。」となります、当然ですが。そこで、まずこのCmydllクラスが宣言されているヘッダをincludeします

 1 : // mydlltest.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
 2 : //
 3 : 
 4 : #include "stdafx.h"
 5 : #include "mydll.h"
 6 : 
 7 : int _tmain(int argc, _TCHAR* argv[])
 8 : {
 9 :   Cmydll hoge;
10 :   return 0;
11 : }
12 : 

増えたのは、5行目です。さて今回のエラーは「\Solution1\mydlltest\mydlltest.cpp(5): fatal error C1083: include ファイルを開けません。'mydll.h': No such file or directory」です。ま、これも当たり前の話で、mydlltestプロジェクト、つまり「\Solution1\mydlltest」には、mydll.hというファイルがありませんから。gccなら「-I」オプションで追加のインクルード ディレクトリを指定できます、もちろんVisual C++でも同じことがGUIで設定可能です。

ソリューション エクスプローラの「mydlltest」で右クリックして「プロパティ」を出します。「構成プロパティ」から辿って「C/C++」→「全般」にある「追加のインクルード ディレクトリ」に、相対パスで「..\mydll」と指定します。絶対パスにすると、ちょっとソリューションのパスを変えたときにツライですから。

で、まったまったエラーは出ます。「mydlltest error LNK2019: 未解決の外部シンボル "__declspec(dllimport) public: __thiscall Cmydll::Cmydll(void)" (__imp_??0Cmydll@@QAE@XZ) が関数 _main で参照されました。」となります。これは、宣言はされてるけど定義されてない(Cmydllの)コンストラクタが参照されてんぞっ、と怒られているわけです。が、これ自分で定義できるわけないので、やっぱり設定を変えていきます。

ソリューション エクスプローラの「mydlltest」で右クリックして、「プロジェクトの依存関係」の項目を選びます。ダイアログが出てきて、プロジェクトが「mydlltest」となっているのを確認して、依存先の「mydll」プロジェクトにチェックします。これで、mydlltestプロジェクトはmydllプロジェクトに依存しているのだということをVisual C++さんに教えてあげます。こうしないと、mydlltestプロジェクトが先にビルドされてしまい、まだmydllプロジェクトで定義されていないコンストラクタを参照することになり、結果「未解決じゃーっ」といわれてビルド失敗するわけです。

テスト実行

これまでの設定でビルドは通るようになりました。で、実行すると「mydll−デバッグ セッションで実行可能」というダイアログが出てくることがあります(出てこないなら、それでいいんだけど)。このときは、「実行可能ファイル名」のところにmydlltestプロジェクトの実行ファイルを指定します。で、実行すると、「mydll.dllが見つからなかったため、このアプリケーションを開始できませんでした。アプリケーションをインストールし直すとこの問題は解決される場合があります。」というメッセージが飛び出します。これは、テストプロジェクトの実行ファイルと同じディレクトリ(または環境パスの通ったディレクトリ)に「mydll.dll」が置かれていないからです。で、ここで最後の設定をします。

mydllプロジェクトのプロパティを開き、全般の出力ディレクトリを「Debug」から「..\mydlltest\Debug」に修正します。これで、dllがmydlltestプロジェクトの出力ディレクトリに生成されます。これで、無事に実行できます。逆にテストプログラムをdll側に出力するのは、dllが増えてきたときにダメになるので、dllをテストプログラム側に出力するようにしましょう。あ、デバッグ実行(F5)すると一瞬でウィンドウが閉じちゃうので、デバッグ無し実行(Ctrl+F5)で試してみましょう。

これでも同じエラーが出る場合は、mydllプロジェクトのプロパティで、全般のコマンドを書き換えるか消します。

いつものアレ

さ、これでようやく設定は終わりです。ま、このままじゃ味気ないので、いつものアレをやってみます。

mydllプロジェクトのCmydllクラスに「void PrintHello(void)」関数メンバを追加します。中身はもちろん「Hello World」の出力です(笑)。まず、ヘッダはこんな感じです

 1 : // 以下の ifdef ブロックは DLL からのエクスポートを容易にするマクロを作成するための
 2 : // 一般的な方法です。この DLL 内のすべてのファイルは、コマンド ラインで定義された MYDLL_EXPORTS
 3 : // シンボルでコンパイルされます。このシンボルは、この DLL を使うプロジェクトで定義することはできません。
 4 : // ソースファイルがこのファイルを含んでいるほかのプロジェクトは、
 5 : // MYDLL_API 関数を DLL からインポートされたと見なすのに対し、この DLL は、このマクロで定義された
 6 : // シンボルをエクスポートされたと見なします。
 7 : #ifdef MYDLL_EXPORTS
 8 : #define MYDLL_API __declspec(dllexport)
 9 : #else
10 : #define MYDLL_API __declspec(dllimport)
11 : #endif
12 : 
13 : // このクラスは mydll.dll からエクスポートされました。
14 : class MYDLL_API Cmydll {
15 : public:
16 :   Cmydll(void);
17 :   // TODO: メソッドをここに追加してください。
18 : };
19 : 
20 : extern MYDLL_API int nmydll;
21 : 
22 : MYDLL_API int fnmydll(void);
23 : 

次に定義のほうは、こんな感じ

 1 : // mydll.cpp : DLL アプリケーションのエントリ ポイントを定義します。
 2 : //
 3 : 
 4 : #include "stdafx.h"
 5 : #include "mydll.h"
 6 : BOOL APIENTRY DllMain( HANDLE hModule, 
 7 :                        DWORD  ul_reason_for_call, 
 8 :                        LPVOID lpReserved
 9 :            )
10 : {
11 :   switch (ul_reason_for_call)
12 :   {
13 :   case DLL_PROCESS_ATTACH:
14 :   case DLL_THREAD_ATTACH:
15 :   case DLL_THREAD_DETACH:
16 :   case DLL_PROCESS_DETACH:
17 :     break;
18 :   }
19 :     return TRUE;
20 : }
21 : 
22 : // これは、エクスポートされた変数の例です。
23 : MYDLL_API int nmydll=0;
24 : 
25 : // これは、エクスポートされた関数の例です。
26 : MYDLL_API int fnmydll(void)
27 : {
28 :   return 42;
29 : }
30 : 
31 : // これは、エクスポートされたクラスのコンストラクタです。
32 : // クラス定義に関しては mydll.h を参照してください。
33 : Cmydll::Cmydll()
34 : { 
35 :   return; 
36 : }
37 : 

書き加えて、実行してください。あ、もちろんmydlltest.cppでPrintHelloを呼び出すのを忘れないでくださいよ。それと、nmydllとかfnmydllとかは消しても構いません。や、むしろいらないものは消しましょう。DllMainは消してはいけません。理由は・・・、何だ?知らないっす。

落穂拾い

DLLの開発が進むと、「○○で既に定義されています」なんて出る場合があります。こんなときは、プロジェクトの設定から「リンカ」→「入力」の「特定のライブラリの無視」に、その「○○」をセミコロン区切りで追加していきます。たとえば「LIBCで既に〜」なら「LIBC」を、さらに「libcpmt.libで既に〜」なら「LIBC;libcpmt.lib」とします。

拾いきれない落穂拾い

C++の標準ライブラリを使おうとすると、「\Solution1\mydll\mydll.h(24) : warning C4251: 'Cmydll::msg' : class 'std::basic_string<_Elem,_Traits,_Ax>' は __export キーワードを使って class 'Cmydll' にエクスポートしてください。」などと警告が出てきます(今回はstd::string)。この警告を消すには、

#pragma warning(disable:4251)
	string err;
#pragma warning(default:4251)

として、無理矢理に警告を消してあげます。他に方法がないのかを知らないのですが、どうも私が調べた限りでは、この方法がメジャーなようです。std::vectorやstd::listなどのようなものは、さらにテンプレートを使っているので、なおさら面倒なようで・・、ここでは扱わないことを御容赦ください。STLを使う場合などは、どうもスタティック リンクライブラリで作成するほうが私には合ってるかもしれません。DLLって結局、その利点が実は危険だったりしますし。

なかなか、DLLって便利なんですけど、DLLを作る側も使う側も、プログラミングとは関係ない部分でコンパイルエラーなんてところで詰まってしまった、「うざったい」なんて思うわけですね。。そんなこんなで、少しでもお役に立てればスタンディング・オーベイションで歓迎の嵐に放り込んでやってください(少しなのに)。

Comments are welcome! We can be reached at whoinside_reshia@hotmail.co.jp
2005/07/03 13:30:35 UTC