VS2003を使って外部呼出し可能なDLLの作成方法

先日久しぶりにWin32アプリケーションをC++で作ろうとしたらいろんなことをまったく覚えてなくてかなりびっくりしました。
普段、VS2008の環境でC#を使っていたのが影響したのだと思いますが、それにしてもひどい忘れようでして、まるで初めてやることのように苦労してしまいました。
で、どうしたのかというと入社2年目くらいの時にWin32DLLをC++で開発をするという仕事が回ってきた時に調べた内容が備忘録としてまとめていてこれを参考にしました。それ以前はDelphiで実行可能形式のプログラムしか作っていなかったので、DLLは作ったことも使ったこともなかったので非常に苦労したのを覚えています。
あとで確認出来るようなかたちでまとめてあるというのは我ながらとてもえらいと褒めてあげたいところですが、褒めるついでにせっかくなのでこれをブログにも転載しておこうと考えました。
非常に初歩的な資料なので「こんなの常識だよなあ...」とも考えたのですが、高度なノウハウよりもこういう当たり前過ぎて暗黙知になっているようなことの方が意外に需要があるということを最近のアクセスログを見て知ったので思い切って載せることにしました。この内容は社内のWikiには転載済みなので、それをベースにして記事にしたので構文の誤りなどもあるかも知れません。
もし誤記やおかしな記述などあればご指摘いただけると嬉しいです。

[追記]
実際に記事を見てみたらあまりに長いので「続きを読む」形式にしました。


1. テスト環境

手元にあったのはかなり昔の資料なので今回記事にするにあたって再度画面のイメージを取り直しました。
加えてテストも再度実施したのでその確認をした環境についてまずはまとめます。

種別 内容
OS Windows XP SP2
開発環境 VS.NET 2003
実行ユーザ itotto (Administrators)


VS.NET2003については特にlibやヘッダーファイルの追加指定は必要ありません。インストール直後の環境で十分です。

2. テストの下準備

(1) VS.NET2003を起動してください
(2) 空のソリューションを作成します

    • 1) 「ファイル」→「新規作成」→「空のソリューション」を選択します
    • 2)「プロジェクト名」にソリューション名を入れて,「場所」は格納したい場所を入れて「OK」ボタンを押してください


(3) テストを駆動するexe用プロジェクトを追加します

    • 1)「ソリューションエクスプローラ」を開いてください
    • 2) さきほど(2)で追加したソリューションを右クリックして「追加」→「新しいプロジェクト」を選択してください
    • 3)「Visual C++プロジェクト」の「Win32」を選択し,「Win32コンソールプロジェクト」を選択してください
    • 4)「プロジェクト名」にTestMainと入れて,「場所」はそのままで「OK」を押してください
    • 5)「完了」ボタンを押してください
    • 6)これでexe用プロジェクトの追加は完成です


(4)呼び出されるDLL用プロジェクトを追加します

    • 1)「ソリューションエクスプローラ」を開いてください
    • 2) (2)で追加したソリューションを右クリックして「追加」→「新しいプロジェクト」を選択してください
    • 3)「Visual C++プロジェクト」の「Win32」を選択し,「Win32コンソールプロジェクト」を選択してください
    • 4)「プロジェクト名」にTestRefDLLと入れて,「場所」はそのままで「OK」を押してください
    • 5)「アプリケーションの設定」を選択してください
    • 6)「DLL」と「シンボルのエクスポート」にチェックを付けて、「完了」ボタンを押してください
    • 7) これでdll用プロジェクトの追加は完成です。一旦ソリューションをビルドして成功することを確認してください


以上で下準備は完了です。


3. TestMain.exeからTestRefDLL.dll内の関数を呼び出してみます

(1) まずはTestRef.dllのソースから要らなそうな部分を削除します


[修正前]

#include "stdafx.h"
#include "TestRefDLL.h"
BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 ){
	switch (ul_reason_for_call){
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
    return TRUE;
}

// これは、エクスポートされた変数の例です。
TESTREFDLL_API int nTestRefDLL=0;

// これは、エクスポートされた関数の例です。
TESTREFDLL_API int fnTestRefDLL(void){
	return 42;
}

// これは、エクスポートされたクラスのコンストラクタです。
// クラス定義に関しては TestRefDLL.h を参照してください。
CTestRefDLL::CTestRefDLL(){ 
	return; 
}


[修正後]

#include "stdafx.h"
#include "TestRefDLL.h"
BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 ){
	switch (ul_reason_for_call){
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
    return TRUE;
}

// これは、エクスポートされた関数の例です。
TESTREFDLL_API int fnTestRefDLL(void){
	return 42;
}


(2) TestMain.exeのソースを修正します

    • 1)まずはstdafx.hに追加します
    • 2)loadlibraryやWindowsAPIを使用するのでwindows.hを追記します
#pragma once

#include	<iostream>
#include	<tchar.h>

// TODO: プログラムに必要な追加ヘッダーをここで参照してください。
// これを追記
#include	<windows.h>
    • 3) TestMain.cppに追加します
#include "stdafx.h"

typedef INT (CALLBACK* fnRefDLL)();

void printError(const char *name, int err) {
	LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER |
		FORMAT_MESSAGE_FROM_SYSTEM |
		FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
		err,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(char*)&lpMsgBuf,
		0,
		NULL
	);
	printf("%s: %d:%s\n", name, err, lpMsgBuf);
	LocalFree(lpMsgBuf);
}

int _tmain(int argc, _TCHAR* argv[]) {
	HMODULE hModule = NULL;
	fnRefDLL hfnRefDll;

	if ((hModule = LoadLibrary("C:\\CppTestSolution\\TestRefDLL\\Debug\\TestRefDLL.dll")) == NULL) {
		printError("× DLLのロードに失敗\n",GetLastError());
	} else {

		printf("○ DLLをロードしました\n");
		if ((hfnRefDll = (fnRefDLL)GetProcAddress(hModule,"fnTestRefDLL")) == NULL) {
			printError("× メソッドが見つかりませんでした\n",GetLastError());
		} else {
			printf("○ メソッドが見つかりました\n");
			
			int rslt = hfnRefDll();
			printf("★ return from fnRefDLL() is %d\n",rslt);
		}
		FreeLibrary(hModule);
	}
	return 0;
}
    • 7) メソッドが見つからないようです...
4. DLLを調べてみる

失敗したとは言え、何となく呼び出せているので方向性としてはだいじょうぶそうです。
ひとまず、DLLの設定で足りない部分があると仮定して正しくメソッドが呼び出せる状態になっているのかを確認します。


(1) エクスプローラでDLLのデバッグモジュールの入ったフォルダを開きます


(2)デバッグモジュール(TestRefDLL.dll)をDependency Walkerで開いてみます


これを見るとどうやら関数名が文字化けしてしまっているようです。
たしかにこれでは名前が違うので呼び出すことが出来ません。
これが意図した名前で表示出来れば正しく動作するような気がします。

5. DLL側のソースを修正する

これはどう考えても呼び出されるDLL側に非があるので、ちゃんと関数をエクスポートできるように修正が必要です。

(1) ヘッダーファイル(TestRefDLL.h)を修正します

    • 1) TestRefDLL.hを開く
    • 2) 不要な部分を削除する


[修正前]

#ifdef TESTREFDLL_EXPORTS
#define TESTREFDLL_API __declspec(dllexport)
#else
#define TESTREFDLL_API __declspec(dllimport)
#endif

// このクラスは TestRefDLL.dll からエクスポートされました。
class TESTREFDLL_API CTestRefDLL {
public:
	CTestRefDLL(void);
	// TODO: メソッドをここに追加してください。
};

extern TESTREFDLL_API int nTestRefDLL;

TESTREFDLL_API int fnTestRefDLL(void);


[修正後]

#ifdef TESTREFDLL_EXPORTS
#define TESTREFDLL_API __declspec(dllexport)
#else
#define TESTREFDLL_API __declspec(dllimport)
#endif

extern TESTREFDLL_API int fnTestRefDLL(void);


修正したのは不要な部分を削除してfnTestRfDLL()にexternをつけただけです。


(2) 再度ビルドして,デバッグモジュールをDependency Walkerで確認します


やはり関数名が文字化けしてしまっています...。
何でだろう...とネットを調べたてみら定義ファイルを追加しちゃえよ!!と書いてあったのでそれに従って見ます。


(3) 定義ファイルを追加してみます

    • 1) ソリューションエクスプローラを開いてください
    • 2) TestRefDLLの「ソースファイル」を右クリックして「追加」→「新しい項目の追加」を選択してください
    • 3)「モジュール定義ファイル(def)」を選択して,ファイル名には「TestRefDLL」と入力して「開く」ボタンを押してください
    • 4) エクスポートしたい関数名と序数をつけて記述して保存してください


今度は正常に名前が表示されました。これでいけそうな気がします。

6. 再度デバッグをする

再度TestMain.exeを使用してテストをします。


(1) ソリューションを一度ビルドしなおしてください
(2) デバッグでプログラムを起動してください
(3) ブレークポイントで停止したら,デバッグで表示されているコンソールをアクティブにしてください


正常に42が返却されていることが確認出来ます。


ちなみにこの呼び出し方であれば,DLL側にブレークポイントを設定することも可能です。

7. 完成ソース

完成のソースをzip圧縮してサーバへ格納しておきます。
LoadLibrary()するDLLのパスのとこだけ書き換えればそのまま使用出来るはずです。

完成ソースファイル