rubyからdllを呼び出す

投稿日:

はじめに

どこからから提供された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)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です