メタプログラミングRuby第2版:6章:コードを書くコード

スポンサーリンク

メタプログラミングRubyとは?

出版社である O’Reilly Japanのサイトからの引用です。

本書はRubyを使ったメタプログラミングについて解説する書籍です。メタプログラミングとは、プログラミングコードを記述するコードを記述することを意味します。前半では、メタプログラミングの背景にある基本的な考えを紹介しながら、動的ディスパッチ、ゴーストメソッド、フラットスコープといったさまざまな「魔術」を紹介します。後半では、ケーススタディとしてRailsを使ったメタプログラミングの実例を紹介します。今回の改訂では、Ruby 2とRails 4に対応し、ほぼすべての内容を刷新。Rubyを使ったメタプログラミングの魔術をマスターし、自由自在にプログラミングをしたい開発者必携の一冊です。

引用:https://www.oreilly.co.jp/books/9784873117430/

6章:コードを書くコードを写経し終えたので内容を振り返ってまとめてみました。

Kernel#eval

evalメソッドの基本

Kernel#evalは渡されたコード文字列を実行してその結果を返す

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

Bindingオブジェクト

  • スコープをオブジェクトにまとめたもの
  • Kernel#bindingとevalを組み合わせれば、後からそのスコープでコードを実行できる
  • Pryのbinding.pryもこれを利用して同スコープの変数を自由に読み書きできる
class MyClass
  def my_method
    @x = 1
    binding
  end
end

b = MyClass.new.my_method
eval('@x', b) # => 1

コード文字列 対 ブロック

コード文字列を扱うevalもブロックを扱うinstance_evalも同じように利用できる。
どちらを使えばよいか?という問への解は「ブロック」である。evalには以下問題がある。

array = %w[a b c]
x = 'd'
eval 'array[1] = x'
array # => ["a", "d", "c"]

array = %w[a b c]
x = 'd'
array.instance_eval do
  self[1] = x
end
array # => ["a", "d", "c"]

evalの問題点:コードインジェクション攻撃

コード文字列を用いるとエディタのシンタックスハイライトや自動補完が使えない問題点がある。
が、最も大きな問題点はコードインジェクション攻撃である。

以下はArrayクラスのメソッドを確認するために標準入力をevalで受け取って実行するコード例。
配列に対する呼び出しの後に、同階層のファイル名を全て返す記述が書けてしまう。
同じ方法でハードディスクの消去やプライベート情報の抜き出しなどが出来てしまう。

def explore_array(method)
  code = "['a', 'b', 'c'].#{method}"
  puts "Evaluating: #{code}"
  eval code
end

loop { p explore_array(gets.chomp) }

# 善意の使い方
# <= find_index('b')
# => Evaluating: ['a', 'b', 'c'].find_index('b')
#    1

# <= map! {|e| e.next }
# => Evaluating: ['a', 'b', 'c'].map! {|e| e.next }
#    ["b", "c", "d"]

# 悪意ある使い方
# <= object_id; Dir.glob('*')
# => Evaluating: ['a', 'b', 'c'].object_id; Dir.glob('*')
#    同階層にある全てのファイル名が分かってしまう

攻撃の対策①:動的ディスパッチ

動的ディスパッチを利用する方法がある。
しかし、これではウェブのインターフェイスからメソッドを渡せない。

def explore_array(method, *arguments)
  ['a', 'b', 'c'].send(method, *arguments)
end

explore_array('find_index', 'b')
# => 1

explore_array('object_id; Dir.glob(\'*\')')
# => undefined method `object_id; Dir.glob('*')' for ["a", "b", "c"]:Array (NoMethodError)

攻撃の対策②:オブジェクトの汚染

Rubyは安全でないオブジェクトに自動的に汚染のマークを付けてくれる。

安全でないオブジェクトとは、webフォーム・コマンドライン・システム変数の読み込み文字列など。
汚染されているかどうかは tainted? メソッドで確認できる。

しかし、いちいち汚染されているか確かめるのは手間。

user_input = 'This text is untainted'
puts user_input
puts user_input.tainted?
# => This text is untainted
#    false

user_input = "User input: #{gets()}"
puts user_input
puts user_input.tainted?

# <= x = 1
# => User input: x = 1
#    true

攻撃の対策③:セーフレベル

Rubyはセーフレベルという概念をデフォルトで提供している。

グローバル変数 $SAFE にセーフレベルを設定すると、危険な操作を制限できる。
セーフレベルは何も制限しない 0 または
汚染された文字列を引数とした以下の操作を制限する 1 が指定できる。

  • Dir, IO, File, FileTest のメソッド呼び出し
  • ファイルテスト演算子の使用、ファイルの更新時刻比較
  • 外部コマンド実行 (Kernel.#system, Kernel.#exec, Kernel.#`, Kernel.#spawn など)
  • Kernel.#eval
  • トップレベルへの Kernel.#load (第二引数を指定してラップすれば実行可能)
  • Kernel.#require
  • Kernel.#trap
$SAFE = 1

array = [10, 20]
element = 30
eval 'p array << element'
# => [10, 20, 30]

text = 'p \'This text is called by eval\''
eval text
# => "This text is called by eval"

user_input = "User input: #{gets()}"
eval user_input
# <= x = 1
# => Insecure operation - eval (SecurityError)

フックメソッド

簡単な例

Rubyでは、クラスが継承されたときやmoduleがミックスインされたときにコードを実行できる。

これはJavaScriptのイベントハンドリングのようなもので、フックメソッドという。

以下はクラスがStringクラスを継承したときにその通知を画面に印字するメソッドの例。
クラスが継承されたときに呼び出される標準搭載のClass#inheritedをオーバーライドした

class String
  def self.inherited(subclass)
    puts "#{self} は #{subclass} に継承された"
  end
end

class MyString < String; end
# => String は MyString に継承された

その他のフックメソッド:Module#included・Module#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#method_added

module M
  def self.method_added(method)
    puts "新しいメソッド:M##{method} が定義された"
  end

  def my_method; end
end
# => 新しいメソッド:M#my_method が定義された

クラスのアトリビュートを独自開発

コードを記述するコードについて学ぶ為にadd_checked()を作成する。

  • 望む機能①:attr_accessor()同様のアトリビュート
  • 望む機能②:妥当性確認機能を持つ
  • 望む機能③:特定のクラスにのみ有効

特定のクラスにのみ有効なアトリビュートとするためにModule#includedのフックメソッドを用い、妥当性確認機能のためにブロックを利用できるようにしている。

また、Module#included内部で更にextendすることで、特定クラスのクラスメソッド(特異クラスのインスタンスメソッド)として定義するイディオムを活用している。
参考:クラス拡張とObject#extend

# テストコード
require 'minitest/autorun'

class Person
  include CheckedAttributes

  attr_checked :age do |value|
    value >= 18
  end
end

class CheckedAttributeTest < Minitest::Test
  def setup
    @bob = Person.new
  end

  def test_accepts_valid_values
    @bob.age = 20
    assert_equal 20, @bob.age
  end

  def test_refutes_invalid_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = 17
    end
  end
end

# 本体コード
module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
      define_method "#{attribute}=" do |value|
        raise 'Invalid attribute' unless validation.call(value)
        instance_variable_set "@#{attribute}", value
      end

      define_method attribute do
        instance_variable_get "@#{attribute}"
      end
    end
  end
end
          
    

スポンサーリンク

  
Scroll Up