C/C++で実装したDLLをC#から呼び出す(1)
遅延ロードをしてみる

2022年2月22日

どもです。
C#で実装されたアプリケーションからC/C++で実装したDLLのライブラリを使用する際に、使用するライブラリは「設定ファイル」などで指定できるようにしたい、切り替えられるようにしたい、という場面があります。
今回のエントリでは、そのような場合に対処する方法について書きます。

1. 定石:DllImport属性を使用する

C/C++で実装されたDLLをC#で使用するためには、「DllImport」という「属性」を使用するのが定石です。
具体的には、例えばC/C++で実装された「SampleLibrary.dll」が提供する「SampleMethod」という関数をC#で使用するためには、以下のような実装になります。

namespace LoadNativeDllSample
{
	class Program
	{
		[DllImport("SampleLibrary.dll")]
		static extern void SampleMethod(int input);
	}
}

しかしながら、この方法で使用可能な「SampleMethod」関数は、「SampleLibrary.dll」が提供するモノに限定されます。
そのため、仮に「SampleLibrary_Mk2.dll」が提供する「SampleMethod」関数を同じプログラム(プログラムを変更せずに)で使用する場合には、DLLファイルの名前を変更する必要があります。
これは、「SampleLibrary.dll」と「SampleLibrary_Mk2.dll」を同じプログラム/ソースコードで使用するように指定ができないことが原因です。

(このような状況が発生するのは、そもそも設計に問題があるのでは…?という指摘は、あると思います。しかし、それを言ってしまってはオシマイなので、今回はスルーします。)

2. 対策

この問題への対策方法として、「LoadLibrary」と「GetProcAddress」を使用する方法があります。
これらの関数の仕様/詳細は、以下のMicrosoftのサイトを参照して下さい。

※英語です。日本語はありませんでした。
(なお、「LoadLibrary」を使用して読み込んだDLLは、必ずFreeLibraryで解放する必要があるので、注意してください。)
C#で実装するアプリケーションでこれらの関数を使用することで、DLLを名前を指定してロード/実行ができます。

例えば、前述の「SampleLibrary.dll」と「SampleLibrary_Mk2.dll」については、アプリケーション側でどちらの関数を使用するかを指定/切り替えることで、それぞれの「SampleMethod」を選択しながら使用できます。

3. やってみよう

3.1. サンプルライブラリ

まず、サンプルの「C/C++実装したライブラリ」です。
目的を考えて、まずはAddという、2つの値の和を返すような関数を例にします。
このとき、簡単にライブラリの名前を表示するようにします。

で。
具体的なコードは以下!

#include "pch.h"
#include <Windows.h>
#include <tchar.h>

#ifdef __cplusplus
extern "C" {
#endif
	__declspec(dllexport) int Add(int value1, int value2)
	{
		_tprintf(_T("LoadNativeDynamicLinkLibrary\n"));
		_tprintf(_T("%s\n"), __FUNCTIONW__);
		_tprintf(_T("value1 = %d\n"), value1);
		_tprintf(_T("value2 = %d\n"), value2);

		return (value1 + value2);
	}
#ifdef __cplusplus
}
#endif

これが、「SampleLibrary.dll」に実装する関数。
で、もう1つ。
「SampleLibrary_Mk2.dll」には、以下のような関数を実装します。

#include "pch.h"
#include <Windows.h>
#include <tchar.h>

#ifdef __cplusplus
extern "C" {
#endif
	__declspec(dllexport) int Add(int value1, int value2)
	{
		_tprintf(_T("LoadNativeDynamicLinkLibrary_Mk2\n"));
		_tprintf(_T("%s\n"), __FUNCTIONW__);
		_tprintf(_T("value1 = %d\n"), value1);
		_tprintf(_T("value2 = %d\n"), value2);

		return (value1 + value2);
	}
#ifdef __cplusplus
}
#endif

表示するメッセージを少し変えただけで、他は変更ありません。
処理については、ホントーに和を求めて返すだけなので、解説は省略します。

3.2. サンプルライブラリの呼び出し側

次に、実装したライブラリの呼び出し側です。
ライブラリの呼び出しは、以下の手順で行います。

  1. DLLのロード
    (LoadLibrary/DLLのパス/名前の指定)
  2. 実行したい関数ポインタの取得
    (GetProcAddress)
  3. 実行したい関数のdelegateを取得
    (GetDelegateForFunctionPointer)
  4. 関数の実行!
  5. DLLの解放(アンロード)

この手順を読んでいて「ん?」となるのが、「関数のdelegateを取得」の部分かと思います。
C#でGetProcAddressで関数ポインタを取得しても、それがどのような関数に対するポインタなのかを知ることはできません。
そのため、delegateにより関数ポインタの型(?)を宣言する必要があります。
基本的には、C言語の関数ポインタと同じ考え方です。

前述の手順を実装すると、以下のコードになります。

string dllPath = @"path\to\SampleLibrary.dll";
IntPtr ptrLib = LoadLibrary(dllPath);
IntPtr ptrAdd = GetProcAddress(ptrLib, "Add");
AddDelegate addDelegate = (AddDelegate)Marshal.GetDelegateForFunctionPointer(ptrAdd, typeof(AddDelegate));

int value1 = 10;
int value2 = 23;
int addedValue = addDelegate(value1, value2);
Console.WriteLine($"addedValue = {addedValue}");

上記のコードの中で、変数dllPathに与える値を、「SampleLibrary.dll」と「SampleLibrary_Mk2.dll」で切り替えることで、使用するDLLを変更する(切り替える)ことができます。

なお「AddDelegate」の定義は、以下になります。

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int AddDelegate(int value1, int value2);

3.3. 動かしてみる前に…

上記のコードの実装(もちろん不足している部分もふくめて)が完了したら、実際にプログラムを動かしてみ…たいのですが、その前に注意すべきことがあります。
それは、「プラットフォーム」の選択です。
VisualStudioでC#のプログラムを開発する場合、その「プラットフォーム」は「AnyCPU」がデフォルトで選択されています。
AnyCPUでは、.NET Frameworkがプログラムを32bit/64bitのどちらの環境で起動/実行させるかをOSが判断します。



ここで問題になるのが、「LoadLibrary」でロードするライブラリの「プラットフォーム」です。
私の環境は「64bit」なので、ライブラリも「64bit向け」でビルドされている必要があります。
「32bit向け」のビルドでは、正しくロード/動作ができません。

更に、プロジェクトの設定が「32ビットを選ぶ」が選択されている場合もあります。
この場合には、強制敵に32bit向けのビルドになります。



このように、「LoadLibrary」を使用してライブラリをロードする場合には、そのアプリケーションが32bit/64bitのどちらのプラットフォームに対応しているのか、またライブラリがどのプラットフォームに向けてビルドされているのか、注意する必要があります。

3.4. それでは動かしてみましょう

上記のコードの実装(もちろん不足している部分もふくめて)が完了したら、実際にプログラムを動かしてみます。
その結果は、下図のようになります。



この図から分かるように、「SampleLibrary.dll」と「SampleLibrary_Mk2.dll」が、途中で切り替えられていることが分かります。

4. まとめ

今回のエントリでは、C#で実装されたプログラムから、C/C++で実装されたDLLを、プログラム起動後にファイル名を指定して読み込んでみました。
この方法は、「遅延ロード」とか「遅延読み込み」と言われる方法です。
比較的古くからある方法、基本的な方法のようです。

今回の問題については、「そもそも…」というカンジで色々意見がある場面は多いと思います。
しかしながら、それでも同じような問題に直面する方もいらっしゃると思います。
今回のエントリが、そのような方の問題解決の一助になれば、幸いです。

ではっ!

ex. 公開ています

今回のエントリで書ききれなかったコードの全容は、GitHubにて公開しています。
その中のLoadNativeDynamicLinkLibraryが対応します。
コチラのコードも、併せて参考にしていただければ幸いです。
※コミットしたコードでは、パスが決め打ちにしており、ビルド/実行する際には注意が必要です。

2022/02/23 更新

コードを一部修正しました。
delegateに対して「UnmanagedFunctionPointer」を追加し、呼び出し規約を明示するよう修正しました。