rubygemsによるrequireの拡張
http://groups.google.com/group/rubeus/msg/d3c923ece8b3a4d0 で疑問。
- Q. image_voodooを使うときにはカレントディレクトリにimage_voodoo.rbというファイルがあっても動作するのに、どうしてrubeusは動作しないの?
- A. image_voodooのサンプルに仕掛けあり!
概要
image_voodoo(http://blog.nicksieger.com/articles/2008/03/27/imagevoodoo-0-1-released)は、RMagick(http://rmagick.rubyforge.org/)が嫌だっていう人のためのimage_science(http://seattlerb.rubyforge.org/ImageScience.html)というrubyのライブラリと互換のあるjrubyのライブラリ。速度などが向上しているらしい。最新バージョンは0.3。
jruby -S gem install image_voodoo
でインストールできる。
ディレクトリ構成はこんな感じになっている。
$ cd $JRUBY_HOME/lib/ruby/gems/1.8/gems/ $ find image_voodoo-0.3 image_voodoo-0.3 image_voodoo-0.3/bin image_voodoo-0.3/bin/image_voodoo image_voodoo-0.3/lib image_voodoo-0.3/lib/image_science.rb image_voodoo-0.3/lib/image_voodoo image_voodoo-0.3/lib/image_voodoo/version.rb image_voodoo-0.3/lib/image_voodoo.rb image_voodoo-0.3/LICENSE.txt image_voodoo-0.3/Manifest.txt image_voodoo-0.3/Rakefile image_voodoo-0.3/README.txt image_voodoo-0.3/samples image_voodoo-0.3/samples/bench.rb image_voodoo-0.3/samples/checkerboard.jpg image_voodoo-0.3/samples/file_greyscale.rb image_voodoo-0.3/samples/file_thumbnail.rb image_voodoo-0.3/samples/file_view.rb image_voodoo-0.3/samples/in-memory.rb image_voodoo-0.3/test image_voodoo-0.3/test/pix.png image_voodoo-0.3/test/test_image_science.rb
で、今回のポイントは
image_voodoo-0.3/lib/image_science.rb image_voodoo-0.3/lib/image_voodoo.rb
この二つのファイル。
機能を実装しているのは、後者image_voodoo-0.3/lib/image_voodoo.rbの方だけど、前者image_voodoo-0.3/lib/image_voodoo.rbはimage_scienceとの互換性の為に(require 'image_science'としてもちゃんと動くように)以下のように実装されている。
require 'image_voodoo' # HA HA...let the pin-pricking begin ImageScience = ImageVoodoo
http://jruby-extras.rubyforge.org/svn/trunk/image_voodoo/lib/image_science.rb
検証
検証するするためのディレクトリを作ります
$ mkdir ~/image_voodoo_test1 $ cd ~/image_voodoo_test1
今回実行するのは、http://blog.nicksieger.com/articles/2008/03/27/imagevoodoo-0-1-released に書いてあるものにrequireを足したもの。
require 'rubygems' puts $:.sort.inspect require 'image_science' ImageVoodoo.with_image("checkerboard.jpg") do |img| img.preview end
image_voodoo_test.rbという名前で作り、さらに
$ touch image_voodoo.rb
とかで空っぽのimage_voodoo.rbというファイルを作る。
あと、サンプル用の画像もコピー。
$ cp $JRUBY_HOME/lib/ruby/gems/1.8/gems/image_voodoo-0.3/samples/checkerboard.jpg ~/image_voodoo_test1/
重要なファイルをまとめると、
$JRUBY_HOME/lib/ruby/gems/1.8/gems/image_voodoo-0.3/lib/image_science.rb $JRUBY_HOME/lib/ruby/gems/1.8/gems/image_voodoo-0.3/lib/image_voodoo.rb ~/image_voodoo_test1/image_voodoo_test.rb ~/image_voodoo_test1/image_voodoo.rb
で、実行すると、
$ jruby image_voodoo_test.rb [".", "/usr/local/jruby-1.1.2/lib/ruby/1.8", "/usr/local/jruby-1.1.2/lib/ruby/1.8/java", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby/1.8", "lib/ruby/1.8"]
と出力され、正常に動作する。
カレントディレクトリは$LOAD_PATHの一番先頭にあるのだから、~/image_voodoo_test1/image_voodoo.rbが呼び出されて、
`const_missing': uninitialized constant ImageVoodoo (NameError)
というエラーが出ても良さそうなもんだけど出ない!ほわーい?
考えても仕方ないので、$JRUBY_HOME/lib/ruby/gems/1.8/gems/image_voodoo-0.3/lib/image_science.rb をいじって以下のようにしてみた。
puts "image_science loaded: #{$:.inspect}" require 'image_voodoo' # HA HA...let the pin-pricking begin ImageScience = ImageVoodoo
で、再度実行。
$ jruby image_voodoo_test.rb [".", "/usr/local/jruby-1.1.2/lib/ruby/1.8", "/usr/local/jruby-1.1.2/lib/ruby/1.8/java", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby/1.8", "lib/ruby/1.8"] image_science loaded: ["/usr/local/jruby-1.1.2/lib/ruby/gems/1.8/gems/image_voodoo-0.3/bin", "/usr/local/jruby-1.1.2/lib/ruby/gems/1.8/gems/image_voodoo-0.3/lib", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby/1.8", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby", "/usr/local/jruby-1.1.2/lib/ruby/1.8", "/usr/local/jruby-1.1.2/lib/ruby/1.8/java", "lib/ruby/1.8", "."]
なんとimage_science.rbがロードされたときには$:の値が全然違う。先頭にあったカレントディレクトリが最後になっている!
そうなんだー。
でも、なぜrubeusはダメで、image_voodooは大丈夫なのか?ポイントは~/image_voodoo_test1/image_voodoo_test.rbのrequireにある。
require 'rubygems' puts $:.sort.inspect require 'image_science' ImageVoodoo.with_image("checkerboard.jpg") do |img| img.preview end
このサンプルは他のサンプルをまねて、
require 'image_voodoo'
ではなく、
require 'image_science'
と書いたからだ!この部分を
require 'image_voodoo'
にして実行すると・・・
$ jruby image_voodoo_test.rb [".", "/usr/local/jruby-1.1.2/lib/ruby/1.8", "/usr/local/jruby-1.1.2/lib/ruby/1.8/java", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby", "/usr/local/jruby-1.1.2/lib/ruby/site_ruby/1.8", "lib/ruby/1.8"] image_voodoo_test.rb:4:in `const_missing': uninitialized constant ImageVoodoo (NameError) from image_voodoo_test.rb:4
予想通り!
まとめ
キーとなるのは、$JRUBY_HOME/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb (以下はコメントを除いてあります)
require 'rubygems' module Kernel alias gem_original_require require # :nodoc: def require(path) # :nodoc: gem_original_require path rescue LoadError => load_error if load_error.message =~ /\A[Nn]o such file to load -- #{Regexp.escape path}\z/ and spec = Gem.searcher.find(path) then Gem.activate(spec.name, "= #{spec.version}") gem_original_require path else raise load_error end end end # module Kernel
実行される順番としては、
- image_voodoo_test.rb:1 require 'rubygems'
- image_voodoo_test.rb:3 require 'image_science'
- Kernel#gem_original_require('image_science')
- $LOAD_PATH上に該当するファイルが見つからなくて、LoadErrorがraiseされる
- 例外がrescueされ、見つからなかったgemについてGem.activateが実行される
- もう一回Kernel#gem_original_require('image_science')
- Kernel#gem_original_require('image_science')
~/image_voodoo_test1/image_voodoo_test.rbのrequire 'image_science'をrequire 'image_voodoo'に変えるとconst_missingになるのは、最初のKernel#gem_original_requireを実行するときに$LOAD_PATH上にファイルが見つかってしまうからですね。なるほど〜。
toRubyで得た教訓が活きて勉強になりました。ソースコード読んでよかった〜!