# 你知道 require 幫你做了什麼事嗎?

> require 在 Ruby 和 Rails 中是用來引用模組的基本方法，透過此方法可以將其他程式庫加載至當前環境。這篇文章詳細說明了 require 的運作原理，並探討了它如何影響 $LOAD_PATH 和 $LOADED_FEATURES 的內容，讓開發者能夠理解需要這些變數的原因及其背後的運作邏輯。

Published: 2016-05-01
URL: https://kaochenlong.com/require

---

也許你曾在 Ruby 或 Rails 專案中寫過這行語法：

```ruby
require &quot;digest&quot;
puts Digest::MD5.hexdigest(&quot;I love you&quot;)    # =&gt; &quot;e4f58a805a6e1fd0f6bef58c86f9ceb3&quot;
```

上面這段語法的大意是「引用 &quot;digest&quot; 模組，然後使用那個模組裡的某個方法產生 MD5 編碼字串」。但你知道這個 `require` 到底做了什麼事嗎? 以下將用我自己寫的一個名為 [takami](https://rubygems.org/gems/takami) 的套件為例，它是一個完全沒功能的空包彈套件，純粹練功用途。

&lt;!--more--&gt;

## require 是怎麼運作的?

讓我們先打開終端機，直接試著印出 `$PATH` 這個變數來看看：

    $ echo $PATH
    /Users/user/.rvm/gems/ruby-2.3.1/bin:/Users/user/.rvm/gems/ruby-2.3.1@global/bin ...[略]...

在你電腦上的輸出結果可能跟我的不太一樣。當你想執行某個程式，例如 `git log`，系統會依照 `$PATH` 列出來的順序，一個一個的問「請問在這個路徑是不是有 `git` 這個程式?」，如果有，就執行它；如果都沒有，就會出現 command not found 的訊息。

在 Ruby 裡有個叫做 `$LOAD_PATH` 的全域變數(可簡寫成 `$:`)的功用跟 $PATH 類似，讓我們開 irb 來試試：

    $ irb
    &gt;&gt; $LOAD_PATH
    =&gt; [&quot;/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib&quot;, ...[略]...

    &gt;&gt; $:
    =&gt; [&quot;/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib&quot;, ...[略]...

當你執行 `require &quot;takami&quot;` 之後，它會把這個 gem 加到 $LOAD_PATH 裡，並且把它加到另一個全域變數 `$LOADED_FEATURES`(可簡寫成 `$&quot;`) 裡：

    $ irb
    &gt;&gt; $LOAD_PATH.count
    =&gt; 9

    &gt;&gt; $LOADED_FEATURES.count
    =&gt; 59

    &gt;&gt; require &quot;takami&quot;
    =&gt; true

    &gt;&gt; $LOAD_PATH.count
    =&gt; 10

    &gt;&gt; $LOAD_PATH
    =&gt; [&quot;/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib&quot;, &quot;/Users/user/.rvm/gems/ruby-2.3.1/gems/takami-0.0.1/lib&quot;, ...[略]...

    &gt;&gt; $LOADED_FEATURES
    =&gt; [&quot;enumerator.so&quot;, &quot;thread.rb&quot;,  ...[略] ... &quot;/Users/user/.rvm/gems/ruby-2.3.1/gems/takami-0.0.1/lib/takami.rb&quot;]

    &gt;&gt; $LOADED_FEATURES.count
    =&gt; 61

在 require 之前 $LOAD\_PATH 只有 9 個，require 之後變 10 個了，而 $LOADED_FEATURES 的數量也從原本的 59 變成 61 了。除了可以在 $LOADED_FEATURES 看到新增加跟 takami 套件有關的東西之外，也可以看的出來 takami 套件的確在 $LOAD\_PATH 裡了，這樣一來就可以直接使用我那個空包彈的 Takami 套件了。

## 原始碼挖挖挖!

首先，`require` 並不是關鍵字，它在 Ruby 裡只是個一般的方法(定義在 Kernel 模組裡)，所以就讓我們試著挖 Ruby 的原始碼出來看看它到底做了什麼事。

但在開挖之前，要先講一下歷史故事，免得挖錯地方。在 Ruby 1.8 時代，Ruby 的 require 就是普通的 require，在 Ruby 1.9 之後，require 的事情則是交給 `rubygems` 套件來管理，所以我們現在應該要去挖 rubygems 套件的[原始碼](https://github.com/rubygems/rubygems)才能挖得到 require 真正的行為。

```ruby
# 檔案: rubygems/lib/rubygems/core_ext/kernel_require.rb
require &#39;monitor&#39;

module Kernel

  RUBYGEMS_ACTIVATION_MONITOR = Monitor.new # :nodoc:

  if defined?(gem_original_require) then
    remove_method :require
  else
    alias gem_original_require require
    private :gem_original_require
  end

  def require path
    RUBYGEMS_ACTIVATION_MONITOR.enter

    path = path.to_path if path.respond_to? :to_path

    spec = Gem.find_unresolved_default_spec(path)
    if spec
      Gem.remove_unresolved_default_spec(spec)
      gem(spec.name)
    end

    if Gem::Specification.unresolved_deps.empty? then
      RUBYGEMS_ACTIVATION_MONITOR.exit
      return gem_original_require(path)
    end

    spec = Gem::Specification.find_active_stub_by_path path

    begin
      RUBYGEMS_ACTIVATION_MONITOR.exit
      return gem_original_require(path)
    end if spec

    found_specs = Gem::Specification.find_in_unresolved path

    if found_specs.empty? then
      found_specs = Gem::Specification.find_in_unresolved_tree path

      found_specs.each do |found_spec|
        found_spec.activate
      end

    else

      names = found_specs.map(&amp;:name).uniq

      if names.size &gt; 1 then
        RUBYGEMS_ACTIVATION_MONITOR.exit
        raise Gem::LoadError, &quot;#{path} found in multiple gems: #{names.join &#39;, &#39;}&quot;
      end

      valid = found_specs.reject { |s| s.has_conflicts? }.first

      unless valid then
        le = Gem::LoadError.new &quot;unable to find a version of &#39;#{names.first}&#39; to activate&quot;
        le.name = names.first
        RUBYGEMS_ACTIVATION_MONITOR.exit
        raise le
      end

      valid.activate
    end

    RUBYGEMS_ACTIVATION_MONITOR.exit
    return gem_original_require(path)
  rescue LoadError =&gt; load_error
    RUBYGEMS_ACTIVATION_MONITOR.enter

    begin
      if load_error.message.start_with?(&quot;Could not find&quot;) or
          (load_error.message.end_with?(path) and Gem.try_activate(path)) then
        require_again = true
      end
    ensure
      RUBYGEMS_ACTIVATION_MONITOR.exit
    end

    return gem_original_require(path) if require_again

    raise load_error
  end

  private :require
end
```

上面這段我把部份的註解拿掉省一點空間。當 `rubygems` 被 require 進來之後，原本 Kernel 模組的 require 方法就被換成 rubygems 裡的 require 方法，並且把原本 Kernel 模組定義的 require 方法 alias 成 `gem_original_require`。

如果該套件本來就已經在 $LOAD_PATH 上的話，它會直接呼叫原本 Kernel 模組定義的 require 方法來載入套件；但如果在 $LOAD_PATH 上沒有找不到的話，它就會試著開始去找看看是不是在系統上有安裝這個套件，然後執行 `activate` 方法，將它加到 $LOAD_PATH 裡。

讓我們來玩一下分解動作：

    $ irb
    &gt;&gt; $LOAD_PATH
    =&gt; [&quot;/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib&quot;, ...[略]...

    # 原本只有 9 個
    &gt;&gt; $LOAD_PATH.count
    =&gt; 9

    # 找到我的套件
    &gt;&gt; my_gem = Gem::Specification.find_by_name(&quot;takami&quot;)
    =&gt; #&lt;Gem::Specification:0x3fdcf54346c8 takami-0.0.1&gt;

    # activate 方法會把這個 gem 加到 $LOAD_PATH 裡
    &gt;&gt; my_gem.activate
    =&gt; true

    # 套件現在加到 $LOAD_PATH 裡了
    &gt;&gt; $LOAD_PATH
    =&gt; [&quot;/Users/user/.rvm/gems/ruby-2.3.1@global/gems/did_you_mean-1.0.0/lib&quot;, &quot;/Users/user/.rvm/gems/ruby-2.3.1/gems/takami-0.0.1/lib&quot;, ...[略]...

    # 數量也變 10 個
    &gt;&gt; $LOAD_PATH.count
    =&gt; 10

    # 咦? 不是在 $LOAD_PATH 了嗎? 怎麼不行
    &gt;&gt; Takimi
    NameError: uninitialized constant Takimi
      from (irb):8
      from /Users/user/.rvm/rubies/ruby-2.3.1/bin/irb:11:in `&lt;main&gt;&#39;

    # 呼叫原本 Kernel 的 require 方法
    &gt;&gt; gem_original_require &#39;takami&#39;
    =&gt; true

    # 可正常執行!
    &gt;&gt; Takami
    =&gt; Takami

並不是有在 $LOAD_PATH 裡就可以直接使用，事實上它還是得靠原本 Kernel 模組的 require 方法把它加到 $LOADED_FEATURES 之後才能使用。所以說穿了，rubygems 其實只算是在幫你管理 $LOAD_PATH 的路徑而已(當然也沒這麼單純啦)，真正 require 的行為還是在 Kernel 模組裡。

順帶一提，如果 require 是 Ruby 內建的標準函式庫(StdLib)，例如本文一開始範例裡提到的 `digest`，$LOAD_PATH 本身並不會有任何變化(因為這些內建的本來就找得到了啊)，但還是會把該模組加到 $LOADED_FEATURES 裡。

## 原本 Kernel 模組的 require

繼續挖 Ruby 原始碼來看看：

```c
// 檔案：load.c

VALUE
rb_require_safe(VALUE fname, int safe)
{
  int result = rb_require_internal(fname, safe);

  if (result &gt; TAG_RETURN) {
    JUMP_TAG(result);
  }
  if (result &lt; 0) {
    load_failed(fname);
  }

  return result ? Qtrue : Qfalse;
}
```

這段是在 Kernel 模組裡 require 方法的實作，真正載入的行為是在 `rb_require_internal` 這個 function 裡，有興趣的朋友可以繼續往下追。Ruby 會檢查那個檔案是不是已經在 $LOADED_FEATURES 裡，如果不在就會把它加進來。

另外，`rb_require_internal` 這個 function 的回傳結果可能有以下 4 種：

```c
/*
 * returns
 *  0: if already loaded (false)
 *  1: successfully loaded (true)
 * &lt;0: not found (LoadError)
 * &gt;1: exception
 */
```

所以，如果 require 成功，`rb_require_safe` 這個 function 會回傳 `true`，但如果之前就已經 require 過的話則會回傳 `false`，如果什麼都找不到，則是直接產生 `LoadError ` 的錯誤訊息：

    $ irb
    # 第一次 require，回傳 true
    &gt;&gt; require &quot;takami&quot;
    =&gt; true

    # 第二次 require 相同的套件，回傳 false
    &gt;&gt; require &quot;takami&quot;
    =&gt; false

    # require 一個找不到的 gem
    &gt;&gt; require &quot;the_gem_not_install&quot;
    LoadError: cannot load such file -- the_gem_not_install

## 小結

也許這個簡單的 require 指令一下子就跑完了，但當你知道它背後是怎麼運作之後，你對 Ruby 就可以有更深一層的了解。在挖原始碼的過程有點辛苦但也很有趣，除了可以看看大師們是怎麼設計的，還可以看到一些平常看不到的東西 :)


