#DLLの配布フォルダとパスの問題
VBAで外部のAPI関数を呼び出すとき、Declare文 をモジュールシートの先頭に
Private Declare PtrSafe Function foldl1 Lib "mapM.dll" (ByRef ...
のように書いて関数とDLLを指定します。上記ではDLL名を"mapM.dll"としか書いていませんが、デフォルトでパスが通っているWindows APIと違って、 VBAHaskell などの自作プログラムの場合はDLLの置き場所とパス指定をどう書けばいいかが悩みのタネでした。
考慮している前提条件はこんな感じです。
- 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
モジュールに以下のマクロを記述します。
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ファイル単位で
また、同じDLLを使うブックそれぞれがLoadLibrary
をしていたら、それらを複数立ち上げたとき2度目以降のLoadLibrary
は無駄な気がします。
###5.AddDllDirectory
と SetDefaultDllDirectories
を使う
結論から書くと、Window APIの AddDllDirectory と SetDefaultDllDirectories を使う方法が自分の目的に合うことがわかりました。「このフォルダもDLL検索パスに追加してね」という機能です。似たものとしてSetDllDirectory
というAPIもあり、Windowsデベロッパーセンターで見るとこの3つはこういう関係のようです。
API | 機能 | 備考 |
---|---|---|
AddDllDirectory | プロセスごとのDLLの 検索パスを追加する |
呼ぶたびにフォルダの指定を追加できる |
SetDllDirectory | プロセスごとのDLLの 検索パスを設定する |
呼ぶたびに前回の指定はリセットされる |
SetDefaultDllDirectories | デフォルトのDLL検索 パスの種類を設定する |
AddDllDirectoryの効果を反映させる引数を与える |
SetDefaultDllDirectories は「ユーザーが指定したフォルダを考慮する/しない」のスイッチ機能のようです。 |
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 説明 より)
-
長所
- Declere文にパスをハードコーディングする必要がない
- 個々のPCにDLLを配布する必要がない
- 個々のPCにパス設定をする必要がない
- パスは環境に合わせて動的に設定可能
- DLLファイル単位ではなくフォルダ単位なので、ひとつのフォルダに多数のDLLがある場合でも1回の呼び出しで済む
-
End
コマンドを打つとDLLを解放してくれるが、パス設定は残っていて、何もしなくてもDLLをそのまま使える ←地味に重要
-
短所
- OSとしてWindows 8またはWindows Server 2012以上を必要とする
###6.結論
VBA側のモジュールはこうしました。
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)
で変換します。
ハードコーディングではなく環境にあわせて動的にフォルダ名を生成するようなこともできるはずです。
###追記
ひとつのVBAモジュールで、リリース版のDLLとデバッグ版のDLLを切り替えたくなる場合があります。この場合はRemoveDllDirectory
という別のAPIを使って以下のようにできます。
BOOL WINAPI RemoveDllDirectory(
_In_ DLL_DIRECTORY_COOKIE Cookie
);
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を押下する」でいいでしょう。