VC PlusPlus:LIB/DLLの生成とLIB呼び出し、DLL呼び出しについて

提供:yonewiki

VC Cpp記事に戻る。

概要

 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 はうまく動作しないね。絶対パスなら上手く動くようでした。実行ファイルの置いてある場所を取得して差し引きして、相対パスの指定で動作してるような仕組みにすれば良さそうでした。なのでプログラム中で環境によってdllを切り替えたり正しいdllが見つかるまで探索するとかという工夫は出来そうです。今回はサンプルファイルを提供する程度のモチベーションしかないのでそこまではやりませんでした。自分もトライする気になったら再度チャレンジしてみたいと思います。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つのプロジェクトを圧縮したファイルを配布しておきたいと思います。以下からダウンロード(DOWNLOAD)して下さい。


メディア:CppDLL CppDLLHost CppLoadDLL.zip


 SetDllDirectoryW関数で絶対パス指定をしなきゃいけないけど相対パス指定みたいにする方法。地道に考えた結果の相対パスでのDLL読み込みのコードです。執念的なモノです。怖い怖い。

#include <iostream>
#include <Windows.h>

typedef int(*intFUNC_void)();

std::wstring MultiByteToWideString(const char* input)
{
    int bufferSize = MultiByteToWideChar(CP_UTF8, 0, input, -1, nullptr, 0);
    wchar_t* output = new wchar_t[bufferSize];
    MultiByteToWideChar(CP_UTF8, 0, input, -1, output, bufferSize);
    std::wstring result(output);
    delete[] output;
    return result;
}

int main(int argc, char* argv[])
{
    BOOL bSetDLLDirResult;

    int iInputNum;
    std::cin >> iInputNum;

    // プログラム起動引数が存在しない場合は終了
    if (argc < 1)
    {
        std::wcout << L"引数が指定されていません。" << std::endl;
        return 1;
    }

    const wchar_t* searchStr = L"\\CppLoadDLL\\x64\\Debug";
    const char* argument = argv[0];

    //プログラム起動引数をワイド文字列に変換:関数化しました。
    std::wstring wcArgument = MultiByteToWideString(argument);

//関数化する前のコード
//    int bufferSize2 = MultiByteToWideChar(CP_UTF8, 0, argument, -1, nullptr, 0);
//    wchar_t* wcArgument = new wchar_t[bufferSize2];
//    MultiByteToWideChar(CP_UTF8, 0, argument, -1, wcArgument, bufferSize2);


    std::wstring result = wcArgument;
    size_t found = result.find(searchStr);
    if (found != std::string::npos)
    {
        // 検索文字列が見つかった場合、一致する手前の文字列を取得する
        result = result.substr(0, found);

        // 取得した文字列の最後尾に "\\CppDLL\\CppDLL\\x64\\Debug" を追加する
        result += L"\\CppDLL\\x64\\Debug";

        // 結果の表示
        std::wcout << "DLLDirectoryパス結果: " << result << std::endl;
    }
    else
    {
        // 検索文字列が見つからなかった場合の処理
        std::cout << "自己実行ファイルパスから" << searchStr << "検索文字列が見つかりませんでした。" << std::endl;
    }

    // LPCWSTR型の引数をとるのでresulet.c_str()を引数にしました。
    bSetDLLDirResult = ::SetDllDirectoryW(result.c_str());

    WCHAR buffer2[MAX_PATH];//SetDllDirectoryWの対になる関数を使う。
    DWORD result2 = GetDllDirectoryW(MAX_PATH, buffer2);
    HMODULE hModule = LoadLibraryW(L"CppDLL.dll");
    if (hModule == 0){
        return 1;
    }
    else {
        intFUNC_void ifLoadDllfunc = (intFUNC_void)GetProcAddress(hModule, "ifDllfunc");
        std::cout << ifLoadDllfunc();
    }
    std::cout << "\n";
    std::cout << "Hello World!\n";

    return 0;
}

コードの説明

 コードの説明はしないですね。長すぎるでしょ。急に本気出したみたいなコードでね。だってプロジェクトを置く場所がひょっとしたら、日本語を含むフォルダの場合もあるでしょ。したら、ワイド文字列対応するっしょ。したらば、コードの説明はワイド文字列とシングルバイト文字列変換の説明が必要になるじゃん。そんなのC++の記事でやったことじゃん。だから説明しないでしょ。string型の使い方の説明?いやじゃん。でもここの管理人はやさしいからコードのコメントつけてるじゃん。それで理解できるじゃん。説明いらないやん。ね。それにこれは執念だから、やらなくていいことだし。  

VC Cpp記事に戻る。