1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

eval・フックメソッド・クラス拡張ミックスイン ― Rubyのコード生成技法まとめ

1
Last updated at Posted at 2026-03-21

コードを記述するコード

概要

この記事は、「メタプログラミングRuby 第2版」第6章「コードを記述するコード」を読み学習した内容を個人学習用にまとめ直したものです。

Rubyにはコードを動的に生成・実行する強力な仕組みが備わっています。本記事では、Kernel#eval、フックメソッド、クラス拡張ミックスインなど、「コードを記述するコード」にまつわる技法について解説します。

eval

Kernel#evalというカーネルメソッド(全てのオブジェクトが利用できるメソッド)はコード文字列を受け取って、その実行結果を返す。

array = [10, 20]
element = 30
eval("array << element") # => [10, 20, 30]

このメソッドを利用することで、書籍執筆当時のREST Client(gem install rest-clientでインストール可能)では、基本的な4つのhttpメソッドを以下のように定義していた。

# 定義したいメソッド(getの例)
def get(path, *args, &b)
  r[path].get(*args, &b)
end

# evalをループすることで以下のように一度に定義できる
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

POSSIBLE_VERBS.each do |m|
  eval <<-end_eval # ヒアドキュメントとしてコード文字列を受け取る
    def #{m}(path, *args, &b)
      r[path].#{m}(*args, &b)
    end
  end_eval
end

Binding オブジェクト

Bindingはスコープをオブジェクトにまとめたものであり、Bindingを使ってローカルスコープを取得すれば、そのスコープを持ち回すことが可能となる。

Bindingオブジェクトは、Kernel#bindingメソッドで生成できる。

class MyClass
  def my_method
    @x = 1
    binding # ← Bindingオブジェクトを返却
  end
end

b = MyClass.new.my_method # b にはBindingオブジェクトが入る

eval "@x", b  # そのBindingのスコープで @x を評価 → 1

さらに、Rubyには事前定義された定数TOPLEVEL_BINDINGが用意されており、トップレベルのスコープのBindingオブジェクトとなっている。

これを使うことでトップレベルのスコープにどこからでもアクセスできる。

class AnotherClass
  def my_method
    eval "self", TOPLEVEL_BINDING 
  end
end

AnotherClass.new.my_method # => main

Pryの仕組み

デバッグでよく使われるbinding.pryは、このBindingオブジェクトの仕組みを利用している。bindingでその場のスコープを取得し、pryがそのスコープ内で対話的にコードを評価することで、ローカル変数やインスタンス変数を自由に確認・操作できる。

def debug_example
  message = "hello"
  binding.pry # ここで実行が停止し、messageなどのローカル変数を確認できる
end

なお、Ruby 2.5以降では標準添付のIRBを使うbinding.irbでも、その場のBindingを開いて対話的に確認できる。Ruby 3.3以降ではIRBとdebug gemの連携も改善されている。

evalの問題点

evalは任意のコード文字列を実行できるため、外部からの入力をそのまま渡すとコードインジェクション攻撃の対象となる。

# ユーザーの入力をそのままevalに渡す危険な例
def execute_user_input(input)
  eval(input)
end

# 悪意のあるユーザーが以下のような入力を送った場合
execute_user_input("system('rm -rf /')") # システムコマンドが実行されてしまう

このため、外部入力をevalに渡すことは避けるべきであり、evalの使用は開発者が管理するコード文字列のみに限定するべきである。

動的なメソッド定義などevalと同等のことを実現したい場合は、define_methodsendなどのより安全な代替手段の利用が推奨される。

# evalを使った定義(前述の例)
POSSIBLE_VERBS.each do |m|
  eval <<-end_eval
    def #{m}(path, *args, &b)
      r[path].#{m}(*args, &b)
    end
  end_eval
end

# define_methodでメソッド名を動的に定義(evalの「def #{m}」部分の代替)
# sendでr[path]に対してメソッドmを動的に呼び出す(evalの「r[path].#{m}(...)」部分の代替)
# コード文字列を経由しないため、外部入力が含まれていても任意のコードとして実行されない
POSSIBLE_VERBS.each do |m|
  define_method(m) do |path, *args, &b|
    r[path].send(m, *args, &b) # r[path].get(...) や r[path].put(...) と同等
  end
end

rest-client gemで実際に行われたevalからdefine_methodへの移行

本記事で紹介したrest-client gemのevalによるメソッド定義は、書籍執筆時点のコードに基づいています。rest-client gem バージョン2.0.0(2016年リリース)でdefine_methodを使った実装に変更されました。これはまさに本セクションで解説した「evalからdefine_methodへの移行」が実践された例です。

セーフレベル

かつてはセーフレベル($SAFE)により汚染された文字列のeval実行を制限できたが、$SAFEはRuby 2.7で非推奨、Ruby 3.0で通常のグローバル変数となり廃止された。関連するtaint/untaint/tainted?などの汚染メソッド群もRuby 3.0以降は実質no-opとなり、Ruby 3.2で完全に削除されている。ERBのsafe_level引数もRuby 2.6で非推奨となり、現在も互換目的で引数自体は残っているが利用は推奨されない。現在のRubyではこれらの仕組みに頼らず、そもそも外部入力をevalに渡さない設計が求められる。

フックメソッド

フックメソッドとは、特定のイベントが発生した際にRubyが自動的に呼び出すメソッドである。これをオーバーライドすることで、イベント発生時に独自の処理を差し込むことができる。

例えば、Class#inheritedは、クラスが継承された時に呼び出されるフックメソッドである。

class BaseClass
  def self.inherited(subclass)
    puts "#{subclass}がBaseClassを継承した"
  end
end

class SubClass < BaseClass
end
# => SubClassがBaseClassを継承した

様々なフックメソッド

inheritedでクラスのライフサイクルにプラグインできるように、Module#includedModule#prependedを使用することでモジュールのライフサイクルにもプラグイン可能。

module M1
  def self.included(othermod)
    puts "M1は#{othermod}にインクルードされた"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2は#{othermod}にプリペンドされた"
  end
end

class C
  include M1
  prepend M2
end

# =>  M1はCにインクルードされた
# =>  M2はCにプリペンドされた

同様に、Module#extendedでモジュールがextendされた時の処理を差し込める。

module M3
  def self.extended(object)
    puts "M3は#{object}にエクステンドされた"
  end
end

obj = Object.new
obj.extend M3
# => M3は#<Object:0x...>にエクステンドされた

また、メソッドのライフサイクルに対応するフックメソッドも用意されている。

フックメソッド 呼び出されるタイミング
method_added メソッドが定義された時
method_removed メソッドがremove_methodで削除された時
method_undefined メソッドがundef_methodで未定義にされた時
singleton_method_added 特異メソッドが定義された時
singleton_method_removed 特異メソッドが削除された時
singleton_method_undefined 特異メソッドが未定義にされた時
class MyClass
  def self.method_added(method_name)
    puts "#{method_name}が定義された"
  end

  def my_method; end
end
# => my_methodが定義された

インクルード時にクラスメソッドを同時に挿入するイディオム

VCR(HTTP呼び出しを記録するgem)ではフックメソッドを利用し、以下のような方法でモジュールのインクルード時にそのモジュールのクラスメソッドをインスタンスメソッドと同時に挿入している。

  1. Rubyが、Bodyincludedフックを呼び出す
  2. フックがRequestに戻り、ClassMethodsモジュールをエクステンドする
  3. extendメソッドが、ClassMethodsのメソッドをRequestの特異クラスにインクルードする
module VCR
  class Request
    include Normalizers::Body
    # ....
  end
end

module VCR
  module Normalizers
    module Body
      def self.included(klass)
        # インクルード先のクラスでClassMethodsモジュールのメソッドをエクステンド
        klass.extend ClassMethods
      end

      # クラスメソッドを定義するためのモジュール内モジュール
      module ClassMethods
        def body_from(hash_or_string)
          #...
        end
      end

      # ...
    end
  end
end

ActiveSupport::Concern

Railsでは、上記のincluded + extend ClassMethodsパターンをActiveSupport::Concernで簡潔に記述できる。

module Normalizers
  module Body
    extend ActiveSupport::Concern

    # ClassMethodsモジュール内のメソッドが自動的にクラスメソッドとして追加される
    module ClassMethods
      def body_from(hash_or_string)
        #...
      end
    end

    # includedブロック内はインクルード先のクラスのコンテキストで実行される
    included do
      # バリデーションやコールバックなどの設定もここに書ける
    end
  end
end

まとめ

  • evalはコード文字列を動的に実行できるが、セキュリティリスクがあるためdefine_method等の代替手段が推奨される
  • Bindingはスコープをオブジェクトとして持ち回し、evalやデバッグツールで活用できる
  • フックメソッドでクラス・モジュール・メソッドのライフサイクルイベントに処理を差し込める
  • クラス拡張ミックスインでインクルード時にクラスメソッドも同時に追加できる

参考文献

この記事は以下の情報を参考にして執筆しました。

1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?