MSYSのrxvtやCygwinでVagrantを実行した際に出力されるNIO4RのDLLエラーについて

Vagrantに内包されているMSYSのrxvtやCygwinで、Vagrantのコマンドを叩いた時に検出されたエラーについてメモしとく。
あらかじめ書いておくが、問題の回避はできたものの、根本的な原因の究明にまでは到っていない。

環境

VagrantWindows版のパッケージを使っていて、vagrant-berkshelfはコマンドプロンプトからインストールしている。

事象

コマンドプロンプトからVagrantのコマンドを実行すると問題無く動いていたのだが、Vagrantに内包されているMSYSのrxvtや、Cygwinから実行すると以下のエラーが出力されて止まってしまった。

$ vagrant status
Vagrant failed to initialize at a very early stage:

The plugins failed to load properly. The error message given is
shown below.

1114: ダイナミック リンク ライブラリ (DLL) 初期化ルーチンの実行に失敗しました。   - C:/Users/heroween/.vagrant.d/gems/gems/nio4r-1.0.0/lib/nio4r_ext.so

なんでかなーと原因を調べてみた。

原因を探る

まず、問題を起こしているNIO4Rについてだが、これはBerkshelfが依存しているGemであり、Vagrantプラグインであるvagrant-berkshelfをインストールすると一緒に入ってくる。
このNIO4Rはクロスプラットフォームな作りとなっており、実行環境のRubyの処理系に応じて実装が変わる。

  • MRI/YARV、Rubiniusの場合はlibevをベースとしてC言語で実装された拡張ライブラリが使われる。
  • JRubyの場合はJava NIOをベースとしたJavaで実装された拡張ライブラリが使われる。
  • 上記以外であればPure Ruby実装のライブラリが使われる。

と、READMEではこのように説明されている。
Pure Rubyだと処理速度が遅いので、CやJava実装の拡張ライブラリも使えるようになっているのだろう。

で、今回はVagrantに内包されている、WindowsRuby(MRI)で実行したら問題が起きた。
上記の説明の通りであれば、MRIなのでC実装の拡張ライブラリが使われて、その際にDLLエラーが発生した…ものと思われるのだが、腑に落ちない点がある。
MSYSのrxvtやCygwinで出るエラーが、コマンドプロンプトでは出ずに、普通に動いているという点だ。

よく分からないので、実装の変わる条件がどうなっているのか、インストールされたNIO4Rのコード(nio.rb)を確認してみた。

if ENV["NIO4R_PURE"] || (ENV["OS"] =~ /Windows/i && !defined?(JRUBY_VERSION))
  require 'nio/monitor'
  require 'nio/selector'
  NIO::ENGINE = 'select'
else
  require 'nio4r_ext'

  if defined?(JRUBY_VERSION)
    require 'java'
    require 'jruby'
    org.nio4r.Nio4r.new.load(JRuby.runtime, false)
    NIO::ENGINE = 'java'
  else
    NIO::ENGINE = 'libev'
  end
end

これを見ると、OSがWindowsだとJRubyで無い限りは、必ずPure RubyのNIO4Rが使われるようだ…。
コマンドプロンプトから実行した場合、エラーが出ずに正常動作するのは、C実装でなくPure RubyのNIO4Rが使われているからということになる。
それでは、MSYSのrxvtやCygwinで実行すると、何故、Pure RubyでなくC実装のNIO4Rが使われるのだろうか。
とりあえず、環境変数OSをコマンドプロンプトで確認してみると、値は以下のように表示された。

C:¥User¥heroween>echo %OS%
Windows_NT

同じようにrxvtとCygwinでも確認すると、

$ echo $OS
Windows_NT

やはり"Windows_NT"と表示される。
それでは、Vagrant実行時にどうなっているのかと、nio.rbに直接puts ENV["OS"]と書き込んで実行したところ、

# コマンドプロンプト
Windows_NT

# MSYSのrxvt
MINGW32_NT-6.1

# Cygwin
CYGWIN_NT-6.1

と、ターミナル毎に異なった値が出力された。
そこで、VagrantのShell実装コマンドを見てみると、以下のように環境変数OSが上書きされていることが分かった。

# Determine the OS that we're on, which is used in some later checks.
# It is very important we do this _before_ setting the PATH below
# because uname dependencies can conflict on some platforms.
OS=$(uname -s 2>/dev/null)

unameコマンドの値で環境変数OSから"Windows"が消えてしまったことで、C実装のNIO4Rが使われるようになっていた訳である。

ここまでで、コマンドプロンプトとMSYSのrxvtやCygwinで挙動が変わっていた原因は分かった。
それでは、DLLエラーを吐く根本的な原因はなんなのだろうか。

処理の実行に失敗しているnio4r_ext.soをobjdumpで確認してみると、

$ objdump -T nio4r_ext.so

nio4r_ext.so:     file format pei-i386

DYNAMIC SYMBOL TABLE:
no symbols


C:¥MinGW¥bin¥objdump.exe: nio4r_ext.so: not a dynamic object

と表示されたので、soファイル自体壊れている可能性もありそうなのだが、Windows環境とC言語に明るくない自分では、究明するまでにかなりハマりそうなので、ここで打ち切ることにした。

対応

対応として、環境変数NIO4R_PUREに真の値を設定しておけば、必ずPure RubyのNIO4Rを使うようになるので、この問題を回避することはできる。

nio.rbのコードを見ると分かるが、そもそも、OSがWindowsの場合はC実装のNIO4Rを使わないような条件になっているし、過去のIssueやPRを追ってみても、単純にサポートしてないだけのように思われる。
どうしてもWindowsでC実装のNIO4Rを使いたいという訳でも無いので、一先ずはここまでにしておく。