VC PlusPlus:LIB/DLLの生成とLIB呼び出し、DLL呼び出しについて
概要
DLLやLIBを自作する基礎についての記事です。LIBファイルはDLLを作成すると同じフォルダに吐き出されます。なのでDLLを作成する記事になります。それから生成したLIBファイルとDLLを使う基礎についても記述します。
DLLを作成する
そもそも、この記事はVisual Studio C++記事の子記事ですので、Visual Studioを使ったDLLの作成方法を示すものです。
1.Visual Studio 2022 CommunityでC++のプロジェクトを作成するためのツールを起動します。スタートアップダイアログで、プロジェクトの新規作成を選択します。
2.テンプレート一覧から、ダイナミックリンクライブラリを選択します。
3.ソリューション名を入力します。ここではCppDLLとします。プロジェクト名も同一とします。ソリューションファイルとプロジェクトファイルは同じディレクトリの配置します。作成するディレクトリは規定値(C:\User\(ユーザID)\source\repos\)を使います。[作成]ボタンを押します。
4.ソリューションエクスプローラーの[ヘッダー ファイル]を右クリックして表示されるコンテクストメニューから[追加]-[新規の項目]を選択します。
5.ヘッダーファイルを追加するので、ファイル名欄に「CppDLL.h」と指定して[OK]ボタンを押します。
6.以下のようにプログラムを記述します。
int ifDllfunc(void) という関数を外部から使えるようにするためのヘッダファイルです。
#pragma once
#ifndef __CPPDLL_H
#define __CPPDLL_H
#ifdef CPPDLL_EXPORTS
#define CPPDLL_API __declspec(dllexport)
#else
#define CPPDLL_API __declspec(dllimport)
#endif
#ifdef __cplusplus
extern "C" {
#endif
CPPDLL_API int ifDllfunc(void);
#ifdef __cplusplus
}
#endif
#endif
コードの説明
CPPDLL_EXPORTS は、CppDLLプロジェクトを自動生成した時点で定義されているマクロです。DLLの生成側のプロジェクトであることを意味付けるためのマクロです。
CPPDLL_EXPORTS が定義されている、本プロジェクトから、このヘッダファイルが参照されている場合には、DLLを出力するという意味のキーワードをつけた関数を定義する必要がありますので CPPDLL_API というマクロに __declspec(dllexport) を割り当てています。他のアプリからヘッダファイルが呼び出された時は、CPPDLL_EXPORTS が定義されていないので、CPPDLL_API というマクロに __declspec(dllimport) を割り当てています。関数はdllからimportされることを意味します。
extern "C" { … } で関数を囲うことで、C言語の関数定義方法を使うことを宣言したことになります。名前修飾を行わないようにするために必須です。拡張子が *.cpp のファイルをC++としてコンパイルするときにはマクロ定義 __cplusplus がされているので、その場合だけ extern の定義をする形式にしています。CppのプロジェクトなのでライブラリがCでコンパイルされることを想定しなくても良さそうに思えますが、優しさですね。
つまり、このような形式を使ってマクロ定義を切り替える仕組みはDLL作成において必須の記述になっていて無駄な記述ではありません。呼び出しにも対応する上ではプログラムをこれ以上に短くすることはできないです。最低限必要なことが詰め込まれています。
7.自動で生成されたdllmain.cppファイルのDllMain関数の外側にint型の数値を返す関数を記述します。以下のようなコードです。
#include "pch.h"
//追記ここから
#include "CppDLL.h"
CPPDLL_API int ifDllfunc(void){
return 99;
}
//追記ここまで
BOOL APIENTRY DllMain(…
8.Visual Studioのメニュー[ビルド]-[ビルド]を選択。
- これで、ソリューション・プロジェクトファイルを作成したディレクトリから見て、.\x64\Debug\ フォルダに CppDLL.dll と CppDll.libが作成されます。この libファイルは CppDLL.h を使う場合には dll が必要となる状態です。CppDll.lib 単独で使うには __declspec(dllimport) キーワードがついていないヘッダファイルを用意する必要があります。そういったヘッダファイルになるようにもう一工夫すると、ライブラリで使うヘッダファイルも作れると思います。例えば、以下のように改造します。
#pragma once
#ifndef __CPPDLL_H
#define __CPPDLL_H
#ifdef CPPDLL_EXPORTS
#define CPPDLL_API __declspec(dllexport)
#else
#ifdef CPPLIB_EXPORTS
#define CPPDLL_API
#else
#define CPPDLL_API __declspec(dllimport)
#endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
CPPDLL_API int ifDllfunc(void);
#ifdef __cplusplus
}
#endif
#endif
コードの説明
8行目~12行目を追加しました。if文をネストするような形式で、CPPLIB_EXPORTS が定義されている場合の処理を追加しています。CPPLIB_EXPORTS と定義されていた時には CPPDLL_API を置き換えるマクロ定数を設定しないようになっています。呼び出し側でマクロを定義する必要はあります。普通はDLLで提供するかLIBで提供するかどっちかだと思うので、lib用にも対応するというのはあまりしないかもしれません。
DLLを呼び出すアプリを作る(Host側)
呼び出し元アプリを作っていきます。
1.Visual Studio 2022 CommunityでC++のプロジェクトを作成するためのツールを起動します。スタートアップダイアログで、プロジェクトの新規作成を選択します。
2.テンプレート一覧から、コンソールアプリを選択します。
3.ソリューション名を入力します。ここではCppDLLHostとします。プロジェクト名も同一とします。ソリューションファイルとプロジェクトファイルは同じディレクトリの配置します。作成するディレクトリは規定値(C:\User\(ユーザID)\source\repos\)を使います。[作成]ボタンを押します。
4.ソリューションエクスプローラーの[ソース ファイル]を展開して表示される CppDLLHost.cpp が選択されて、エディットビューに CppDLLHost.cpp のタブが開かれているのを確認して、その中にコードを追記していきます。dllの読み込みには2種類の方法があります。
- dllを作ったときに作成したヘッダファイルとdllとlibファイルを読み込む方法 暗黙的リンク
- dllファイルをLoadLibraryで読み込む方法 明示的リンク
- LoadLibraryを使う方法では、コードインテリジェンス機能と型チェックが動作しないので、dllの仕組みについて熟知している必要があります。このあとでサラッと紹介します。
- CppDLL.dllを暗黙的リンクで読み込む形のコードにします。*.exe のあるフォルダに dll をコピーするか、環境変数を設定する必要があります。dllの探索優先順序についてはおおむね以下のとおりです。ここでは環境変数にdllのあるフォルダへのパスを設定してみようと思います。
- exeファイルのあるディレクトリ
- プログラム実行時のディレクトリ
- Windowsディレクトリ
- System32ディレクトリ
- 環境変数PATHに登録されているディレクトリ
5.環境変数のPATHにdllのフォルダを追加します。
暗黙的リンクで追記する内容は以下のようなものです。
#include <iostream>
#include <Windows.h>
#include "..\CppDLL\CppDLL.h"
#pragma comment(lib, "..\\CppDLL\\x64\\Debug\\CppDLL")
int main()
{
int iInputNum;
std::cin >> iInputNum;
std::cout << ifDllfunc();
std::cout << "\n";
std::cout << "Hello World!\n";
}
コードの説明
- include <Windows.h>
- DLLのプロジェクトがWindows.hファイルを参照していたので、呼び出す側もWindows.hをインクルードします。
- include "..\CppDLL\CppDLL.h"
- 相対パスでヘッダファイルを読み込んでいます。追加のインクルードディレクトリに「..\CppDLL\」あるいは絶対パスを追加して、#include "CppDLL.h"のようにすっきりした読み込みにもできますが、プロジェクトのプロパティがごちゃごちゃするという意味では、どっちもそんなに変わらないので、相対パスによる記述を採用しました。ヘッダファイルを読み込むことでどんな関数があるかも見えるようになり、コードインテリジェンス機能にも影響が加わり、プログラミングしやすくなります。型チェックもしてくれます。
- pragma comment(lib, "..\\CppDLL\\x64\\Debug\\CppDLL")
- こちらも相対パスでlibファイルを読み込んでいます。コンパイル・リンクするときはlibファイルを参照して、処理がされ、実行ファイルが作成でき、実行時にはdllファイルを探して、関数を実行するような動きになります。
std::cin >> iInputNum;
- テストプログラムとして途中でとめられるように、コンソールへの数値入力待ち状態になるようにするために追加しました。
std::cout << ifDllfunc();
- ここでdllの関数を呼び出しています。ifDllfunc() は引数は必要ないですが、int型の数値を戻り値に受け取ります。今回の例では必ず 99 が返ってくるようにしました。ナインティナイン好きですね。腹話術というネタが好きだったな。数十年前に読売テレビでやっていた怒涛のくるくるシアターで1回しか見たことのないネタです。くるくるシアターのエンディングテーマの「雲南の風/SACRA」という曲もすばらしいものだったと思います。「ついのすみか」というアルバムに収録されています。録音した音源を持っています。話がそれました。
明示的リンクで追記する内容は以下のようなものです。
#include <iostream>
#include <Windows.h>
#include "..\CppDLL\CppDLL.h"
typedef int(*intFUNC_void)();
int main()
{
//BOOL bSetDLLDirResult;
//bSetDLLDirResult = ::SetDllDirectoryW(L".\\..\\..\\..\\CppDLL\\x64\\Debug\\");
//HMODULE hModule = LoadLibrary(L"CppDLL.dll");
HMODULE hModule = LoadLibrary(L".\\..\\..\\..\\CppDLL\\x64\\Debug\\CppDLL.dll");
intFUNC_void ifLoadDllfunc = (intFUNC_void)GetProcAddress(hModule, "ifDllfunc");
int iInputNum;
std::cin >> iInputNum;
std::cout << ifLoadDllfunc();
std::cout << "\n";
std::cout << "Hello World!\n";
コードの説明
- SetDllDirectoryW(L".\\..\\..\\..\\CppDLL\\x64\\Debug\\");
- メイン関数内で、最初にDllの配置されているディレクトリを指定します。今回の場合はdllを実行ファイルのあるフォルダにコピーしたりしないで使いたいので、このような相対パスを引数にとる形になります。実行ファイルが生成されるディレクトリからの相対パスなので、3回も親ディレクトリを辿る必要が生じています。マクロで CPPDLL_EXPORTS も CPPLIB_EXPORTSも定義していないので、関数のキーワードに __declspec(dllimport) が付与され、関数を呼び出すと、dllを読み込んで処理をしようとします。もっと本格的なdllでは、関数名が重複しないように関数名にも工夫をしておいた方がよいかもしれません。個人的、属する組織的な範囲で使うだけのプログラムなら、重複してから考えればよいのであまり神経質になる必要もないでしょう。
- うまくSetDllDirectoryWで意図した動作が得られないのでコメント化しました。なんでだろう。なので次の命令で相対パスを記述することにしました。
HMODULE hModule = LoadLibrary(L".\\..\\..\\..\\CppDLL\\x64\\Debug\\CppDLL.dll");
- こうやって相対パスで dll を読み込むことが出来ます。SetDllDirectoryW はうまく動作しないね。hModuleには64Bit Windowsの場合、64bitのアドレスが取得できます。16進数で16桁ですね。アドレスが取得できない場合は 0 で失敗を意味します。
intFUNC_void ifLoadDllfunc = (intFUNC_void)GetProcAddress(hModule, "ifDllfunc");
- 次に、hModuleに取得したアドレスを引数に持つGetProcAddress関数で第二引数で指定したdll内の関数ifDllfuncの関数ポインタを取得します。int 型の戻りで、引数voidの関数ポインタ型のintFunc_voidという型を定義し、この型の形式の関数ポインタ変数 ifLoadDllfunc で受け取ります。
std::cout << ifLoadDllfunc();
- 標準出力にifLoadDllfunc()の結果を表示します。例によって99ですね。
あとは簡単なので説明しません。謎になってしまった SetDllDirectoryW 関数が機能しないところは、地道に考えていきたいと思います。
自分でコーディングするの面倒だわっていう人のために3つのプロジェクトを圧縮したファイルを配布しておきたいと思います。以下からダウンロードして下さい。