WindowsのDLLについて

自分にWindows系の知識がだいぶ足りないので、試したことを書く。

試した環境は、古いんだけど Windows 7 / Visual C++ 2010 Express / Visual Basic 2010 Express (今は、2013 Expressとか、2013 Communityとかあるらしい)

# VC++で作成するプロジェクトの種類
新しいプロジェクトを作成するときに、カテゴリとして CLR, Win32, 全般の3つがある。
全般は汎用の空プロジェクトなので置いておいて、大きくわけてCLRとWin32があると理解すれば良い。
CLRは .NET Runtime上で実行するためのプログラムを作るためのもので、Win32はWindowsネイティブのプログラムを作るためのものと理解した。
別の言い方をすると、CLRの方がいわゆるマネージドコード、Win32の方がアンマネージドコード。

# VC++でコンソールアプリを作る
Visual Studioの新しいプロジェクトの作成で、「CLR コンソール アプリケーション」を選択すると、Hello Worldのプログラムがひな形として生成される。

```c++:hello.cpp
#include "stdafx.h"

using namespace System;

int main(array<System::String ^> ^args)
{
    Console::WriteLine(L"Hello World");
    return 0;
}
```

CLR用のプログラムを記述するのは、C++/CLIと言うC++の上位互換言語らしい。(昔は、Managed Extensions for C++ だったらしい)
見慣れないのは、main()のプロトタイプで、`(int argc, char *argv[])`の代わりにJavaやC#のような`String`の配列で受けている。
`^` は、.NETのオブジェクトへの参照を表す記号で、基本的にはポインタと同じものだと思っておけば良い(のかな?)

これを実行すると、一瞬コマンドプロンプトが表示されて、`Hello World`を表示した後すぐに閉じる。
ソースの`Hello World`の部分を日本語にしても、そのまま出力される。このときのソースの文字コードはCP932だった。
文字列リテラルに`L`がついているので、wchar_t になるはずなので、日本語版 Windows の C++ の wchar_t は CP932なのか？

# VC++でDLLを作る

続いて、DLLを作ってみる。
新しいプロジェクトから、「Win32 プロジェクト」を選ぶ。(CLR クラスライブラリを選ばないのは、ここではアンマネージドコードを試してみたかったため)
「Win32 プロジェクト」を選ぶと、その後 「Win32 アプリケーション ウィザード」と言うのが開くので、「アプリケーションの種類」でDLLを選ぶとDLLを作成できる。
このとき、「シンボルのエクスポート」にチェックしておいた。(必要かはわからないが、勘で)

関係なさそうなファイルを除くと、以下の3つのファイルが生成される。(dlltestは、作成したプロジェクトの名前)

* dlltest.h
* dlltest.cpp
* dllmain.cpp

## dlltest.h

```cpp:dlltest.h
#ifdef DLLTEST_EXPORTS
#define DLLTEST_API __declspec(dllexport)
#else
#define DLLTEST_API __declspec(dllimport)
#endif

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

extern DLLTEST_API int ndlltest;

DLLTEST_API int fndlltest(void);
```

ライブラリのヘッダファイル。ライブラリ自身をコンパイルするときには、`DLLTEST_EXPORTS`マクロが定義され、それ以外の場合は`DLLTEST_EXPORTS`マクロが定義されないことで、`__declspec(dllexport)`と`__declspec(dllimport)`を切り替えている。

## dlltest.cpp

```cpp:dlltest.cpp
#include "stdafx.h"
#include "dlltest.h"


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

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

// これは、エクスポートされたクラスのコンストラクターです。
// クラス定義に関しては dlltest.h を参照してください。
Cdlltest::Cdlltest()
{
	return;
}
```
ライブラリの実装。説明は不要だろう。

## dllmain.cpp

```cpp:dllmain.cpp
#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE 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;
}
```
DLLのエントリポイント。Unix系のsoにはない仕様だが、DLLの初期化や後片付けを書くことができるらしい。

## ビルド
ビルドすると、Debugディレクトリに以下のファイルが生成される。

* dlltest.dll
* dlltest.exp
* dlltest.ilk
* dlltest.lib
* dlltest.pdb

もう調べるのが面倒なのでいちいち触れないが、重要なファイルだけ説明すると、

`*.dll`
いわゆるDLL。アプリケーションの実行時にPATHが通ったところにある必要がある。
`*.lib`
インポートライブラリ。`.lib` は Unix系の `*.a` 相当だが、DLLを作成したときにできた `.lib` は静的ライブラリではなく、DLLをリンクするための情報が書かれている(?)

# コンソールアプリからDLLを呼ぶ(参照設定)
さきほど作ったdlltestを呼ぶアプリケーションを作ってみる。
dlltestのソリューション/プロジェクトを開いている状態で、新しいプロジェクトから「CLR コンソール アプリケーション」を選ぶ。
このとき、「ソリューション」のところを「新しいソリューションを作成する」の代わりに「ソリューションに追加」を選ぶと、同じソリューションにdlltest(DLL)とhello(アプリ)がある状態になる。

## include pathの追加
アプリケーションのソースファイルから、DLLのヘッダファイルをincludeできる必要があるので、アプリケーションのプロジェクトを右クリックして「プロパティ」を選び、プロパティ ページを開く。
「構成プロパティ」の「C/C++」を選び、「追加のインクルードファイル」にDLLのヘッダのディレクトリを指定する。(相対パスで指定できるので、`..\dlltest\`等と指定する。

## 参照設定の追加
同じプロパティ ページで、「共通プロパティ」、「Framework と参照」を選び、「新しい参照の追加...」ボタンを押す。
「プロジェクト」タブから、DLLのプロジェクトを選択し、OKを押すと参照に追加される。

## 呼び出しコードの追加
ソースを修正して、DLLを呼び出すコードを追加する

```cpp:hello.cpp
#include "stdafx.h"
#include "dlltest.h"

using namespace System;
using namespace System::Diagnostics;

int main(array<System::String ^> ^args)
{
    Console::WriteLine(L"Hello World");
    Debug::WriteLine(fndlltest());
    return 0;
}
```

`Debug::WriteLine()`の中にある `fndlltest()`がDLLの関数の呼び出しである。

## 実行
再度ソリューションエクスプローラからアプリケーションを右クリックし、「スタートアップ プロジェクトに設定」を選ぶ。
この状態でデバッグ実行すると、プログラムが実行される。

# コンソールアプリからDLLを呼ぶ(外部DLL)
DLLとアプリケーションが同じソリューションにある場合(DLLがアプリのサブプロジェクトのような場合)は、先ほどの参照設定を使う方法で良いのだが、DLLが外部から提供されたような場合は、参照設定が使えない(やり方があるのかも知れないけど)
ヘッダファイルとDLL、LIBだけが提供されたような場合は、以下の手順でリンクできる。

## include pathの追加
参照設定の場合と同じ

## ライブラリの追加
アプリケーションのプロパティページから、「構成プロパティ」、「リンカー」、「入力」を選び、「追加の依存ファイル」に `.lib` を相対パス(絶対パスでも良い)で指定する。

## 呼び出しコードの追加
参照設定の場合と同じ

## 実行
実行時には、DLLがPATHの通った場所にある必要があるので、とりあえずexeと同じディレクトリ(アプリのDebugディレクトリ等)にコピーしてやると実行できる。

# VBからDLLを呼ぶ
DLLを呼ぶサンプルなので、Visual Basic 2010 Expressの新しいプロジェクトで、「コンソール アプリケーション」を選ぶ。

```vbnet:Module1.vb
Module Module1

    Sub Main()

    End Sub

End Module
```
VB.NETも、エントリポイントはMain()らしい。
こちらはVC++と違いHello Worldすら出力してくれないので、実行しても何も出力されない。

## 呼び出しコードの追加

```vbnet:Module1.vb
Module Module1
    <System.Runtime.InteropServices.DllImport("dlltest.dll")> Private Function fndlltest() As Integer
    End Function

    Sub Main()
        Debug.Print(fndlltest())
    End Sub

End Module
```

これで、いけるはず、と思って実行すると、「System.EntryPointNotFoundException: DLL 'dlltest.dll' の 'fndlltest' というエントリ ポイントが見つかりません。」と言ってアプリが落ちてしまう。

さんざん悩んだ末に、DLLの方のヘッダに`extern "C"`を追加してDLLをビルドし直したところ、無事にVBから呼び出すことに成功した。

```cpp:dlltest.h
extern "C" DLLTEST_API int fndlltest(void);
```

DllImportの引数でC++の名前にも対応できるのかも知れないけど、今のところ`extern "C"`が必要と言う結果になった。
https://msdn.microsoft.com/ja-jp/library/dt232c9t.aspx あたりを真面目に読めば解決策がわかるかもしれない。

## 追記

上記のサンプルだと、VBから読んでいるDLLの関数に引数がないため気がつかなかったのだが、引数があると実行時に`PInvokeStackImbalance`と言うエラーが出てしまう。
再度上記ページを読んで試してみたところ、VBから呼ぶDLLの関数には、`__stdcall`が必要なようだ。
`__declspec`と違い、`__stdcall`の位置は返値の型と関数名の間でないといけないらしいので、上記サンプルの`DLLTEST_API`に含めるわけにもいかず、どうしたものだろうか。