†Ruby黒魔術経典†

 †Ruby黒魔術経典†

名古屋Ruby会議04 発表資料

A5e5ee2fb9e4ce3c728ed9e3ef6e916f?s=128

Tomohiro Hashidate

June 08, 2019
Tweet

Transcript

  1. †Ruby 黒魔術経典† @joker1007 (Repro inc. CTO) 名古屋Ruby 会議04

  2. self.inspect @joker1007 Repro inc. CTO Data Engineering, Architect Ruby で悪事を働く⼈と⾒做されている

  3. We provide a web service as ... Analytics of Mobile

    Apps, Web Apps. Marketing Automation. We're hiring!!
  4. https://youtu.be/1mauqP9zWbM?t=31

  5. ⼤いなる⼒には ⼤いなる責任が 伴う

  6. メタプログラミングや魔術的な挙動をする コードは強⼒だが 侵してはならないこともある

  7. 今⽇の議題 以下の内容について話す。 黒魔術において守るべきルール 実際に魔術を編み出すために使えるパーツ 考え⽅の具体例

  8. Part1 守りの黒魔術

  9. 守りの黒魔術 その1 組込みクラスを壊さない 特に require しただけで挙動を変えない 上書きさせたい場合は、必ずユーザーに指定させる 組込みのクラス・メソッドは全てのRuby ユーザーが期待している挙動が ある。(

    当たり前の話) 暗黙的に弄って挙動を変えたら、何が起こるのか分からなくなる。
  10. 守りの黒魔術 その2 スタックが追える様にすること eval を使う場合に定義場所のロケーションを適切に設定する。 でないと例外が発⽣した時にどこで定義されたコードなのか分からない。 実はRuby の例外のバックトレースは⾃分で上書きできるので、ノイズに なりそうな情報を隠すこともできる。

  11. 守りの黒魔術 その3 パフォーマンスを意識すること eval やbinding は負荷が⾼い。 呼び出し回数を少なくするために以下の様な⽅法が使える。 クラスレベルでソースコードをキャッシュする クラス定義時や初回実⾏時のみ動作する様にする メソッド定義にだけ利⽤する

    初回実⾏時にメソッドを上書きする
  12. 守りの黒魔術 その4 TracePoint の利⽤は明⽰的に そして、絶対にensure でdisable すること。 途中で例外が発⽣するとtrace が有効のままになる。 trace

    が有効のままになるとマジで何が起きるか分からない。 フックが暴⾛してstack level too deep になるのはまだ良い⽅。
  13. Part2 黒魔術に使えるAPI の探し⽅

  14. 基本はリファレンスをひたすら読むこと それだけだと雑過ぎるので、もうちょい解説する。

  15. るりまのここを読むべし BasicObject Object Module/Class Method Proc Kernel (ObjectSpace)

  16. ⼀回読んでも忘れるから、 とりあえずこの辺りを読み返す癖を付けておく。 ちなみに、 RubyVM::AbstractSyntaxTree はまだるりまが無いです。 ( プルリクチャンス)

  17. 使えそうな機能の例 評価コンテキスト操作 eval 系 トリガー/ フック メソッドフック, TracePoint, included, inherited,

    method_missing, trace_var, trap, finalizer ⼤域脱出 throw/catch, Fiber
  18. 使えそうな機能の例 ( 続き) オブジェクト参照 _variable_get 系, const_get, ObjectSpace 変数/ 定数操作

    _variable_set 系, const_set メタデータ取得 Method やProc から取れる情報 メソッドの動的定義 define_method, module_eval
  19. メタプログラミングパターン メソッド定義 DSL 動的解析 静的解析 ⾃動的/ 暗黙的処理の追加 ⾔語拡張 ↓ に向かう程魔術度が増す

  20. Part3 考え⽅の具体例 暗黙のブロック引数 it を作ってみよう see. https://bugs.ruby-lang.org/issues/15897

  21. proc の中で暗黙の内に it でパラメータを参照できるとは、 Ruby の動作に置き換えるとどういうことかを考えてみる。 評価コンテキスト内で it というローカル変数に値が⼊っている proc

    のself に it というメソッドが定義されていて、パラメータを取 得できる proc の外側で it が定義されている。 ローカル変数 or 引数 これらのどれかが実現できれば良さそう。
  22. ローカル変数追加⽅式について検討 local_variable_set が使えそう local_variable_set は変数書き換えは簡単だが、新規に追加するのは難 しい binding が毎回新しく⽣成されるため binding を固定してeval

    しなければならない そもそもブロック呼び出しに実際に使われている値をどうやって事前 に取得する? なんか無理っぽい
  23. メソッド追加⽅式について検討 評価コンテキストにおけるself は取得できる Proc#binding やTracePoint で可能 単純にメソッドを定義するとあるクラスのインスタンス全てが影響す る 特異メソッドとして定義すれば可能かも しかしスコープを抜けた後も参照できてしまう

    Refinements は使えないか ブロックの定義が別の場所なのでeval が必要 ローカル変数⽅式と同様の問題がある
  24. 外側でit を定義する⽅法について検討 新しいproc でラップして追加できる ブロックに渡される引数を事前に知る必要が無い やはりeval する必要がある

  25. 結論: 恐らくeval が必須 そしてproc でwrap ⽅式が現実的 eval するためにはソースコード断⽚が必要

  26. ブロックのソースコードを取る⽅法 RubyVM::AST.of or parser gem で位置を特定し読む ( またお前か)

  27. 特定のメソッドを対象にPoC を書く module Ext def map(*args, &block) source = File.readlines(block.source_location[0])

    proc_binding = block.binding ast = RubyVM::AbstractSyntaxTree.of(block) args_tbl = ast.children[0] block_node = ast.children[2] if args_tbl.empty? extracted = extract_source( source, block_node.first_lineno, block_node.first_column, block_node.last_lineno, block_node.last_column) new_block = proc_binding.eval("proc { |it| #{extracted} }") super(*args, &new_block) else super(*args, &block) end end end Array.prepend(Ext) n = 3 [1, 2, 3].map { p it + n } # => [4, 5, 6]
  28. 出来た! 後はgem にするだけ

  29. こんな感じで、⾃分の場合はゴールから逆算して考える。 やりたい事が出来るとはRuby においてオブジェクトの状態や変数のスコ ープ、メソッドの定義がどうなっていればいいかを想像し、そこに⾄る⽅ 法を逆向きに辿って実現可能な⽅法を考える。

  30. ちなみに、実はこれ RubyKaigi2019 で話したものと同じ パターンを使っている

  31. 最後に 黒魔術を使うためにはRuby の挙動や各オブジェクトが何なのかというこ とを詳しく知る必要がある。 魔術的な挙動を起こす⽅法を知ることは、安全なコードの書き⽅を知るこ とにも繋がる。 いざという時の選択肢も増える。 Ruby より深く楽しみ、より良いコードに繋げよう