自分に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のプログラムがひな形として生成される。
#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
#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
#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
#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を呼び出すコードを追加する
#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の新しいプロジェクトで、「コンソール アプリケーション」を選ぶ。
Module Module1
Sub Main()
End Sub
End Module
VB.NETも、エントリポイントはMain()らしい。
こちらはVC++と違いHello Worldすら出力してくれないので、実行しても何も出力されない。
呼び出しコードの追加
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から呼び出すことに成功した。
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
に含めるわけにもいかず、どうしたものだろうか。