RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

RubyKaigi 2025 参加レポート - Ruby で pidfd を利用する

はじめに

こんにちは。SRE チームの吉原 哲です。

RubyKaigi 2025 に参加してきました。中でも特別興味のあるセッションがありました。Maciej Mensfeld (@maciejmensfeld) 氏による Bringing Linux pidfd to Ruby です。彼は Karafka の作者で、以前社内の Karafka をアップグレードしたときお世話になった方です。そのときの話はこのブログエントリ (Kafka と Karafka を無事故・無停止でアップグレードした話) をご参照ください。今回 RubyKaigi へ参加するにあたって是非ともお話を聞きたいと思っていました。

本エントリでは pidfd とは何なのか事前調査した内容と、発表の内容を織り交ぜながら解説します。

pidfd とは

Linux 5.3 で導入された pidfd (PID File Descriptor) は、プロセスを File Descriptor として扱う仕組みです。一度取得した pidfd はそのプロセスが死ぬまで決して他プロセスを指さないため、PID ベースでプロセス管理をする際の様々な問題を解決することができます。

PID でプロセス管理をする問題

以下にいくつか問題点をあげてみます。

シグナルレース

子プロセスが終了すると SIGCHILD が送られますが、誰でも SIGCHILD のハンドラを設定できます。

waitpid を呼んだ最初のハンドラが勝利し、終了コードはそのとき一回だけしか取得できません。

signal races

誤 kill 問題

Unix 系 OS では長年、プロセスを PID (ただの整数) で識別してきました。しかし PID は有限なので、プロセスが終了するといずれ再利用されます。この再利用とkill pid を投げるまでの僅かな遅延が重なると、別プロセスを誤って kill してしまう問題が存在します。

GDB のビルドボットテストにおいては、断続的に謎の FAIL が起きていました。原因は、バックグラウンドで送った kill がたまたま再利用された PID に命中していたことでした。著者は sporadically kills the wrong process と記しており、高頻度で fork/exit を繰り返す環境では問題の顕在化が確認されました。

こうした事例は頻繁に起きるわけではありません。再利用される PID のタイミングが偶然一致したときだけ発生するため、再現するには大量の短命プロセスや CI の並列テストなどの限定的な環境が必要です。

セキュリティ

セキュリティに関する問題も存在します。例えば、CVE-2019-6133 は PolicyKit(polkit)0.115 に存在したローカル権限昇格の脆弱性です。

この脆弱性は、polkit が「プロセス開始時刻」を使って一時的な認可キャッシュを識別する仕組みに起因します。fork() はアトミックではないため、以下の手順で攻撃が可能です。

  1. 親プロセスが認可を得た直後に終了
  2. 同じ PID が即座に再利用される
  3. 攻撃側プロセスがその PID で起動

この PID 再利用レースにより、攻撃側が親プロセスの認可を「横取り」できてしまいました。

管理コスト

PID ベースのポーリングでは常に子プロセスを監視しなければならない手間に加えて、CPU も無駄にしてしまいます。モニタリングのインターバルにより、プロセスが死んだこと検知するのに遅延があります。

PID-based Polling

プロセスツリーの制限

プロセスツリーを追うビルトインの機能は提供されていません。孤児プロセスになった場合は init (PID 1) に再配置されてしまいます。また子プロセスでなければ直接監視する方法はありません。

Process Hierarchy Limitations

pidfd の利点

プロセス識別子の一意性が保証され、誤って他のプロセスにシグナルを送信するリスクを完全に排除することができます。

また File Descriptor として扱えるため、select/poll/epoll で polling が可能です。

Pidfd API Capabilities

pidfd の制限

Linux Kernel 5.3 以降でのみ利用可能です。macOS や Windows では利用できません。

Ruby では言語としてサポートされていないので、FFI を利用して syscall をマップする必要があります。

PID と同じく PID namespace の制限を受けます。

PID で可能だったプロセスグループ単位での操作はできません。

pidfd limitations

Ruby における pidfd

Ruby においては io-event がこれまでのところ pidfd を使っている唯一のライブラリです。PumaUnicornSidekiqPitchfork などはこれまで通りの fork で管理しています。

今回の発表で紹介された Karafka Swarm における FFI を利用した pidfd 実装はわずか 150 行程度の Ruby コードで実現されています。このモジュールでは、#supported? によりサポートされている platform かどうかで判別されて pidfd を利用するか出し分けを行っています。

Karafka Swarm ではプロセスの生死監視はもちろん、pidfd によるプロセスへの確実なシグナル送信が行えます。またゾンビプロセスのクリーンアップも行い、スレッドセーフで動作します。

これに加えて興味深いのが、子プロセスが親の pidfd を持つことにより双方向でプロセスの状態を監視することができるようになっています。子プロセスが親のプロセスの状態を見ることにより、自身が孤児プロセス (Orphan Process) になっているか判別できます。Karafka のワーカープロセスでは自身が孤児プロセスになった場合は、自動で自身を終了させるようになっています。

このモジュールの詳細の実装は Maciej Mensfeld 氏のプレゼンテーション資料にて解説されています。ご興味がある方はご参照ください。

まとめ

これまでの PID によるプロセス管理と違い、pidfd には魅力的な機能を持っていることがわかりました。特に大量の短命なプロセスを扱っている環境の場合、誤った対象にシグナルを送る事故を完全に防ぐことができます。この他にも

  • アプリケーションを様々なプロセスが動いている環境で動かす必要がある
  • 直接の子プロセス以外を管理したい
  • 誤ったプロセスに対して PID が再利用されたときにセキュリティ上の問題が起きる場合

などといったケースでは検討するに値するでしょう。

また常駐サービスのスーパーバイザにおいても、ワーカープロセスを安全に管理できるので pidfd の有効性はあると考えられます。

とはいえ多くのケースではオーバーエンジニアリングになることは否めません。実際にプレゼンテーション中でも

In most cases, pidfd is overkill  — even in Karafka

と言っており、日常的なスクリプトや数プロセス程度を管理するのであれば、レースに遭遇する確率は極めて小さく、従来の PID での管理で十分だと思われます。

参考

We are hiring!

ラクスルでは成長するビジネスを支える SRE を募集しています。 https://hrmos.co/pages/raksul/jobs/rksl866q_2

RubyKaigi 2025 参加レポート

RubyKaigi 2025 参加レポート

RubyKaigi 2025

こんにちは!Enterpriseチームの土田です。 今回、念願の RubyKaigi 2025に参加してきましたので、その様子をレポートします。 今回は発表セッションの技術的な内容というよりも、「初参加エンジニアの目線から、イベントの楽しみ方や実用Tips」にフォーカスしてお届けします。次回以降、参加を検討されている方の参考になれば嬉しいです。

私と RubyKaigi

ラクスルにサーバーサイドエンジニアとして入社して、もうすぐ6年。現在はEnterpriseチームでエンジニアリングマネージャーを務めています。一番好きな言語が Rubyということもあり、Rubyが好きな自分にとって、Ruby(on Rails)をメインに使ってているという話を聞いたのが、ラクスルに入社する決め手にもなりました。

そんな私、RubyKaigi は今回が初参加。実は5年前に参加予定だったもののコロナ禍で中止。その後も子育てなどで機会を逃していたのですが、ようやく念願の参加が叶いました。家庭の事情で長期滞在が難しく3日間のみの参加となりました。

余談ですが、元イベント制作会社の勤務&元バックパッカー経歴があり、地方に出かけてイベント事に参加となると色々と心踊るものがあります。今回のRubyKaigiが松山で開催されたことも、個人的には大きな楽しみの一つでした。エンジニアにはあまり見かけない経歴かもしれませんが、そうしたバックグラウンドを持つメンバーも自然に馴染んでいるのがラクスルの面白いところです。

行き先は愛媛・松山!スケジュールと移動

初日のスケジュール

  • 5:30 自宅出発
  • 6:45 羽田空港到着
  • 7:25 羽田空港発(飛行機)
  • 8:55 松山空港着(飛行機)
  • 9:10 シャトルバス発
  • 9:45 会場前到着
  • 10:00 RubyKaigi 開始

最終日のスケジュール

  • 17:30 RubyKaigi 終了
  • 17:40 タクシー乗車 → ホテルにより荷物をピックアップ
  • 18:30 松山空港着 → 夕食
  • 19:45 松山空港出発(飛行機)
  • 21:15 羽田空港到着(飛行機)
  • 21:40 羽田空港出発(在来線)
  • 22:50 帰宅

移動の所感

スケジュールは慎重に。空港移動には余裕をもって!

国内線は国際線ほど早く到着する必要はないとはいえ、余裕がない移動はやはり気持ちが焦ります。羽田空港ように広い空港は、空港内の移動にもそれなりに時間がかかります。一方地方空港はコンパクトでスムーズな移動がしやすく、時間的余裕が生まれやすいと思います。

今回のフライトは、事前に「本当にこのスケジュールで大丈夫か?」と何度もシミュレーションしたうえで、ややタイトながらも無事に乗り切ることができました。ただ、当日はどうしてもバタバタしてしまったため、あまりおすすめできるスケジュールではなかったかもしれません。

地方開催のRubyKaigiは“前日入り・後日帰り”が正解

Kaigiそのものには問題なく参加できたのですが、せっかく地方開催の RubyKaigi なので、もし日程に余裕があるなら、前日入り&後日帰りのスケジュールを強くオススメします。前日の夜から多くの企業が主催するに DrinkUp イベントがあり貴重な交流の機会が広がります。また、Kaigi 終了後にも各種イベントが続くこともあり、滞在を延ばす価値は十分にあります。観光を楽しむのも含めて、地方開催ならではの体験をぜひ満喫いただくのも良いと思います。

パッキング術 - 2泊3日の持ち物

私はなるべくバックパックひとつで旅を完結させたい派です。スーツケースを引くのも、機内預け荷物の受け取りも、正直ちょっと面倒。今回も、バックパックひとつで2泊3日の旅程を乗り切りました。

もともと荷物をコンパクトにまとめるのは得意なほうですが、それができたのは旅程が2泊3日だったからというのも大きいです。着替えや生活用品も最小限に絞り、工夫すればなんとかなります。

油断大敵。帰りの荷物が増える罠

とはいえ、帰り道にはちょっとした落とし穴が…。カンファレンスのノベルティやお土産など、想定外に荷物が増えるんですよね。今回は持ち帰りスペースの確保が不十分で、リュックがパンパンになってしまいました。

理想は、前後に1泊ずつ余裕をもたせたスケジュール。その分、荷物も少し増える想定で準備した方がよさそうです。特に、カンファレンス中に会社ロゴ入りのTシャツを着る場合など、洗濯の可否も視野に入れておくと安心です。現地で洗濯できる環境があれば、荷物も減らせて一石二鳥ですね。

今回はなんとか収めたものの、次回からは帰りの荷物や洗濯の可能性も見越して、少し余裕を持たせたパッキングを心がけたいと思いました。快適に旅を楽しむためにも、「行きだけの荷物」で満足しないのがポイントです。

RubyKaigi遠征の持ち物と事前準備メモ

必要なかったもの、持っていかずに正解だったもの

  • プライベートのPC
    • 宿では基本的に寝るだけ。夜はDrinkUpイベントに参加することも多く、結局開く時間がありませんでした。
  • アメニティ類
    • ホテルには基本的なアイテム(歯ブラシ・タオルなど)が一通り揃っているので、わざわざ持参する必要はありませんでした。

あって良かったもの、持っていけば良かったもの

  • 会社のTシャツなど
    • さりげなく会社をアピールできるTシャツは◎。
  • 名刺
    • エンジニアだと意外と忘れがちなので注意が必要です!
  • 充電器やモバイルバッテリー類
    • スマホはもちろん、PCの充電もできるモバイルバッテリーがあると安心。特に会場内ではバッテリー切れが命取りです。

宿選びのポイント

  • 会場・繁華街の両方にアクセスしやすい場所が便利
    • セッション後の交流会や観光も考えると、バランスの良い立地がおすすめ。
    • 過去同じ会場や近隣で行われた他のイベント情報なども参考にするのも良いと思います
  • 個人的には朝食付きのプランがおすすめです
    • 朝はサッと食べて会場へGO。逆に夕食はイベントで提供されたり外食が多くなるので、付けなくてもOK。
  • 洗濯機の有無も重要
    • 会社Tシャツを連日着回す場合は、洗濯できるかどうかも宿選びのポイントになります。

事前準備のチェックリスト

  • DrinkUp などのサイドイベント情報をチェック!
    • 事前に申し込みが必要なものもあるため、早めに把握&予約がおすすめ。意外な出会いや情報交換のチャンスも多いです。
  • 開催地の気温や天候をチェック!
    • 気温や天気によって服装・持ち物が変わってきます。事前のリサーチが快適さを左右します。

カンファレンス以外の楽しみ方

RubyKaigi は、セッションだけが魅力ではありません!様々な楽しみ方があります。

Official Party(at 初日の夜) では、おいしいお酒においしい料理を楽しめました!

初日の夜には公式パーティーが開催され、Rubyコミュニティの方々と交流する絶好の機会となりました。また、公式イベント以外にも参加協賛各社が主催する DrinkUp などの懇親会も多数ありましたす。これらのイベントでは、企業の取り組みを知れるだけでなく、参加者との貴重な交流の場にもなるので、ぜひ参加してみることをおすすめします。イベント情報は RubyKaigi サイトの「Events」ページでチェック&申し込みができますが、人気のイベントは初日当日にはほぼ満席になっていたので早めの確認が必要です。

さらに、前日入りした人向けに Day 0 の DrinkUp や、最終日翌日にはゴルフイベントなども開催されていて、カンファレンス期間外にも楽しめるイベントが充実していました。

ランチタイムには数量限定のお弁当や無料のキッチンカーも用意されていましたが、せっかくの地方開催ということもあり、時間に余裕があれば地元グルメを堪能するのもおすすめです。

各社のブースを巡るのも楽しみの一つでした。「この会社のプロダクトもRubyを使っているのか」と驚かされることも多く、サービス内容だけでなく技術スタックについて深い話ができることもありました。また、各社は工夫を凝らした面白いノベルティグッズを配布していて、それを集めるのも楽しいポイントの一つですね。

参加してみての感想

コミッターのみなさんによる公開ディスカッションのセッション

RubyKaigi に参加して最も印象的だったのは、「Rubyという言語が好きで集まっている」という一体感です。特にRubyコミッターによる言語自体についての話が多く、3日目に行われたコミッター同士のディスカッションは非常に興味深いものでした。Rubyという言語がどのように発展していくのか、熱い議論が交わされる様子を目の当たりにし、みんなで作り上げてきた言語であることを実感できました。

今年のトレンドとしては、型の話とパフォーマンス関連の話題が多かったように感じます。特に型については自分のチームも導入を検討しているので、関連セッションには積極的に参加しました。また、AIを活用することが開発の前提として組み込まれ始めており、大きな変化を感じました。

カンファレンス全体にはお祭りのような雰囲気があり、各社のブースを回ると「ここもRubyを使っているの?」と驚かされることも多々ありました。そんな中、ラクスルでもRubyを積極的に活用していることをもっと多くの方に知ってもらいたいと、あらためて実感しました。

個人的に嬉しかったのが、前職の仲間やラクスルを卒業された方々との再会です。いろいろな立場でRubyに関わっている人たちと同じ空間で再び会話できた時間はプチ同窓会のようでとても良かったです。

次回参加する方へのアドバイス

もし時間が許されるなら、前日に現地入りしカンファレンス終了の翌日に帰るスケジュールを組み、セッションだけでなくその土地での体験や人とのつながりも含め、めいっぱい楽しむことをおすすめします。

また、せっかくの機会なので、積極的にコミュニティに参加してみてください。知らない人との会話が苦手でも、Rubyという共通の話題があれば会話も弾みますし、開発スタイルやプロダクトの話、言える範囲で開発チームの話をしてみるのも良いと思います。現に私は DrinkUp でそういう話ができ、非常に有意義な時間を過ごすことができました。

おわりに

初めての RubyKaigi 参加は、想像以上に充実した時間となりました。Rubyコミュニティの温かさを肌で感じ、多くの刺激をもらうことができました。次回は(許されるなら)もう少し余裕を持ったスケジュールで参加し、より多くの交流を楽しみ、その土地ならではの魅力もしっかり味わいたいと思います。

みなさんも機会があれば、ぜひ RubyKaigi に参加してみてください! 次回、函館でお会いできるのを楽しみにしています。

【RubyKaigi2025予習ブログ】Rubyのパーサーを基礎から学ぶ

こんにちは!ラクスル事業本部でサーバーサイドエンジニアをしている久冨(@tomi_t0mmy)です。

今年もRubyKaigiがやってきますね!あの熱気をまた感じられるのかと思うととてもワクワクします。

私は昨年、新卒1年目でRubyKaigiに初参加しました。

techblog.raksul.com

初心者にも温かく接してくださるRubyistの皆さんのおかげでとても楽しい3日間だったのですが、昨年の私の感想としては……

「楽しいけど、セッションの内容は全然理解できない(泣)」

でした。

セッションの内容を理解できなくても楽しめたけど、理解できたらもっと楽しいはず……!!!
ということで、今年は昨年よりも予習の量を増やし、かつ分野を絞ることで1つでも「理解できた!」と思えるセッションの数を増やすことを目指すことにしました。この記事では、RubyKaigi初心者の私が予習した内容をアウトプットしていこうと思います。このブログがどこかのRubyistの助けになれば幸いです!

今回私が勉強したのはパーサー関連です。 Rubyは自由自在に書けるという強みがあり、その特性を活かしたTRICKというコンテストもあります。昨年のRubyKaigiでもキーノートでTRICKについて説明されていましたが、こんなにも変幻自在なコードを書けちゃうのは面白いですよね!

rubykaigi.org

こんなコードを実行している裏側はどうなっているのか?と興味が湧いたので、今回はRubyのインタプリタ、その中でもコードの意味を解析するパーサー周りについて勉強してみることにしました。

パーサーとは

Rubyのパーサーに興味があるとはいえ、私はインタプリタの仕組みなどをちゃんと勉強したことがなかったので、一般的なインタプリタ・パーサーの仕組みの勉強から始めました。

インタプリタの構成

一般的にインタプリタの処理は以下のようなフローで行われます。

  1. 字句解析
  2. 構文解析
  3. 中間コード生成
  4. 最適化
  5. 機械語生成

それぞれのフローについて役割、実行の主体、アウトプットをまとめました。

フロー名 役割 実行の主体 アウトプット
字句解析 ソースコードを意味のある最小単位(トークン)に分解する 字句解析器(lexer) トークン列
構文解析 トークン列から言語の文法に従って意味のある構造(構文木)を作成する 構文解析器(パーサー) 抽象構文木(AST)
中間コード生成 ASTから中間表現に変換する コードジェネレーター 中間表現(IR)
最適化 中間表現を効率的な形に変換する オプティマイザー 最適化された中間表現
機械語生成 中間表現を実行可能な機械語に変換する コードジェネレーター 機械語コード

今回はこの中でも構文解析とパーサーの生成について掘り下げていきます。

言語の文法の定義方法

構文解析では、字句解析によって生成したトークン列と文法から意味のある構造(抽象構文木)を作成します。そのためには言語の文法を定義する方法が必要です。

文法を定義するための記法として代表的なものがバッカス・ナウア記法(BNF)です。BNFでは以下のように文法を定義します。

<数字> ::= 0|1|2|3|4|5|6|7|8|9
<英字> ::= a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z
<名前> ::= <英字>|<名前><英字>|<名前><数字>

上記は以下を意味しています。

  • <数字>は0~9のいずれか
  • <英字>はa~zのいずれか
  • <名前>は <英字>、<名前><英字>、<名前><数字> のいずれか( <名前><英字> は <名前> の後ろに <英字> をつけたものを表す)

この場合における0~9やa~zはこの文法規則においてこれ以上置き換えることができません。このような記号を終端記号と呼びます。終端記号以外の記号は非終端記号と呼びます。この例では <英字>や<名前>といったものが非終端記号です。

このように決まった記法で定義された文法をもとにしてパーサーが作成されます。実際には、バッカス・ナウア記法を拡張した拡張バッカス・ナウア記法が多く用いられます。

抽象構文木

構文解析のアウトプットである抽象構文木(AST)は、文法構造をツリー構造で表したものです。試しにRubyを構文解析するためのライブラリであるRipperを使って3 + 5 * 2 というプログラムの抽象構文木を出力してみましょう。

以下のコードを実行してみます。

require 'ripper'
require 'pp'

code = "3 + 5 * 2"
ast = Ripper.sexp(code)

pp ast

すると、以下のような出力が得られます。

Ripperの出力結果

これはすなわち、以下のようなASTが得られたことになります。

得られたAST

構文解析のアルゴリズム

構文解析では、字句解析で得られたトークンと文法の定義をもとに解析を進めていきます。その解析の際のアルゴリズムには大きくトップダウン型とボトムアップ型の2種類があります。

トップダウン型とは、プログラムを左から右に走査し、非終端記号を終端記号に置き換えていくアルゴリズムのことです。これだけだと分かりにくいかもしれないので、私の理解を図を用いてざっくりと説明しようと思います。

以下のような規則からなる文法があったとします。

S ::= aAd   (1)
A ::= b|c   (2)

abdというプログラムが与えられたとき、トップダウン型では以下のように解析します。

トップダウン型のアルゴリズム
上記のように、トップダウン型では「これから読み込むものの形を先に仮定」して解析を進めます。

トップダウン型では読み込むものの形を仮定するため、文法に加える制限が大きく適用範囲は比較的狭くなります。また、文法の形がそのままパーサープログラムの形になるため、プログラムは比較的分かりやすいのが特徴です。
トップダウン型の例としてはLL法などが挙げられます。

一方、ボトムアップ型はプログラムを左から右に走査し、終端記号を非終端記号に置き換えていくアルゴリズムです。

また私なりの理解を図にしてみると、先ほどと同じ文法規則・プログラムが与えられたときボトムアップ型では以下のように解析します。

ボトムアップ型のアルゴリズム

このように、ボトムアップ型では「確定したものが出てきたら順次置き換える」という戦略を取ります。

トップダウン型と比較して考えると、ボトムアップ型はその適用範囲の広さが特徴的です。またパーサープログラムの実装が分かりにくいものになることが多いことも特徴のひとつで、パーサージェネレーターを用いてプログラムを自動生成することが多いようです。
ボトムアップ型の例としてはLR法、SLR法、LALR法などが挙げられます。

パーサーの実装方法

パーサーの実装方法には人が手書きで書く方法とパーサージェネレーターを用いて自動生成する方法があります。

パーサージェネレーターとは、文法規則を記述したファイルをもとにパーサーのプログラムを自動生成するものです。代表的なものとしてYacc、Bisonなどがあります。

パーサージェネレーターを使うメリットとしては以下のようなものがあります。

  • 複雑なパーサープログラムを効率的に開発できる
  • 文法の定義とパーサープログラムの実装に乖離がなくなる

Rubyのパーサーの歴史

ここまでの内容を踏まえて、次はRubyのパーサーについて理解を深めていきます!
今年のセッションを聞くにあたって、Rubyのパーサーの歴史についてざっくりと理解しておくと良さそうだと思ったので時系列順でまとめてみました。

CRuby誕生

CRubyはまつもとゆきひろさんが作った最初のRuby実装です。当初はパーサージェネレーターとしてYaccを用いていたようです。このYaccの入力となる文法規則を記述したファイルがparse.yです。その後、YaccからBisonに移行されます。

この頃の実装ではパフォーマンスが重視されており、パーサーとランタイムが密結合していたようです。

Rubyツールの多様化とパーサーの移植性問題

その後、JRubyなどの実装やRipperなどのパーサーライブラリといった、CRubyのパーサーに依存する様々なツールが作られます。しかし、CRubyのパーサーがランタイムと密結合しているために、各ツールでCRubyと同じパーサーを再実装し、CRubyのアップデートに追従する必要があるという課題がありました。

Lrama誕生

Yuichiro Kanekoさんが新たなパーサージェネレーターであるLramaを作成し、CRubyに追加されます。LramaはBisonの置き換えを目指して再実装されたパーサージェネレーターです。

これまではCRubyはBisonに依存していましたが、Bisonの細かなバージョンでうまく動作しない、Bisonだと思い通りに拡張することができない、といった課題が発生していました。Lramaの追加により”脱Bison”が達成でき、上記のような課題の解消に貢献したということですね。

Lramaは現在も改善が続けられています。特に、上記で述べた移植性の問題の解消に向けてユニバーサルパーサー、すなわちRuby本体から独立して動くパーサーの実現に向けて作業が行われているようです。

Prism誕生

Lramaの導入と同時期に、エラー許容性のあるユニバーサルパーサーとしてPrismが実装され、Ruby3.3でdefault gemとして追加されました。Ruby3.4ではparse.yから生成されるパーサーに代わり、Prismがデフォルトのパーサーとして採用されたようです。

エラー許容性とは、パーサーの解析中に構文エラーが発生しても解析を継続できる能力のことです。SorbetやRuby LSPなどの登場により、エラー許容性の重要度が高まっています。

上記の取り組みとともに、Prismプロジェクトはユニバーサルパーサーの実現にも注力しました。Rubyエコシステムの様々なパーサーメンテナーと緊密に連携し、ユニバーサルパーサーとしての要件を整理していったようです。現在では様々なプロジェクトがPrismを使用しています。

Prismは手書きで実装されたパーサーなので、パーサージェネレーターであるLramaとは対照的ですね。

今年気になるセッション

最後に、今年のRubyKaigiで個人的に気になるセッションを紹介していこうと思います!せっかく勉強したのでパーサー関連のセッションに重点的に参加したいです。

Make Parsers Compatible Using Automata Learning

rubykaigi.org

Ruby3.4からデフォルトになったPrismが、従来のパーサーであるparse.yと高い互換性を保っている方法について説明するセッションです。ここではオートマトン学習という手法に着目するようですね。このブログでは省略してしまったのですが、構文解析のアルゴリズムの裏側ではオートマトンを多用しているので、興味のある方は事前に調べておくとより理解が深まるかもしれません!

Ruby's Line Breaks

rubykaigi.org

Rubyの”改行”の扱いに着目したセッション!これまたニッチな話題ですね。しかし、言われてみればいろんな意味を持ちうる改行がどのように処理されているのか気になります。 Lramaの作者であるYuichiro Kanekoさんの発表ということもあり、注目のセッションです!

Dissecting and Reconstructing Ruby Syntactic Structures

rubykaigi.org

parse.yの文法規則の構造について深掘るセッションのようです。parse.yに記載されている文法規則は非常に複雑とのことなので、一体どのように読み解いていったのか、また読み解いた結果どのような示唆が得られたのか非常に気になります!

The Implementations of Advanced LR Parser Algorithm

rubykaigi.org

Lramaのパーサーアルゴリズムの改善についてのセッションです。ボトムアップ型であるLALR法をさらに進化させたIRLR法というアルゴリズムについて、またLramaへの実装について説明してくださるようです。 ゴリゴリのアルゴリズムの話のようですね!LALR法だけでも予習していかねばと身が引き締まります…!

最後に

パーサー関連の話題は勉強すればするほど 奥が深くて面白いですね! 最近のRubyにおいてはLramaとPrismという2つの大きなトレンドがあるようなので、両者の違いに着目しながら聞いてみたいです。

それでは、Rubyistの皆さんと松山でお会いできるのを楽しみにしています!

参考文献

コンパイラの構成と最適化 (第2版)|朝倉書店

Ruby: 2024年までのPrismパーサーの長い歴史を振り返る(翻訳)|TechRacho by BPS株式会社

Rubyパーサーを一新するprism(旧YARP)プロジェクトの全容と将来(翻訳)|TechRacho by BPS株式会社

Lrama LRパーサジェネレータが切り開く、Rubyの構文解析の未来 | gihyo.jp

Prism:エラートレラントな、まったく新しいRubyパーサ | gihyo.jp

【RubyKaigi 2025参加者向け】RuboCopに関する2024年発表内容振り返り&今年の見どころを紹介

はじめに

こんにちは。ラクスル事業部 Webエンジニアの西元・森田です。

私達は業務で主にRuby on Railsを使って開発をしているため、RubyKaigi 2025に参加することを決めました。

現在、私達の事業部では複数のチームが並行して印刷ECサイトを開発しています。

それぞれのチームでは印刷商材の拡大や新規サービス追加に加え、会社として力を入れていくM&Aを行うため技術的負債の改善もしており、複数チームでコーディング規約を守って行くためにRuboCopを活用しています。

また、最近は社内でCursorを導入したため、VS CodeのRuboCop拡張を使う場面が増えています。

RuboCopの最新情報を取り入れて、コードの質を保ちつつ、効率よく開発を進めたいと思い、RubyKaigi 2025のRuboCop: Modularity and AST Insightsの発表に注目しています。

Rubykaigi 2024では、RuboCop: LSP and Prism でRuboCopの最新動向の発表があったため、このタイミングで振り返ることで、今年の発表の予習になると考えました。

今回は、2024年の内容をふりかえりながら、2025年のRubyKaigiをより楽しむための前提知識をまとめました。


2024年の発表

1. RuboCopにLSPが公式搭載された(RuboCop v1.53以降)

概要

2023年、RuboCopに--lspオプションが追加され、エディタからLSP機能が正式に使えるようになりました。

実際にVSCodeでRuboCopを有効化して、Outputを見たところ、以下のように -—lspオプションで起動されていることがわかりました。

[client] Starting language server: /Users/y.morita/.rbenv/versions/3.x.x/bin/rubocop --lsp

これによって、毎回ターミナルでrubocop -aを実行しなくても自動でフォーマットされるようになります。

⚠️RuboCopのバージョンが1.53.0未満の場合、LSPは使えないことに注意してください。

紹介されていた設定例

詳しい設定内容は公式ドキュメントを参照してください。

  • 自動修正

    settings.jsoneditor.formatOnSave

      "[ruby]": {
        "editor.defaultFormatter": "RuboCop.vscode-RuboCop",
        "editor.formatOnSave": true
      },
      "rubocop.autocorrect": "true",
    
  • RuboCopの実行パス・バージョンを指定したい時

    settings.jsoncommandPath, mode

    ※ 両方指定した場合、commandPathが優先されます。

      "rubocop.commandPath": "/Users/y.morita/.rbenv/versions/3.x.x/bin/rubocop",
    

    自分の場合、当初、GemfileのRuboCopを参照していたものの("rubocop.mode": "enableViaGemfile")、Gemfileで指定した拡張copが、バージョン1.53以下のRuboCopに依存していたため、LSPが使用できませんでした。

    よって、暫定対応としてcommandPathでローカルにインストールした最新バージョンのRuboCopを指定し、LSPを有効にしています。

  • Rubyの実行環境と、RuboCop解析対象を分けたい時

    .rubocop.ymlTargetRubyVersion

      AllCops:
        TargetRubyVersion: 2.7
    

    たとえばRuby 2.7以上で動くGemを開発していて、手元の実行環境がRuby 3.2の場合、解析対象を2.7にしておくことで、3.0以降の構文やメソッドを使ってしまうのを防げます。


2. Parser gemからPrismへパーサの変更

RuboCopとAST(抽象構文木)

RuboCopはRubyコードのスタイルと文法規則を自動でチェックしてくれる頼れる存在です。その裏側では、Rubyプログラムを解析し、ルールに違反していないかをチェックする仕組みが動いています。

この解析の中心となるのがパーサーと呼ばれるプログラムです。パーサーは、Rubyのソースコードをコンピュータが理解しやすい構造、すなわち抽象構文木 (Abstract Syntax Tree, AST) に変換します。

例えばParserのASTで 2 + 2 を表すと下記のようになります。

s(:send,
  s(:int, 2), :+,
  s(:int, 2))

2 + 2 というRubyのプログラムとして書かれた文字列がASTに変換されることで意味のある構造に分解されます。

このようにASTにすることで、RuboCopは単なる文字列としてのコードではなく、その意味構造に基づいてより高度な解析(例えば、「特定のメソッドが呼ばれているか」「特定の構文が使われているか」など)を行うことができます。

私達WEBエンジニアがよく使っている正規表現によるパターンマッチングよりも高度な解析ができます。

現在のパーサー:Parser gem

これまでRuboCopは、Rubyで書かれた強力なパーサーライブラリである Parser gem を利用してきました。

RubyKaigiでパーサーをイメージしながら話を聞けるように、実際にParser gemがどのようにASTを生成するのか簡単なコードで試してみました。

まず、Parser gemをインストールします。

Ruby

gem install parser
irb

irb上で以下のRubyコードをパースしてみます。

  • 例1: puts 'こんにちは' のパース

    Ruby:

      require 'parser'
    
      Parser::CurrentRuby.parse("puts 'こんにちは'")
    

    実行結果:

      s(:send, nil, :puts,
        s(:str, "こんにちは"))
    

    このASTは、「レシーバーなし(nil)」で「puts」メソッドが呼び出され、その引数として「文字列 "こんにちは"」が渡されていることを示しています。

  • 例2: クラス定義を含むコードのパース

    あまり意味はないクラスですが、クラスをパースして眺めてみます。

    Ruby:

      require 'parser'
    
      source = <<~EOF
        class Raksul
          attr_accessor :hoge
    
          validates :hoge, presence: true
        end
      EOF
      Parser::CurrentRuby.parse(source)
    

    実行結果:

      s(:class,
        s(:const, nil, :Raksul), nil,
        s(:begin,
          s(:send, nil, :attr_accessor,
            s(:sym, :hoge)),
          s(:send, nil, :validates,
            s(:sym, :hoge),
            s(:hash,
              s(:pair,
                s(:sym, :presence),
                s(:true))))))
    

    ぎりぎりこの実行結果から元のRubyのコードに近いものをイメージすることができますね。

新しいパーサー:ruby/Prism

2024年の講演ではRuboCopのParser gemからPrismへパーサーが変更になるというお話がありました。

PrismはRubyの公式で開発が進められている新しいパーサーで、エラートレランス、C言語実装で高速、Ruby公式パーサーのため安定したメンテナンスが期待される、という特徴があります。

Prismは自動生成ツールではなく、Rubyコミッターの手によって一つ一つ丁寧に実装されており、Rubyコミッターの皆さんには頭が下がります。

また速度に関しては、昨年のRubyKaigiの発表でPrismはParser::CurrentRubyより33倍速く実行できるという話がありました。

参考:https://speakerdeck.com/koic/rubocop-lsp-and-prism?slide=81

Prismが生成するASTの構造は、Parser gemのものとは異なります。Parser gemと同様に簡単なコードをPrismでパースしてみました。

まず、Prism gemをインストールし、irbで試してみます。

Ruby:

gem install prism
irb
  • 例1: puts 'こんにちは' のパース

    Ruby:

      require 'prism'
    
      source = "puts 'こんにちは'"
      result = Prism.parse(source)
      ast = result.value
      puts ast.statements.body.first.inspect
    

    実行結果:

      @ CallNode (location: (1,0)-(1,22))
      ├── flags: newline, ignore_visibility
      ├── receiver: ∅
      ├── call_operator_loc: ∅
      ├── name: :puts
      ├── message_loc: (1,0)-(1,4) = "puts"
      ├── opening_loc: ∅
      ├── arguments:
      │   @ ArgumentsNode (location: (1,5)-(1,22))
      │   ├── flags: ∅
      │   └── arguments: (length: 1)
      │       └── @ StringNode (location: (1,5)-(1,22))
      │           ├── flags: ∅
      │           ├── opening_loc: (1,5)-(1,6) = "'"
      │           ├── content_loc: (1,6)-(1,21) = "こんにちは"
      │           ├── closing_loc: (1,21)-(1,22) = "'"
      │           └── unescaped: "こんにちは"
      ├── closing_loc: ∅
      └── block: ∅
    

    Parser gemの出力と比較すると、ノードの種類や属性が異なっていることがわかります。Prismでは、メソッド呼び出しは CallNode で表され、引数は ArgumentsNode の下に StringNode として詳細な位置情報とともに格納されています。

    Parser gemに比べ複雑に見えますが、引数で暗黙的に表現するのに比べ、属性名を明確に表示しているので見慣れるとドキュメントとしてはこちらのほうが理解しやすいようにも感じます。

  • 例2: クラス定義を含むコードのパース

    Ruby:

      require 'prism'
    
      source = <<~EOF
          class Raksul
           attr_accessor :hoge
    
           validates :hoge, presence: true
          end
      EOF
      result = Prism.parse(source)
      ast = result.value
      puts ast.statements.body.first.inspect
    

    実行結果:

      @ ProgramNode (location: (1,0)-(5,3))
      ...
      irb(main):039:0> puts ast.statements.body.first.inspect
      @ ClassNode (location: (1,0)-(5,3))
      ├── flags: newline
      ├── locals: []
      ├── class_keyword_loc: (1,0)-(1,5) = "class"
      ├── constant_path:
      │   @ ConstantReadNode (location: (1,6)-(1,12))
      │   ├── flags: ∅
      │   └── name: :Raksul
      ├── inheritance_operator_loc: ∅
      ├── superclass: ∅
      ├── body:
      │   @ StatementsNode (location: (2,2)-(4,32))
      │   ├── flags: ∅
      │   └── body: (length: 2)
      │       ├── @ CallNode (location: (2,2)-(2,20))
      │       │   ├── flags: newline, ignore_visibility
      │       │   ├── receiver: ∅
      │       │   ├── call_operator_loc: ∅
      │       │   ├── name: :attr_accessor
      │       │   ├── message_loc: (2,2)-(2,15) = "attr_accessor"
      │       │   ├── opening_loc: ∅
      │       │   ├── arguments:
      │       │   │   @ ArgumentsNode (location: (2,16)-(2,20))
      │       │   │   ├── flags: ∅
      │       │   │   └── arguments: (length: 1)
      │       │   │       └── @ SymbolNode (location: (2,16)-(2,20))
      │       │   │           ├── flags: static_literal, forced_us_ascii_encoding
      │       │   │           ├── opening_loc: (2,16)-(2,17) = ":"
      │       │   │           ├── value_loc: (2,17)-(2,20) = "hoge"
      │       │   │           ├── closing_loc: ∅
      │       │   │           └── unescaped: "hoge"
      │       │   ├── closing_loc: ∅
      │       │   └── block: ∅
      │       └── @ CallNode (location: (4,2)-(4,32))
      │           ├── flags: newline, ignore_visibility
      │           ├── receiver: ∅
      │           ├── call_operator_loc: ∅
      │           ├── name: :validates
      │           ├── message_loc: (4,2)-(4,11) = "validates"
      │           ├── opening_loc: ∅
      │           ├── arguments:
      │           │   @ ArgumentsNode (location: (4,12)-(4,32))
      │           │   ├── flags: contains_keywords
      │           │   └── arguments: (length: 2)
      │           │       ├── @ SymbolNode (location: (4,12)-(4,16))
      │           │       │   ├── flags: static_literal, forced_us_ascii_encoding
      │           │       │   ├── opening_loc: (4,12)-(4,13) = ":"
      │           │       │   ├── value_loc: (4,13)-(4,16) = "hoge"
      │           │       │   ├── closing_loc: ∅
      │           │       │   └── unescaped: "hoge"
      │           │       └── @ KeywordHashNode (location: (4,18)-(4,32))
      │           │           ├── flags: symbol_keys
      │           │           └── elements: (length: 1)
      │           │               └── @ AssocNode (location: (4,18)-(4,32))
      │           │                   ├── flags: static_literal
      │           │                   ├── key:
      │           │                   │   @ SymbolNode (location: (4,18)-(4,27))
      │           │                   │   ├── flags: static_literal, forced_us_ascii_encoding
      │           │                   │   ├── opening_loc: ∅
      │           │                   │   ├── value_loc: (4,18)-(4,26) = "presence"
      │           │                   │   ├── closing_loc: (4,26)-(4,27) = ":"
      │           │                   │   └── unescaped: "presence"
      │           │                   ├── value:
      │           │                   │   @ TrueNode (location: (4,28)-(4,32))
      │           │                   │   └── flags: static_literal
      │           │                   └── operator_loc: ∅
      │           ├── closing_loc: ∅
      │           └── block: ∅
      ├── end_keyword_loc: (5,0)-(5,3) = "end"
      └── name: :Raksul
    

    クラスの定義になってくるとなかなか詳細を追ってみようという気持ちにはなりにくいですね。

  • 利用時の注意点

    Prismを使うには、.rubocop.ymlTargetRubyVersion を3.3以上にする必要があります。


2025年の発表

RubyKaigiのScheduleや講演者であるITOさんのブログを拝見したところ、RuboCopの「プラグイン・アドオン・AST」について話されるようです。

  1. RuboCop プラグイン(RuboCop 1.72以降)

    これまでは、rubocop-rspecrubocop-performanceなどの拡張Copを使うには、Gemを個別にインストールした上で、.rubocop.ymlrequireで読み込む必要がありました。

     require:
       - rubocop-rspec
       - rubocop-performance
    

    RuboCop 1.72以降では、.rubocop.ymlのpluginsに記述することで、Gemのインストール不要で、拡張Copが公式のプラグインとしてサポートされるようになりました。

     plugins:
       - rubocop-rspec
       - rubocop-performance
    

    また、BundlerでインストールされているGemをRuboCopが検知して、関連プラグイン(例:RSpecを使っている場合、rubocop-rspec)を提案してくれるようになるようです。

    詳細については、公式ドキュメントやRubyKaigi 2025での発表に注目です。

  2. アドオン

    Shopifyが提供しているRuby LSPは、エディタ上でのコード補完や定義ジャンプといった豊富な機能を持っています。このアドオンという実験的機能に、RuboCopが組み込まれるようになりました。現時点では大きな変更やユーザへの影響はないものの、RubyKaigiで今後の展望について発表があるようです。

  3. AST

    去年に引き続き、Parser gemからPrismへの変更についての話が予定されています。今回の予習記事で紹介したASTの実行イメージが理解を助けてくれると良いなと思います。

    Rubyの新構文に対応した件についての話もあるようです。

    例えば、Ruby 3.4以降でサポートされるitの暗黙的定義に対応してStyle/ItBlockParameterでデフォルトだと下記はOKとなります。

     [1, 2, 3].each { puts it }
     [1, 2, 3].each { |it| puts it }
    

    しかし、下記のようなナンバードパラメータを使うのはデフォルトではNGとなります。

     [1, 2, 3].map { _1 * _2 }
    

まとめ

普段業務で使っている際は深く意識していませんでしたが、今回の予習を通して、RuboCopが速度面や利便性で進化していることがわかりました。

Rubykaigi 2025での発表にも着目したいです。

RubyKaigi 2025 初参加エンジニアのためのRuby静的型付けガイド

RubyKaigi 2025、近づいてきましたね!

ラクスルでは、今年が RubyKaigi 初参加となるエンジニアも多いため、社内で予習会を開催しています。
本記事ではその中から、Rubyの静的型付けに関するトピックをまとめました。

この記事では、以下の点を整理しておきます:

  • Ruby の型まわりの全体像と分類(RBS系 / RBI系)
  • 各ツールの特徴やメリット・デメリット
  • RubyKaigi 2025 における関連セッションの予習
  • 実際に導入する際の利用イメージ

はじめに:Ruby における型の考え方

Ruby は「動的型付け」言語として知られ、型を明示せずに柔軟で直感的なコードが書けます。 一方で、大規模開発や長期運用の現場では「型がないことで見逃されるバグ」「リファクタリング時の不安」などが課題になる場面も多くあります。
そうした背景から登場したのが、Ruby に静的型付けを導入する試みです。


型付けアプローチの分類

大きく分けて 2 つの流派があります:

🔷 RBS系(Ruby 公式系 / コードとは別のファイルで型定義)

  • RBS (Ruby Signature):Ruby の型情報を扱う言語および、それを扱う Gem(Ruby 本体にバンドル済み)
  • Steep:RBS をもとに型チェックを行うツール
  • TypeProf:Ruby のコードから RBS を推論するツール
  • rbs-inline:コードファイルにコメントとして型を記述し、そこから RBS ファイルを生成できる

🛠 利用イメージ:

  • プロダクトコードはそのまま書く
  • sig/ ディレクトリ以下に .rbs ファイルを定義
  • steep check で型チェックを実行 → 実行前にバグを検出
  • typeprof を使えば、既存コードから型を生成できる
  • rbs prototype rb でも既存コードから型を生成可能
  • rbs prototype rbi を使えば、後述の .rbi から .rbs を生成可能
  • rbs prototype runtime を使えば、ランタイムの型情報から型定義を生成可能
  • IDE で補完や型チェックを効かせたい場合にも有効

「Ruby 公式の型定義ツールを使いたい」といったニーズに向いています。


🔶 RBI系(Sorbet 系 / コード中に型を直接記述)

  • Sorbet:Stripe が開発した型チェッカーとその周辺ライブラリ群
  • RBI (Ruby Interface):依存 Gem や Rails、DSL の型定義ファイル(.rbi
  • Tapioca:型定義ファイルを自動生成するツール

🛠 利用イメージ:

  • メソッド定義の直前に sig { params(x: Integer).returns(String) } のような注釈を記述
  • CLI で型チェックを実行(srb tc
  • sorbet/ 以下に tapioca で Gem などの .rbi ファイルを生成
  • 実行時チェックも可能(T::Struct, T::Enum など)

「コード中に型注釈を直接書きたい」といったニーズに向いています。


比較表:RBS系 vs Sorbet系

項目 RBS系 (Steep, TypeProf) Sorbet系 (Sorbet, Tapioca)
型定義 .rbs ファイル .rbi ファイル + 注釈 DSL
型チェック方法 外部ツール(Steep) Sorbet CLI
型推論 TypeProf で対応 一部対応(限定的)
IDE 連携 Ruby Language Server など Sorbet Language Server
実行時型検査 rbs prototype runtime Sorbet のランタイムチェッカー
サードパーティ Gem の対応 rbs collection Tapioca で自動生成

RubyKaigi 2025 関連セッション

以下は、RubyKaigi 2025 における静的型付けに関係が深いセッションです。
Ruby の静的型付けに興味のある方には、特におすすめです!

※内容は RubyKaigi 公式サイトのセッション紹介をもとにした要約です。

📌 Introducing Type Guard to Steep (tk0miya, JP)

  • Steep に新たに導入される拡張機能「Type Guard」が紹介されます。
  • 開発者が独自の型絞り込みロジックを定義でき、より柔軟な型チェックが可能に!

rubykaigi.org

📌 Automatically generating types by running tests (sinsoku_listy, JP)

  • テスト実行時に型情報を収集し、RBS 型定義を自動生成する Gem が紹介されます。
  • 既存アプリケーションへの RBS 導入がより簡単に!

rubykaigi.org

📌 Writing Ruby Scripts with TypeProf (mametter, JP)

  • Ruby 3.4 にバンドルされた、Ruby 構文の全範囲をサポートする TypeProf が紹介されます。
  • 実際の経験に基づく活用事例や注意点も共有されます!

rubykaigi.org

📌 From C extension to pure C: Migrating RBS (amomchilov, EN)

  • RBS の実装を C 拡張から pure C に移行した取り組みが紹介されます。
  • Prism、Sorbet、JRuby、TruffleRuby などで RBS の活用が進むと期待されます!

rubykaigi.org

📌 Inline RBS comments for seamless type checking with Sorbet (Morriar, EN)

  • Sorbet を使い、Ruby コードにコメント形式で型情報を埋め込む手法が紹介されます。
  • RBI から RBS への変換ツールなど、両者の連携についても幅広く紹介されます!

rubykaigi.org


おわりに

「動的型付け」やメタプログラミングが Ruby の大きな特長である一方、それらに適切な型付けを行うということは非常に困難です。 だからこそ、Ruby コミュニティの壮大な試みに今後も目が離せません。

RubyKaigi、今年も思いっきり楽しみましょう!