「VC PlusPlus:LIB/DLLの生成とLIB呼び出し、DLL呼び出しについて」の版間の差分
編集の要約なし |
|||
199行目: | 199行目: | ||
あーだめだだめだ。相対パスでヘッダファイルとlibファイルを呼び出す方式でDLLを呼び出すことはできないね。LoadLibraryで呼び出すとには、SetDllDirectoryW関数の設定は有効だけど、アプリ起動時の呼び出しには無効だな。 |
2023年5月19日 (金) 18:55時点における版
概要
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の仕組みについて熟知している必要があります。このあとでサラッと紹介します。
- ここではdllファイルをコピーせず、前もって作成した、CppDLL.dllを読み込む形のコードにします。普通は *.exe のあるフォルダに dll をコピーします。追記する内容は以下のようなものです。
#include <iostream>
#include <Windows.h>
#include "..\CppDLL\CppDLL.h"
#pragma comment(lib, "..\\CppDLL\\x64\\Debug\\CppDLL")
int main()
{
::SetDllDirectoryW(L".\\..\\..\\..\\CppDLL\\x64\\Debug\\");
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ファイルを探して、関数を実行するような動きになります。
- SetDllDirectoryW(L".\\..\\..\\..\\CppDLL\\x64\\Debug\\");
- メイン関数内で、最初にDllの配置されているディレクトリを指定します。今回の場合はdllを実行ファイルのあるフォルダにコピーしたりしないで使いたいので、このような相対パスを引数にとる形になります。実行ファイルが生成されるディレクトリからの相対パスなので、何度も親ディレクトリを辿る必要が生じています。マクロで CPPDLL_EXPORTS も CPPLIB_EXPORTSも定義していないので、関数のキーワードに __declspec(dllimport) が付与され、関数を呼び出すと、dllを読み込んで処理をしようとします。もっと本格的なdllでは、関数名が重複しないように関数名にも工夫をしておいた方がよいかもしれません。個人的、属する組織的な範囲で使うだけのプログラムなら、重複してから考えればよいのであまり神経質になる必要もないでしょう。
- 今回は実行ファイルと同じディレクトリにdllをコピーしない方法にしたので相対パスを使うべくこのような特殊な処理を行いました。dllを相対パスで指定する危険性についても理解が必要です。dllの配置するディレクトリには、保護されたディレクトリを使った方が安全性は高まると思います。同じディレクトリにDLLを置いた場合の危険性とさほど差はありません。個人的、属する組織的のような狭い範囲で使う分には攻撃するメリットがあまりないので、そこも神経質になる必要はないかもしれません。
std::cin >> iInputNum;
- テストプログラムとして途中でとめられるように、コンソールへの数値入力待ち状態になるようにするために追加しました。
std::cout << ifDllfunc();
- ここでdllの関数を呼び出しています。ifDllfunc() は引数は必要ないですが、int型の数値を戻り値に受け取ります。今回の例では必ず 99 が返ってくるようにしました。ナインティナイン好きですね。腹話術というネタが好きだったな。数十年前に読売テレビでやっていた怒涛のくるくるシアターで1回しか見たことのないネタです。くるくるシアターのエンディングテーマの「雲南の風/SACRA」という曲もすばらしいものだったと思います。「ついのすみか」というアルバムに収録されています。録音した音源を持っています。話がそれました。
- 説明はこんなところです。みんなのPCでも動くといいな。
あーだめだだめだ。相対パスでヘッダファイルとlibファイルを呼び出す方式でDLLを呼び出すことはできないね。LoadLibraryで呼び出すとには、SetDllDirectoryW関数の設定は有効だけど、アプリ起動時の呼び出しには無効だな。