はじめに
どこからから提供されたdllを使う場合、一般的にはC++などから呼び出すが、コンパイラを用意したり、コードを変更するたびにコンパイルしなおすのが手間で、少し前からrubyのFiddle::Importerのdlloadを使っていた。
これが、自前でVisual C++でビルドしたrubyと、RubyInstallerから持ってきたrubyでDLL検索の挙動が違うので、その違いを調べてみた。
コードと環境
まずこんなコード。mydll.dllというDLLファイルにadd()という関数が入っていて、それを呼び出すコード。
# test.rb
require 'fiddle/import'
module M
extend Fiddle::Importer
dlload "mydll.dll"
extern "int add(int, int)"
end
p M.add(1,2)
これを以下のような環境で実行する。
[RubyInstallerのruby.exe] > \rubyinstaller-2.7.1-1-x86\bin\ruby.exe --version ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [i386-mingw32] [VC++2015でビルドしたruby.exe] > \Ruby27-32\bin\ruby --version ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [i386-mswin32_140]
挙動の違い
RubyInstaller は(試した範囲では)ruby.exeと同じディレクトリにDLLを置くか、dlloadで指定するdllファイル名をフルパス付きで書かないと、エラーが出て呼び出せなかった。
一方、VC++でビルドした方は、カレントディレクトリにDLLを置いておけば実行できた。
今までは、VC++で自分でビルドしたものを使っていて、rbファイルと同じディレクトリにDLLを置き、そのディレクトリで作業していたので、何不自由なく利用できていたが、RubyInstallerのrubyを試しに使ってみたところdlloadがエラーになって動かないことに気が付いたのが事の発端。
mingw32版とmswin32版のコードの違い
dlloadを実行すると、fiddleのdlopenが呼ばれるようで、その部分のコードを見ると、こうなっている。
HAVE_DLFCN_Hが定義されていたらdlfcn.hを使い、ない場合で_WIN32が定義されて入れたらWin32APIのLoadLibraryを使う。で、mingw32はどうやらdlfcn.hがあるらしい。
#if defined(HAVE_DLFCN_H)
# include <dlfcn.h>
# /* some stranger systems may not define all of these */
#ifndef RTLD_LAZY
#define RTLD_LAZY 0
#endif
#ifndef RTLD_GLOBAL
#define RTLD_GLOBAL 0
#endif
#ifndef RTLD_NOW
#define RTLD_NOW 0
#endif
#else
# if defined(_WIN32)
# include <windows.h>
# define dlopen(name,flag) ((void*)LoadLibrary(name))
# define dlerror() strerror(rb_w32_map_errno(GetLastError()))
# define dlsym(handle,name) ((void*)GetProcAddress((handle),(name)))
# define RTLD_LAZY -1
# define RTLD_NOW -1
# define RTLD_GLOBAL -1
# endif
#endif
mingwのdlfcn.hはこれのようで、dlopenはこうなってる。LoadLibraryではなく、LoadLibraryExにLOAD_WITH_ALTERED_SEARCH_PATH のフラグ付き。
/* POSIX says the search path is implementation-defined.
* LOAD_WITH_ALTERED_SEARCH_PATH is used to make it behave more closely
* to UNIX's search paths (start with system folders instead of current
* folder).
*/
hModule = LoadLibraryEx( (LPSTR) lpFileName, NULL, LOAD_WITH_ALTERED_SEARCH_PATH );
LoadLibraryの検索PATH
LoadLibrary、LoadLibraryExいずれも、デフォルトでカレントディレクトリは探してくれるし、Exの方にLOAD_WITH_ALTERED_SEARCH_PATHをつけても探し方は多少変わるがほぼ同じ。
ではなぜmingwの方 (LoadLibraryExの方) はカレントディレクトリのDLLを見つけてくれないのか。もう少し調べてみた。
RubyInstaller独自コード
こんなページを見つけた。
Dependent DLLs can instead be loaded by setting the Windows DLL search path using the environment variable RUBY_DLL_PATH or by the function RubyInstaller::Runtime.add_dll_directory.
どうやら、RubyInstallerのrubyには独自の修正が入っている模様。確かに、「RUBY_DLL_PATH=.」や RubyInstaller::Runtime.add_dll_directory(“.”) を入れるとDLLを探してくれるようになった。
コード的には、dll_directory.rb で SetDefaultDllDirectories.call(0x00001000) を実行し、singleton.rb でRUBY_DLL_PATHの各ディレクトリに対して DllDirectory.new(path) を実行している。
SetDefaultDllDirectories の引数 0x00001000はドキュメントによると、 LOAD_LIBRARY_SEARCH_DEFAULT_DIRS で、以下と同義とある。つまり、exeと同じディレクトリ → C:\Windows\system32 → AddDllDirectory や SetDllDirectory で設定したディレクトリーの順に検索。この時点でカレントディレクトリが対象外になっている。
- LOAD_LIBRARY_SEARCH_APPLICATION_DIR
- LOAD_LIBRARY_SEARCH_SYSTEM32
- LOAD_LIBRARY_SEARCH_USER_DIRS
対策
ではどうしたらいいか。
環境変数RUBY_DLL_PATH や RubyInstaller::Runtime.add_dll_directoryでカレントディレクトリ(“.”) をDLLの検索範囲に入れると期待通りに探してくれるのは確認できた。
だが、これを見ると、カレントディレクトリからDLLを探すのやセキュリティ的に良くないらしい。
ということは、RUBY_DLL_PATH や RubyInstaller::Runtime.add_dll_directoryでカレントディレクトリ(“.”) を追加する方法は正しくなく、 dlloadでDLLを指定する際にFull PATHを指定するのが正解のよう。
というわけで、最初に書いたコードは次のように書けばいい。これで。mswin32版もmingw32版も両方同じように動作した。
# test.rb
require 'fiddle/import'
module M
extend Fiddle::Importer
#dlload "mydll.dll"
dlload __dir__ + '\mydll.dll'
extern "int add(int, int)"
end
p M.add(1,2)