LoginSignup
36
40

More than 5 years have passed since last update.

WindowsのDLLについて

Last updated at Posted at 2015-05-19

自分に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のプログラムがひな形として生成される。

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

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

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

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を呼び出すコードを追加する

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の新しいプロジェクトで、「コンソール アプリケーション」を選ぶ。

Module1.vb
Module Module1

    Sub Main()

    End Sub

End Module

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

呼び出しコードの追加

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から呼び出すことに成功した。

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に含めるわけにもいかず、どうしたものだろうか。

36
40
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
40