Gangmax Blog

Ruby学习笔记第二篇: require

关于Ruby语言,之前我一直有一个疑问:当Ruby解释器遇到”require ‘abc’”这样的表达式的时候,它是如何从本地查找并加载对应的ruby程序文件的呢?

在Java中,这是通过CLASSPATH概念实现的;在Python中,有PYTHONPATH(参考这里)。所以很自然地,我相信在Ruby语言里面也有对应的概念,用于Ruby解释器在运行时从一个文件系统查找对应的Ruby程序文件,比如”$:/$LOAD_PATH”。

有意思的是:自从Ruby有了gem的概念,看起来这个search/load的过程改由gem来做了。可是当我尝试google gem背后的机制时,却没有搜索到描述gem search/load机制的介绍文章。

我今天突发奇想在irb中运行了以下代码:

1
2
3
4
5
1.9.2p290 :001 > require.class
ArgumentError: wrong number of arguments (0 for 1)
from /home/user/.rvm/rubies/ruby-1.9.2-p290/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:34:in `require'
from (irb):1
from /home/user/.rvm/rubies/ruby-1.9.2-p290/bin/irb:16:in `<main>'

虽然得到了错误,但它却暴露出了gem require的实现代码:

custom_require.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++

module Kernel

if defined?(gem_original_require) then
# Ruby ships with a custom_require, override its require
remove_method :require
else
##
# The Kernel#require from before RubyGems was loaded.

alias gem_original_require require
private :gem_original_require
end

##
# When RubyGems is required, Kernel#require is replaced with our own which
# is capable of loading gems on demand.
#
# When you call <tt>require 'x'</tt>, this is what happens:
# * If the file can be loaded from the existing Ruby loadpath, it
# is.
# * Otherwise, installed gems are searched for a file that matches.
# If it's found in gem 'y', that gem is activated (added to the
# loadpath).
#
# The normal <tt>require</tt> functionality of returning false if
# that file has already been loaded is preserved.

def require path
if Gem.unresolved_deps.empty? then
gem_original_require path
else
spec = Gem::Specification.find { |s|
s.activated? and s.contains_requirable_file? path
}

unless spec then
found_specs = Gem::Specification.find_in_unresolved path
unless found_specs.empty? then
found_specs = [found_specs.last]
else
found_specs = Gem::Specification.find_in_unresolved_tree path
end

found_specs.each do |found_spec|
found_spec.activate
end
end

return gem_original_require path
end
rescue LoadError => load_error
if load_error.message.end_with?(path) and Gem.try_activate(path) then
return gem_original_require(path)
end

raise load_error
end

private :require

end

通过以上代码的启示,我们可以明白以下内容:

  1. Ruby语言自带的require功能是由Kernel module作为一个method提供的,具体的功能可以参考这里。基本上它的功能是:从$LOAD_PATH给定的路径列表里面,查找以给定参数作为名称的文件,如果文件的扩展名是”.rb”,则将它作为Ruby源文件加载;如果文件的扩展名是”.so”,”.o”或”.dll”,则将它作为一个Ruby本地扩展加载;如果找不到任何匹配的文件,则抛出LoadError。

  2. 有了gem之后,gem用自己的require覆盖(override)了Ruby语言默认的require实现:

    1. 首先,使用系统自带的require方法search/load给定参数,如果成功则完成load过程;
    2. 如果系统require加载不成功,则在系统中已经安装的gems中,查找给定的文件名称,如果找到,则激活(activate)该文件所属的gem(即将该gem对应的目录加入loadpath);
    3. 如果仍然找不到,则抛出LoadError。
  3. 另外,在Ruby1.9中,还新增了Kernel.require_relative方法,主要是用来加载给定相对路径中的文件,在”Programming Ruby 1.9”一书中的”Chapter 16 Organizing Your Source”中被提及,具体可以参考这里

从这里,Ruby再次体现了Neal Ford所谓的灵活:gem可以通过override Ruby默认的require方法构建新的require功能,在Java里面这是不可想象的:我们最多可以定制新的ClassLoader,却无法修改系统ClassLoader的delegate机制或者系统ClassLoader的行为。

在这背后,是Paul Graham所说的两种编程语言哲学的区别。

Comments