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


実行される順番としては、

  1. image_voodoo_test.rb:1 require 'rubygems'
    1. $JRUBY_HOME/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb によって、Kernel#requireが拡張される
  2. image_voodoo_test.rb:3 require 'image_science'
    1. Kernel#gem_original_require('image_science')
      1. $LOAD_PATH上に該当するファイルが見つからなくて、LoadErrorがraiseされる
    2. 例外がrescueされ、見つからなかったgemについてGem.activateが実行される
      1. $LOAD_PATHが変更される$JRUBY_HOME/lib/ruby/site_ruby/1.8/rubygems.rbのGem.activate
    3. もう一回Kernel#gem_original_require('image_science')
      1. $JRUBY_HOME/lib/ruby/gems/1.8/gems/image_voodoo-0.3/lib/image_science.rbがロードされる
        1. require 'image_voodoo'
          1. $JRUBY_HOME/lib/ruby/gems/1.8/gems/image_voodoo-0.3/lib/image_voodoo.rbがロードされる


~/image_voodoo_test1/image_voodoo_test.rbのrequire 'image_science'をrequire 'image_voodoo'に変えるとconst_missingになるのは、最初のKernel#gem_original_requireを実行するときに$LOAD_PATH上にファイルが見つかってしまうからですね。なるほど〜。


toRubyで得た教訓が活きて勉強になりました。ソースコード読んでよかった〜!