Daclare文のDLL検索フォルダを指定する

#DLLの配布フォルダとパスの問題
VBAで外部のAPI関数を呼び出すとき、**Declare文** をモジュールシートの先頭に

`Private Declare PtrSafe Function foldl1 Lib "mapM.dll" (ByRef ... `

のように書いて関数とDLLを指定します。上記ではDLL名を"mapM.dll"としか書いていませんが、デフォルトでパスが通っているWindows APIと違って、[ VBAHaskell ](https://qiita.com/mmYYmmdd/items/c731edf943acc0a0ebe9)などの自作プログラムの場合はDLLの置き場所とパス指定をどう書けばいいかが悩みのタネでした:frowning2:。

考慮している前提条件はこんな感じです。

- VBAモジュールはいろいろな環境に配布してライブラリとして使ってもらう
- 配布するモジュールのDeclare文に特定のパスをハードコーディングしたくない（DLL名だけにしたい）
- 複数人でネットワークフォルダを共有できる環境では、DLLは個々のPCに配布せず共有フォルダに置きたい
- 開発・デバッグ時はリリースDLLとデバッグDLLを使い分けたい
- DLLを再ビルドしたとき、VBA側のプロセスを落とさずにDLLファイルを入れ替えたい
- 必要に応じてフォルダを動的に指定したい

とりあえず考えられる選択肢は以下のようなものだと思います。

###1. デフォルトでパスが通っている場所に置く
C:\WINDOWS、C:\WINDOWS\system32、C:\Program Files\Microsoft Office\Office14\ などです。

* **長所**
    * Declere文にパスをハードコーディングする必要がない 
* **短所**
    * 個々のPCにDLLを配布する必要がある
    * 配置するフォルダが限定されるうえ、ユーザーはそこにファイルを置く権限を持っていないかもしれない。
 
###2. 適当な共有フォルダに置いて、そこにパスを通す

* **長所**
    * Declere文にパスをハードコーディングする必要がない
    * 個々のPCにDLLを配布する必要がない
* **短所**
    * 個々のPCにパス設定が必要なうえ、ユーザーはその権限を持っていないかもしれない

###3.Declare文にパスを追記する
Declare文を `Lib "\\NetworkDirve\NetworkDirectory\mapFdll"` などと環境ごとに書き換えます。

* **長所**
    * 個々のPCにパス設定をしなくていい
* **短所**
    * Declere文がたくさんあると面倒
    * DLLを置くフォルダを変更するときはパスの書き換えが（多数）発生
    * VBAプログラムを別の環境に持って行ったときは、パスの書き換えが（多数）発生

###4.LoadLibraryする
Excelだったら `ThisWorkbook` モジュールに以下のマクロを記述します。

```vbnet
Private Declare PtrSafe Function LoadLibrary Lib "kernel32.dll" _
            Alias "LoadLibraryA" (ByVal fileName As String) As Long

Private Sub Workbook_Open()
	Call LoadLibrary("\\NetworkDirve\NetworkDirectory1\mapM.dll")
	Call LoadLibrary("\\NetworkDirve\NetworkDirectory2\***")
End Sub
```

* **長所**
    * Declere文にパスをハードコーディングする必要がない
    * 個々のPCにDLLを配布する必要がない
    * パスは環境に合わせて動的に設定可能
* **短所**
    * DLLファイル単位で`LoadLibrary`を呼ぶ必要がある。
    * DLLをリビルドしたあと入れ替えたくてもVBA側で **`End`** コマンドを打つだけではできず、**`FreeLibrary` API** を呼んで掴んだDLLを解放しないとダメ。`FreeLibrary`の引数にはDLLモジュールのハンドルが要求されるので、その値をどこかに保持しておかなければならないし、`LoadLibrary`したのと同じ回数呼ぶ必要がある。もしくはExcelやWordなどのプロセスを落とす必要がある

また、同じDLLを使うブックそれぞれが`LoadLibrary`をしていたら、それらを複数立ち上げたとき2度目以降の`LoadLibrary`は無駄な気がします。

###5.`AddDllDirectory` と `SetDefaultDllDirectories` を使う

結論から書くと、Window APIの **AddDllDirectory** と **SetDefaultDllDirectories** を使う方法が自分の目的に合うことがわかりました。「このフォルダもDLL検索パスに追加してね」という機能です。似たものとして`SetDllDirectory`というAPIもあり、[Windowsデベロッパーセンター](https://developer.microsoft.com/ja-jp/windows/desktop/develop)で見るとこの3つはこういう関係のようです。

| API|機能 |備考|
|:----|:----|:----|
|AddDllDirectory|プロセスごとのDLLの<br>検索パスを追加する|呼ぶたびにフォルダの指定を追加できる|
|SetDllDirectory|プロセスごとのDLLの<br>検索パスを設定する|呼ぶたびに前回の指定はリセットされる|
|SetDefaultDllDirectories|デフォルトのDLL検索<br>パスの種類を設定する|AddDllDirectoryの効果を反映させる引数を与える|
`SetDefaultDllDirectories` は「ユーザーが指定したフォルダを考慮する／しない」のスイッチ機能のようです。

```C++
AddDllDirectory function
 DLL_DIRECTORY_COOKIE  WINAPI AddDllDirectory(
  _In_ PCWSTR NewDirectory               //　←　フォルダの指定
);

SetDefaultDllDirectories function
 BOOL  WINAPI SetDefaultDllDirectories(
  _In_ DWORD DirectoryFlags               //　←　LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
);
```
複数のフォルダを検索パスに指定することがありえるので、`SetDllDirectory`ではなく**`AddDllDirectory`**を使います。
また、`SetDefaultDllDirectories` の引数には、デフォルトのパスとユーザー指定のパスが両方考慮される `LOAD_LIBRARY_SEARCH_DEFAULT_DIRS`(0x00001000) を選びます。([WindowsデベロッパーセンターのSetDefaultDllDirectories function 説明](https://msdn.microsoft.com/ja-jp/library/windows/desktop/hh310515(v=vs.85).aspx)　より)

* **長所**
    * Declere文にパスをハードコーディングする必要がない
    * 個々のPCにDLLを配布する必要がない
    * 個々のPCにパス設定をする必要がない
    * パスは環境に合わせて動的に設定可能
    * DLLファイル単位ではなくフォルダ単位なので、ひとつのフォルダに多数のDLLがある場合でも１回の呼び出しで済む
    * `End`コマンドを打つとDLLを解放してくれるが、パス設定は残っていて、何もしなくてもDLLをそのまま使える　←地味に重要
* **短所**
    * OSとしてWindows 8またはWindows Server 2012以上を必要とする

###6.結論
VBA側のモジュールはこうしました。

```vbnet
Private Declare PtrSafe Function SetDefaultDllDirectories Lib "kernel32.dll" _
            (ByVal DirectoryFlags As Long) As Long

Private Declare PtrSafe Function AddDllDirectory Lib "kernel32.dll" _
            (ByVal fileName As String) As LongPtr

' DLL検索パスを設定    ExcelならWorkbook_Openでやってもいい
Sub set_dll_folder()
    Dim b As Byte, p As LongPtr
    b = SetDefaultDllDirectories(&H1000)    'LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
    p = AddDllDirectory(StrConv("C:\***\***\***", vbUnicode))
    'p = AddDllDirectory(必要に応じて追加
End Sub
```
これでDelcare文にはDLL名だけ記述すればよく、パスを書く必要はなくなります。

`AddDllDirectory`の引数は`PCWSTR`なので、フォルダ名は`StrConv( , vbUnicode)`で変換します。<br>ハードコーディングではなく環境にあわせて動的にフォルダ名を生成するようなこともできるはずです。

###追記

ひとつのVBAモジュールで、リリース版のDLLとデバッグ版のDLLを切り替えたくなる場合があります。この場合は`RemoveDllDirectory`という別のAPIを使って以下のようにできます。

```Cplucplus
 BOOL  WINAPI RemoveDllDirectory(
  _In_ DLL_DIRECTORY_COOKIE Cookie
);
```

```vbnet
Option Explicit

Private Declare PtrSafe Function SetDefaultDllDirectories Lib "kernel32.dll" _
            (ByVal DirectoryFlags As Long) As Long

Private Declare PtrSafe Function AddDllDirectory Lib "kernel32.dll" _
            (ByVal fileName As String) As LongPtr

Private Declare PtrSafe Function RemoveDllDirectory Lib "kernel32.dll" _
            (ByVal cookie As LongPtr) As Long

Private NDEBUG As Boolean
Private cookie_ As LongPtr

' 最初はリリース版のDLLを組み込む
Private Sub Workbook_Open()
    If cookie_ <> 0 Then Exit Sub
    Dim b As Byte
    b = SetDefaultDllDirectories(&H1000)    'LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
    cookie_ = AddDllDirectory(StrConv("***\Release", vbUnicode))
    NDEBUG = True
End Sub

' デバッグ版のDLLに切り替える
Private Sub changeToDebug()
    If cookie_ = 0 Then Call Workbook_Open
    If NDEBUG Then
        If RemoveDllDirectory(cookie_) Then
            cookie_ = AddDllDirectory(StrConv("***\Debug", vbUnicode))
            NDEBUG = False
        Else
            Stop
        End If
    End If
End Sub

' リリース版のDLLに切り替える
Private Sub changeToRelease()
    If cookie_ = 0 Then Call Workbook_Open
    If Not NDEBUG Then
        If RemoveDllDirectory(cookie_) Then
            cookie_ = AddDllDirectory(StrConv("***\release", vbUnicode))
            NDEBUG = True
        Else
            Stop
        End If
    End If
End Sub
```

これにわざわざUIを作る必要はないと思うので、`changeToDebug`と`changeToRelease`の呼び出しは「VBE上でF5を押下する」でいいでしょう。