Upgrade to Pro — share decks privately, control downloads, hide ads and more …

特殊変数大全

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for dak2 dak2
September 19, 2026

 特殊変数大全

名古屋Ruby会議05 の登壇資料
https://regional.rubykaigi.org/nagoya05/sessions/dak2/

Avatar for dak2

dak2

September 19, 2026

More Decks by dak2

Other Decks in Technology

Transcript

  1. ▪ 特殊亭へんすう Daichi Kamiyama @dak2 ・株式会社タイミーのエンジニア ・お仕事では Rails を書いている ・プライベートではよく

    Rust を書いている ・型とかコンパイラが好き ・Totopia Brewery の Hazy IPA が楽しみすぎる ── 今日は芸名の通り、特殊変数の話しかできません。 二 / 四十七
  2. ▪ 特殊変数とは? で始まるグローバル変数の一種 組み込み変数とも呼ぶ $ 例:$_ $~ $& $1 $/

    $; ── Perl から受け継いだ、少々風変わりな一族。 https://docs.ruby-lang.org/ja/4.0/doc/spec=2fvariables.html#global 三 / 四十七
  3. ▪ 動かしてみる % ruby -v && echo -e "a\nb" |

    ruby -e 'gets; $_; gets; p $_' ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25] "b\n" ── gets で読んだ行が、$_ に収まっている。 四 / 四十七
  4. ▪ 箱の中で動かすと? 箱 = Ruby::Box(Ruby 4.0 の experimental 機能。RUBY_BOX=1 で有効化)

    % ruby -v && echo -e "a\nb" | RUBY_BOX=1 ruby -e 'gets; $_; gets; p $_' ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25] ruby: warning: Ruby::Box is experimental, and the behavior may change in the future! See https://docs.ruby-lang.org/en/4.0/Ruby/Box.html for known issues, etc. "a\n" ── 二度読んで、一度しか更新されなかった。 五 / 四十七
  5. ▪ p $_ の裏側で何が起きるか Ruby VM 命令 C 関数 getter

    p $_ 実行前に、細かい「VM 命令」の列へ変換される ↓ getglobal :$_ $_ を読む部分は、ただの getglobal 命令 ↓ rb_gvar_get(id) 箱を探し、変数に登録された getter を呼ぶ ↓ rb_lastline_get() フレームの svar から lastline を取り出して返す 七 / 四十七
  6. ▪ どんな VM 命令に変換される? % ruby -v && echo -e

    "a\nb" | RUBY_BOX=1 ruby -e 'gets; gets; p $_' --dump=insn ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin24] == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,16)> 0000 putself 0001 opt_send_without_block ( 1)[Li] <calldata!mid:gets, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 pop 0004 putself 0005 opt_send_without_block <calldata!mid:gets, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0007 pop 0008 putself 0009 getglobal :$_ 0011 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> 0013 leave 八 / 四十七
  7. ▪ getglobal 命令の正体 insns.def に書かれた、この命令の C コードのテンプレート。 /* get global

    variable id. */ DEFINE_INSN getglobal (ID gid) () (VALUE val) // attr bool leaf = false; { val = rb_gvar_get(gid); } ── gid は $_を表す識別子。= idLASTLINE https://github.com/ruby/ruby/blob/ruby_4_0/prism_compile.c#L9549 九 / 四十七
  8. ▪ rb_gvar_get の中身 VALUE rb_gvar_get(ID id) { RB_VM_LOCKING() { entry

    = rb_global_entry(id); var = entry->var; if (gvar_use_box_tbl(box, entry)) { use_box_tbl = true; // キャッシュの有無で分岐(省略) } } if (!use_box_tbl) { retval = (*var->getter)(entry->id, var->data); } return retval; } ── use_box_tbl 判定が false になった場合に getter が呼ばれている https://github.com/ruby/ruby/blob/ruby_4_0/variable.c#L1052 十 / 四十七
  9. ▪ おさらいと参りましょう 期待:2 回目の gets で読んだ文字列が、標準出力される 実際:1 回目に読み込んだ文字列が、標準出力されていた $ echo

    -e "a\nb" | RUBY_BOX=1 ruby -e 'gets; $_; gets; p $_' # expected "b\n" # actual (4.0.0〜4.0.3) "a\n" # 1 回目の値が返る 十一 / 四十七
  10. ▪ キャッシュの正体 static inline bool gvar_use_box_tbl(const rb_box_t *box, const struct

    rb_global_entry *entry) { return BOX_USER_P(box) && !entry->var->box_dynamic && (!entry->var->box_ready || entry->var->setter != rb_gvar_readonly_setter); } ── box_dynamic と box_ready の二枚看板が関わっているようだ https://github.com/ruby/ruby/blob/ruby_4_0/variable.c#L1013 十二 / 四十七
  11. ▪ Box 生成時の gvar_tbl static void box_entry_initialize(rb_box_t *box) { //

    ...(省略) box->loading_table = st_init_strtable(); box->ruby_dln_libmap = rb_hash_new_with_size(0); box->gvar_tbl = rb_hash_new_with_size(0); box->classext_cow_classes = st_init_numtable(); // ...(省略) } ── Box を生成するたびに、gvar_tbl に空の Hash インスタンスが作られる https://github.com/ruby/ruby/blob/ruby_4_0/box.c#L158 十五 / 四十七
  12. ▪ グローバル変数の隔離 ユーザーが定義した $foo = "hoge" は box ごとに隔離されてハッシュテーブルに格納さ れる

    だから box の外で $foo を参照しても "hoge" にはならない https://github.com/ruby/ruby/blob/ruby_4_0/variable.c#L1033 十六 / 四十七
  13. ▪ フレームと env メソッド呼び出しやブロック実行のたびに、VM スタックにフレームが積まれ る。 フレームは自分のローカル変数を置く領域 env を持ち、ep でそこを指す。

    VM スタック ↑ 積む ┌─────────────┐ │ frame 3 │ ← ep(実行中) ├─────────────┤ │ frame 2 │ ├─────────────┤ │ frame 1 │ └─────────────┘ frame の中身(env) [ a ][ b ][ c ][ me/cref ][ ... ] ローカル変数 ep[-2] 十九 / 四十七
  14. ▪ ブロックも自分の env を持つ ブロック引数やブロック内で定義した変数は、ブロック自身の env に入る。 外側の変数を読むときは、外側の env へ辿る。

    def foo(a, b) [1].each do |x| [2].each do |y| a + b + x + y end end end # method env: a, b # block env: x # block env: y block env y ← 実行中 ↓ 外側の env へ block env x ↓ method env a b a + b + x + y を評価するとき y → 自分の env ── ローカル変数はフレームごとの env に置かれる 二十 / 四十七 x → 1つ外 a b → 2つ外
  15. ▪ lep(local ep) このフレームを辿ると、メソッドのフレームの ep に着く。 これを lep(local ep)と呼ぶ。 def

    foo(a, b) [1].each do |x| [2].each do |y| a + b + x + y end end end # ← ここの ep が lep block env # block ep ↓ 辿る # block ep block env y ← 実行中の ep x ↓ 辿る method env a b ← 定義元 = lep どのブロックの中にいても、定義元まで辿れば 同じ lep に着く 二十一 / 四十七
  16. ▪ 特殊変数 は lep にだけ置かれる * や $~ は lep[-2]

    に vm_svar として保存される。 ブロック内で読み書きしても、必ず lep まで辿ってから lep[-2] を見る。 $_ def foo(a, b) # lep[-2] に vm_svar [2].each do |y| end end y ← 実行中 ↓ $_ はここには無い [1].each do |x| block env gets # $_ を書く → lep へ p $_ # $_ を読む → lep へ end block env x ↓ ここにも無い method env vm_svar a b vm_svar ← lep = lep[-2] $_ の置き場はここだけ * ここでの「特殊変数」は主に svar($_ と $~ 系)のこと。$/ などは普通のグローバル、$! は rescue フレームのローカル or ec->errinfo で、lep[-2] には入らない。 二十二 / 四十七
  17. ▪ スコープを抜けると lep ごと消滅 メソッドを抜けると lep[-2] の vm_svar も一緒になくなる。 def

    foo(a, b) [1].each do |x| [2].each do |y| gets # $_ → foo の lep[-2] end end end method env a b $_ = gets の行 ← foo の lep ✘ return で lep[-2] ごと消える main env $_ = "before" ← main の lep $_ = "before" # main の lep[-2] foo(1, 2) # foo の lep が積まれ、消える p $_ # => "before" ── 呼び出し元は別の lep を持つ ※ Thread.new / Fiber.new に渡したブロックの lep(ec->root_lep)では、lep[-2] ではなく ec->root_svar を使う(図は簡略化) 二十三 / 四十七
  18. ▪ ローカル変数と特殊変数 の対比 * 置き場所 粒度 ローカル変数 各フレームの の env

    フレームごと 特殊変数 lep の env( メソッドフレームごと ep lep[-2] ) メソッドもブロックも各自持つ ブロックフレームは外側の分を共有 def foo [1].each { x = 1; gets } p $_ # → 見える p x # → NameError end * ここでの「特殊変数」は主に svar($_ と $~ 系)のこと。$/ などは普通のグローバル、$! は rescue フレームのローカル or ec->errinfo で、lep[-2] には入らない。 二十四 / 四十七
  19. ▪ なぜ gvar_tbl に乗せられないか ・ $_ は frame-local なので、フレームが変わるたび に値も変わる必要がある

    ・ box で一意な置き場所を1つキャッシュすると、別 フレームの古い値が返る 二十五 / 四十七
  20. ▪ box_dynamic フラグを立てる $_ の定義時にフラグを立てておく rb_gvar_box_dynamic("$_"); 読み書き時はこのフラグで分岐する(gvar_use_box_tbl) ・ フラグなし →

    box の gvar_tbl に値をキャッシュ。box で一意 ・ フラグあり → キャッシュせず毎回 getter を呼ぶ → lep[-2] を辿る https://github.com/ruby/ruby/blob/ruby_4_0/io.c#L15741 二十六 / 四十七
  21. ▪ 「値がどこにあるか」で四分類 ① フレームごとに別の値 ② 実行コンテキストごとに別の値 $_ $~ $1〜$9 $!

    $@ $? frame-local thread-local($? のみ thread-local) ③ プロセスごとに別の値 ④ 仮想変数 $/ $, $0 $\ $stdout $$ $VERBOSE hooked(C グローバル、VALUE に紐づく) virtual(計算専用、別の場所を見に行くだけ) 二十七 / 四十七
  22. ▪ $! とは の中で、いま捕まえている例外が入る変数。 rescue => e の e は、$!

    と大体同じもの。 rescue begin raise "boom" rescue p $! # => #<RuntimeError: boom> end p $! # => nil ── rescue を抜けると nil に戻る https://docs.ruby-lang.org/ja/latest/class/Kernel.html#V_--21 二十八 / 四十七
  23. ▪ raise は何段も一気に戻る C には例外がない。CRuby は setjmp / longjmp で「一気に戻る例外

    の動作」を作っている。 setjmp で戻り先を覚えておき、longjmp で途中のフレームを全部飛ば してそこへ戻る。 https://github.com/ruby/ruby/blob/ruby_4_0/eval_intern.h#L169 https://github.com/ruby/ruby/blob/ruby_4_0/eval_intern.h#L166-L167 ── longjmp が運べるのは int 1つだけ(何で飛んだかの種別)。例外オブジェクトは運べない。 それでも rescue => e には例外が届く。どこに置いてくる? 二十九 / 四十七
  24. ▪ 例外は ec->errinfo に置く は値を運べないので、飛ぶ前に実行コンテキスト(ec) に例外を 置き、戻った先で取り出す。 置き場所は rb_execution_context_t 構造体のメンバ

    errinfo。 longjmp raise longjmp 探索 rescue ec->errinfo = 例外 飛ぶ前に、ec 構造体のメンバ errinfo に例外オブジェクトを代入する(eval.c の setup_exception) EC_JUMP_TAG(ec, TAG_RAISE) ↓ c → b → a のフレームを飛ばして、vm_exec の受け口まで一気に戻る table から rescue を探す ↓ 受け口が ec->errinfo を取り出し、どの rescue で受けるか決める(vm.c) ↓ rescue フレームを積む → ec->errinfo = nil 例外は rescue 節用フレームのローカルスロットにコピーされ、errinfo は空に戻る https://github.com/ruby/ruby/blob/ruby_4_0/eval.c#L565 三十 / 四十七
  25. ▪ $! の置き場所 rescue に入った後の例外は、rescue 節用フレームのローカル変数 スロットにある。 % ruby -v

    && ruby -e 'begin; raise "boom"; rescue; p $!; end' --dump=insns ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25] == disasm: #<ISeq:rescue in <main>@-e:1> local table (size: 1, ...) [ 1] "$!"@0 ← rescue 節 ISeq のローカル変数テーブルに "$!" 0000 getlocal_WC_0 0002 putobject "$!"@0 ← 例外をローカルスロットから読む StandardError 0004 checkmatch 0006 branchunless 3 14 ← StandardError === $! の判定 0008 putself 0009 getglobal :$! ← ユーザが書いた p $! は getglobal 0011 opt_send_without_block <calldata!mid:p, ...> 0013 leave 0014 getlocal_WC_0 0016 throw "$!"@0 0 ← 合わなければスロットの例外を再送出 https://github.com/ruby/ruby/blob/ruby_4_0/eval.c#L2010-L2029 三十一 / 四十七
  26. ▪ $! は Thread をまたがない begin raise "outer" rescue Thread.new

    { p [:thread, $!] }.join # [:thread, nil] Fiber.new { p [:fiber, $!] }.resume # [:fiber, nil] p [:main, $!] # [:main, #<RuntimeError: outer>] end 別の Thread / Fiber は別の実行コンテキスト(ec) を持ち、 フレームの列も別。 ── 値は ec のフレームから毎回計算される 三十二 / 四十七
  27. ▪ $/ とは gets や each_line が「行」を区切るときの区切り文字。初期値は "\n"。 p $/

    # => "\n" "a,b,c".lines # => ["a,b,c"] $/ = "," "a,b,c".lines # => ["a,", "b,", "c"] https://docs.ruby-lang.org/ja/latest/class/Kernel.html#V_--2D0 三十三 / 四十七
  28. ▪ $/ の置き場所 実体は io.c の C グローバル変数 rb_rs。プロセスに 1

    つ。 rb_define_hooked_variable("$/", &rb_rs, 0, deprecated_rs_setter); ・ 第 3 引数の getter は 0(指定なし)→ デフォルトの getter が C グローバ ル変数 rb_rs を直接読む ※ thread-local / frame-local な特殊変数は読む場所によって値が異なるので、getter 側にロジックを書いて処理されている https://github.com/ruby/ruby/blob/ruby_4_0/io.c#L15733 三十四 / 四十七
  29. ▪ $/ は Thread で共有 p $/ # => "\n"

    Thread.new { $/ = "," }.join p $/ # => "," p "a,b,c".lines # => ["a,", "b,", "c"] rb_rs 所。 別スレッドでの代入が見える は C グローバル変数なので、どの Thread / Fiber から見ても同じ 1 か 三十五 / 四十七
  30. ▪ $stdout とは puts / print / p の出力先。puts "x"

    は $stdout.puts "x" と同じ。 class Nagoya attr_reader :s def initialize; @s = +""; end def write(*a) = a.each { @s << _1.to_s } def puts(*a) = a.each { write(_1, "\n") } def tty? = false def flush = self end nagoya = $stdout = Nagoya.new puts "hello" # 画面には出ない $stdout = STDOUT # 戻す p nagoya.s # => "hello\n" 三十六 / 四十七
  31. ▪ $stdout の置き場所 rb_define_virtual_variable("$stdout", stdout_getter, stdout_setter); static VALUE stdout_getter(ID id,

    VALUE *ptr) { return rb_ractor_stdout(); } VALUE rb_ractor_stdout(void) { if (rb_ractor_main_p()) { return rb_stdout; // main Ractor: C グローバル変数 } else { rb_ractor_t *cr = GET_RACTOR(); if (cr->r_stdout == 0) cr->r_stdout = rb_io_prep_stdout(); return cr->r_stdout; // それ以外: rb_ractor_t のフィールド } } ── virtual variable=値を置く場所を持たず、読み書きが毎回関数呼び出しになる 値の場所は「読む側がどの Ractor か」で変わる https://github.com/ruby/ruby/blob/master/ractor.c#L1390 三十七 / 四十七
  32. ▪ $stdout は Ractor ごとに別 buf = $stdout = Nagoya.new

    Thread.new { puts "from thread" }.join $stdout = STDOUT p buf.s # => "from thread\n" p STDOUT.object_id p STDOUT.fileno # => 16 # => 1 p Ractor.new { $stdout.object_id }.value p Ractor.new { $stdout.fileno }.value # => 24 # => 1 同じ出力先 別の IO オブジェクト fd は同じ ── Ractor ごとに、同じ fd 1 を指す別の IO オブジェクトが作られている https://github.com/ruby/ruby/blob/ruby_4_0/io.c#L9392-L9395 三十八 / 四十七
  33. ▪ Ractor では $stdout が分かれ ているのはなぜ? 答え:共有不可能だから。 p $stdout.object_id #

    => 16 p Ractor.new { $stdout.object_id }.value # => 24 別の IO オブジェクト p Ractor.new { $/ }.value # => "\n" こちらは同じものが見える ── $stdout の IO は mutable。Ractor 間では共有できない。 三十九 / 四十七
  34. ▪ Ractor とは Ruby 3.0 で入った並列実行の単位。Thread と違い GVL を共有しない s

    = "hello" p Ractor.new(s) { |x| x.object_id }.value == s.object_id # => false コピーされる f = "hello".freeze p Ractor.new(f) { |x| x.object_id }.value == f.object_id # => true そのまま渡る ── 同時に走っても壊れないように、共有可能なオブジェクト以外は Ractor 間で共有 しない 四十 / 四十七
  35. ▪ 共有できるもの・できないもの ・ 共有できる:凍結された String、Integer、Symbol、shareable な値を指す 定数 など(変わらないもの) ・ 共有できない:可変な

    Array、Hash、IO など(変わりうるもの) $x = [1, 2] Ractor.new { $x }.value # => can not access global variable $x from non-main Ractor # (Ractor::IsolationError) # ※ Ractor 内で発生した例外は Ractor::RemoteError に包まれて届く ── 通常のグローバル変数は、非 main Ractor から触ると Ractor::IsolationError 四十一 / 四十七
  36. ▪ $/ と $stdout の分かれ道 ・ $/ の中身は Frozen String

    → 共有できる ・ $stdout の中身はバッファを持つ mutable な IO → 共有できない // io.c Init_IO rb_define_hooked_variable("$/", &rb_rs, 0, deprecated_rs_setter); rb_gvar_ractor_local("$/"); // not local but ractor safe rb_define_virtual_variable("$stdout", stdout_getter, stdout_setter); rb_gvar_ractor_local("$stdout"); 四十二 / 四十七
  37. ▪ 特殊変数の取り回し方 対応の宣言 Ractor Ruby::Box rb_gvar_ractor_local rb_gvar_box_dynamic 許可リスト 除外リスト(常に getter

    / setter へ) rb_gvar_box_ready 対応済み宣言(readonly なら getter へ) 未対応の変数 拒否 全部コピー 分け方 getter / setter を通す。その中で置き 場を分ける getter / setter を通さず gvar_tbl で代 替 初回 get で getter → clone → gvar_tbl Ractor::IsolationError は rb_ractor_t、$! は ec、$_ は svar。 は共有のまま許可 $stdout $/ 初回 get だけ getter を呼ぶ。2 回目以降の get と set は呼ばない 四十三 / 四十七
  38. ▪ 「特殊変数」は多様性がある 同じ $ 付きでも、読み書きする格納場所も、紐づく単位もバラバラ。 格納場所 $_ $! $/ $stdout

    lep[-2] の rescue フレームのローカルスロット C グローバル / vm_svar rb_rs rb_stdout rb_ractor_t::r_stdout 紐づく単位 フレーム ec(Fiber) プロセス Ractor ※ ── 「特殊変数」と言っても一枚岩じゃない ※ fd 1 はプロセスに 1 つで全 Ractor が共有。IO オブジェクトの方が Ractor ごとに生成される(rb_io_prep_stdout は fd 1 を指す IO を作るだけ で、fd は dup しない)。 四十四 / 四十七
  39. ▪ 特殊変数を追うと、Ruby の処理系が見 えてくる 今日話せなかったこと、まだまだあります。 ・ VM ec->errinfo が運ぶのは例外だけじゃない。break /

    throw の情報 THROW_DATA も ・ GC rb_rs が指す String が回収されないのは、gvar エントリの marker が *data をマ ークしているから ・ JIT $1 は ZJIT で GetSpecialNumber という専用 HIR になるが、specialize の余地 はまだ残っていそう ── $ 一文字から始まる大冒険 四十五 / 四十七
  40. ▪ おことわり 今日お話しした内容は Ruby 4.0 系(4.0.0〜4.0.7) 時点 の実装に基づいています。 $_ の修正は

    4.0.4 以降、$! / $@ の修正は 4.0.6 以降に入 ったもので、同じ 4.0 系内でも挙動が変わっています。 Ractor も Ruby::Box も改善中。挙動は、今後変わるかも しれません。 四十六 / 四十七
  41. % ruby -v && echo -e "a\nb" | RUBY_BOX=1 ruby

    -e 'gets; $_; gets; p $_' ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin25] ruby: warning: Ruby::Box is experimental, and the behavior may change in the future! See https://docs.ruby-lang.org/en/4.0/Ruby/Box.html for known issues, etc. "b\n" お後がよろしいようで https://github.com/ruby/ruby/pull/16303 四十七 / 四十七