Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

We provide a web service as ... Analytics of Mobile Apps, Web Apps. Marketing Automation. We're hiring!!

Slide 4

Slide 4 text

https://youtu.be/1mauqP9zWbM?t=31

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Part1 守りの黒魔術

Slide 9

Slide 9 text

守りの黒魔術 その1 組込みクラスを壊さない 特に require しただけで挙動を変えない 上書きさせたい場合は、必ずユーザーに指定させる 組込みのクラス・メソッドは全てのRuby ユーザーが期待している挙動が ある。( 当たり前の話) 暗黙的に弄って挙動を変えたら、何が起こるのか分からなくなる。

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

守りの黒魔術 その4 TracePoint の利⽤は明⽰的に そして、絶対にensure でdisable すること。 途中で例外が発⽣するとtrace が有効のままになる。 trace が有効のままになるとマジで何が起きるか分からない。 フックが暴⾛してstack level too deep になるのはまだ良い⽅。

Slide 13

Slide 13 text

Part2 黒魔術に使えるAPI の探し⽅

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

使えそうな機能の例 評価コンテキスト操作 eval 系 トリガー/ フック メソッドフック, TracePoint, included, inherited, method_missing, trace_var, trap, finalizer ⼤域脱出 throw/catch, Fiber

Slide 18

Slide 18 text

使えそうな機能の例 ( 続き) オブジェクト参照 _variable_get 系, const_get, ObjectSpace 変数/ 定数操作 _variable_set 系, const_set メタデータ取得 Method やProc から取れる情報 メソッドの動的定義 define_method, module_eval

Slide 19

Slide 19 text

メタプログラミングパターン メソッド定義 DSL 動的解析 静的解析 ⾃動的/ 暗黙的処理の追加 ⾔語拡張 ↓ に向かう程魔術度が増す

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

proc の中で暗黙の内に it でパラメータを参照できるとは、 Ruby の動作に置き換えるとどういうことかを考えてみる。 評価コンテキスト内で it というローカル変数に値が⼊っている proc のself に it というメソッドが定義されていて、パラメータを取 得できる proc の外側で it が定義されている。 ローカル変数 or 引数 これらのどれかが実現できれば良さそう。

Slide 22

Slide 22 text

ローカル変数追加⽅式について検討 local_variable_set が使えそう local_variable_set は変数書き換えは簡単だが、新規に追加するのは難 しい binding が毎回新しく⽣成されるため binding を固定してeval しなければならない そもそもブロック呼び出しに実際に使われている値をどうやって事前 に取得する? なんか無理っぽい

Slide 23

Slide 23 text

メソッド追加⽅式について検討 評価コンテキストにおけるself は取得できる Proc#binding やTracePoint で可能 単純にメソッドを定義するとあるクラスのインスタンス全てが影響す る 特異メソッドとして定義すれば可能かも しかしスコープを抜けた後も参照できてしまう Refinements は使えないか ブロックの定義が別の場所なのでeval が必要 ローカル変数⽅式と同様の問題がある

Slide 24

Slide 24 text

外側でit を定義する⽅法について検討 新しいproc でラップして追加できる ブロックに渡される引数を事前に知る必要が無い やはりeval する必要がある

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

特定のメソッドを対象に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]

Slide 28

Slide 28 text

出来た! 後はgem にするだけ

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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