VBAから自作DLLにコールバック関数を渡す (C++)

# はじめに
VBAから利用するDLLを作成する際に、よく調べなおすので記事に残しておきます。世の中にはいろいろ便利なツールがありますが、Excelを多用している職場ですと結局VBAに戻ってきてしまうのです。

# 環境
OS: Windows 11 Home
Excel：Microsoft 365
C++エディタ：Microsoft Visual Studo Community 2022 (64 ビット)

# 前提
・DLLでの関数のエクスポートは定義ファイル（.def）で行っており、そちらの記載は省略しています。
・コードサンプルですが、VBAで実行前に後述のSetDllDirectoryでDLLのディレクトリを追加済みとします。
（起動後に１回実行すれば大丈夫です。また、ライブラリをフルパスで指定する場合は不要です。）

# ※注意※
・何か間違えるとたいていの場合、Excelが落ちます。実行前に保存は忘れずに。試すのは自己責任でお願いします。
・Declareステートメントの`Sub`と`Function`を間違えただけでも正常に動きません。
・VBAの引数渡しは`ByRef`が初期値ですので、`ByVal`を省略すると正常に動きません。
・VBAは宣言でデータ型を省略すると`Variant`になりますので`As Long`などを省略すると正常に動きません。
・DLLのビルド先ディレクトリを登録した場合はDLLのビルド前にVBEで「リセット」するか`End`ステートメントを実行しないと上書き禁止でビルドに失敗することがあります。
（ファイル ○○.dll を開いて書き込むことができません。）


# コードサンプル
## VBAで自作DLLを利用するための事前実行コード
VBAでライブラリをフルパスで指定する場合は不要です。
```vb:VBA
'動的にDLLを取得するためのWinAPI
Private Declare PtrSafe Function SetDefaultDllDirectories Lib "kernel32" (ByVal DirectoryFlags As Long) As Long
Private Declare PtrSafe Function AddDllDirectory Lib "kernel32" (ByVal fileName As String) As LongPtr ' PCWSTR
Private Const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = &H1000 '：0x00001000

' DLLのあるディレクトリのパスを渡します
Public Sub SetDllDirectory(dllDirectory As String)
    SetDefaultDllDirectories LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
    AddDllDirectory StrConv(dllDirectory, vbUnicode)
End Sub

' もしブックと同じディレクトリにDLLを置いた場合は以下を実行します
Public Sub AddCurrentDirectory()
    SetDllDirectory ThisWorkbook.Path
End Sub
```


## 数値を受け取るコールバック関数を渡す
フィボナッチ数列を指定した項目数だけ、順にコールバック関数に渡すサンプルです。
```cpp:DllExample
#include <comutil.h>

// コールバック関数にフィボナッチ数列を渡す
void WINAPI Fibonacci(void (CALLBACK *func)(int, int), int term)
{
	if (!func || term < 1)
		return;

	// フィボナッチ数列を計算
	int a = 0, b = 1;
    func(a, 1);
    if (term == 1)
        return;
    func(b, 2);
    if (term == 2)
        return;
	for (int i = 2; i < term; i++)
	{
		int c = a + b;
		a = b;
		b = c;
		// コールバック関数を呼び出す
		func(c, i + 1);
	}
}
```

```vb:VBA
Private Declare PtrSafe Sub Fibonacci Lib "DllExample" (ByVal func As LongPtr, ByVal term As Long)

Sub FibonacciTest()
    Fibonacci AddressOf callback, 10
End Sub

' DLLにてコールバック関数の返却型をvoidに定義しているため、
' Subプロシージャでないと正常に引数を受け取れません
Private Sub callback(ByVal v As Long, ByVal i As Long)
    Debug.Print i & "番目: " & v
    ' 1番目: 0
    ' 2番目: 1
    ' 3番目: 1
    ' 4番目: 2
    ' 5番目: 3
    ' 6番目: 5
    ' 7番目: 8
    ' 8番目: 13
    ' 9番目: 21
    ' 10番目: 34
End Sub
```


## 文字列を受け取るコールバック関数を渡す
VBAからDLLに文字列を渡し、そのままコールバック関数に渡すサンプルです。
DLLで文字列を変換しないと文字化けします。
```cpp:DllExample
#include <comutil.h>
#include <string>

// コールバック関数の引数は、ByValでStringを受け取るため(BSTR)で定義する
// また、ByValでDLLに文字列を渡す場合はchar*で受ける
void WINAPI TelephoneGameVal(void (CALLBACK* func)(BSTR), char* c)
{
	if (!func || !c)
		return;

	// VBAからマルチバイト文字列でBSTRが渡されるので、
	// ワイド文字列に変換してからコールバック関数に渡す
	int iBufferSize = MultiByteToWideChar(CP_ACP, 0, c, -1, NULL, 0);
	std::wstring ws(iBufferSize, L'\0');
	MultiByteToWideChar(CP_ACP, 0, c, -1, &ws[0], iBufferSize);
	func(SysAllocString(ws.c_str()));
}
```

```vb:VBA
Private Declare PtrSafe Sub TelephoneGame Lib "DllExample" (ByVal func As LongPtr, ByVal word As String)

Sub TelephoneGameTest()
    TelephoneGame AddressOf callback, "ACBあいうえお"
End Sub

' DLLにてコールバック関数の返却型をvoidに定義しているため、
' Subプロシージャでないと正常に引数を受け取れません
Private Sub callback(ByVal word As String)
    Debug.Print word ' ACBあいうえお
End Sub
```


# 参考
https://www5b.biglobe.ne.jp/~kouta_y/news/yomo/06.html