Ruby 4.0 移行では新機能より暗黙の依存を確認する

Ruby 4.0.0 は2025年12月25日に公開された。Ruby Box、ZJIT、Ractor の改善が大きく紹介された一方、Ruby 3.x で動いていた既存コードを移す際に先に確認すべきなのは、新機能を採用できるかではない。標準添付ライブラリ、削除された API、例外表示、RubyGems と Bundler、C 拡張に対する暗黙の依存が残っていないかである[1]

Ruby 4.0.0 には RubyGems 4.0.3 と Bundler 4.0.3 が同梱された。したがって、実行処理が Ruby 4.0 で通るだけでは移行完了にならない。依存関係の解決、インストール、テスト、外部通信、配布まで含めた一連の手順が、新しい既定値と API で成立する必要がある[2]


移行対象は Ruby 本体だけではない

Ruby のメジャーバージョン更新では、言語仕様の差分だけを調べても影響範囲を確定できない。Ruby 4.0 の変更は、少なくとも四つの層に現れる。

第一は言語と実行時の層である。展開演算子に nil を渡した場合の挙動、Ractor の通信 API、非推奨 API の削除、バックトレースの表現が変わる。第二は Ruby が同梱するライブラリの層である。以前は追加指定なしで利用できたライブラリが、別の gem への依存として扱われる場合がある。

第三は RubyGems と Bundler の層である。Ruby 本体を変更していなくても、コマンドのオプション、Gemfile の記述、ロックファイル、Bundler の公開 API に依存する処理が変わる。第四は C 拡張とオペレーティングシステムの層である。Ruby コードのテストが通っても、ネイティブ拡張のビルドやファイル記述子の扱いに問題が残ることがある。

移行作業は、この四層を一つずつ確認する必要がある。Ruby 4.0 を起動して単体テストが通ったという結果だけでは、残る三層を検証できない。


標準添付ライブラリへの暗黙依存を外す

CGI は用途によって依存先が分かれる

Ruby 4.0 では CGI ライブラリが default gem から外れた。Ruby 本体が引き続き提供するのは cgi/escape で利用できるエスケープ系の機能であり、CGI 全体ではない[2]

既存コードで CGI.escape や CGI.escapeHTML だけを使っている場合は、読み込む対象を cgi/escape に限定できる。CGI クラスのその他の機能を使う場合は、cgi gem をアプリケーションの依存関係として明示する必要がある。確認対象は require 文だけではない。ライブラリ内部で CGI 定数を参照している箇所や、実行環境にたまたま cgi gem が入っていたため動いていた処理も含まれる。

SortedSet は Set の移動とは別に確認する

Set は標準ライブラリからコアクラスへ移ったが、それによって SortedSet までコアに入ったわけではない。set/sorted_set.rb は削除され、SortedSet は自動ロードされなくなった。使用を継続する場合は sorted_set gem を導入し、明示的に読み込む必要がある[2]

この変更は、Set を利用しているコード全体ではなく SortedSet を利用している箇所に限定して影響する。名称検索で利用箇所を特定し、並べ替え順への依存を含むテストを実行すればよい。Set の変更だけを見て一律に gem を追加すると、不要な依存を増やすことになる。

Net::HTTP は送信できても意味が変わり得る

Ruby 4.0 に同梱された Net::HTTP では、本文を持つ POST や PUT に Content-Type を指定しなかった場合、application/x-www-form-urlencoded を自動設定する従来の挙動が削除された。送信自体が成功しても、受信側が本文をフォームデータとして解釈しなくなる可能性がある[2]

この差分は、HTTP クライアントの単体テストだけでは見落としやすい。要求ヘッダーを検査するテストか、実際の受信側と接続する結合テストで確認する必要がある。期待する形式が JSON、フォーム、その他の形式のいずれであっても、Content-Type を呼び出し側で明示すれば Ruby の既定値に依存しない。


削除された API と表示変更は異なる形で失敗する

Ractor は通信と終了待ちを分けて置き換える

Ractor::Port の導入に伴い、Ractor.yield、Ractor#take、Ractor#close_incoming、Ractor#close_outgoing は削除された。通信には Ractor::Port、終了待ちには Ractor#join、終了値の取得には Ractor#value を使う構成へ変わっている[2]

旧 API の名称を機械的に置き換えるだけでは不十分である。従来の yield と take は、Ractor の終了値とメッセージ通信を近い形で扱っていた。Ruby 4.0 では、処理の完了を待っているのか、独立した通信路から値を受信しているのかを区別して設計し直す必要がある。Ractor を使用していないコードには影響しないため、一般的な移行作業の必須改修として広げる必要はない。

終了ステータスはビット演算から専用メソッドへ移す

Process::Status#& と Process::Status#>> は Ruby 3.3 で非推奨となり、Ruby 4.0 で削除された[2]。終了コードを取得する処理は exitstatus、正常終了を判定する処理は success?、シグナル終了を判定する処理は signaled? など、Process::Status が提供する目的別のメソッドへ置き換えるべきである[3]

ビット演算を残していたコードは、終了コード以外の状態を同じ整数から読み取っている場合がある。置き換え時には、単に構文を直すのではなく、正常終了、異常終了、シグナル終了、停止のどれを判定していたのかを確認する必要がある。

非推奨 API とバックトレースはテスト側にも影響する

ObjectSpace._id2ref は Ruby 4.0 で非推奨となった。使用箇所がある場合は、オブジェクト ID から後で実体を復元する設計自体を確認する必要がある。単純な名称変更ではなく、オブジェクトの生存期間とガベージコレクションに依存する処理を除去できるかが判断点になる[2]

引数個数が不正な ArgumentError のバックトレースには、レシーバーのクラス名またはモジュール名が含まれるようになった。また、internal と表示されていたフレームの見え方も変更された[2]。例外の種類や終了状態ではなく、メッセージ全文やバックトレース全文を固定文字列で比較するテストは失敗し得る。必要な契約が例外クラス、主要な文言、呼び出し元のいずれであるかを分けて検証すべきである。


Bundler 4 は同じインストール手順の意味を変える

Ruby 4.0.0 は Bundler 4 を同梱する。Bundler 4 では、bundle install に渡した一部のオプションを次回以降へ暗黙に記憶する挙動が削除された。対象には path、without、with、deployment、frozen などが含まれる。従来の手順が、最初の一回だけオプションを渡し、その後の実行で保存済み設定を利用していた場合、同じコマンド列でもインストール対象や配置先が変わり得る[4]

継続して必要な値は、環境変数または bundle config に明示する必要がある。CI の各ジョブが毎回新しい環境で動く場合でも、開発端末や長寿命のビルド環境に残る設定との差を確認しなければならない。再利用された作業ディレクトリだけで成功する手順は、再現可能な移行手順とはいえない。

Gemfile では依存元の厳密な指定が既定となり、複数の大域的な source、path、git の指定は受け入れられなくなった。どの gem をどの取得元から入れるかを、個別指定またはブロックで結び付ける必要がある。新しいロックファイルには CHECKSUMS 節も追加される[4]

Bundler の内部 API を呼ぶスクリプトも確認対象である。Bundler.clean_env、Bundler.with_clean_env、Bundler.clean_system、Bundler.clean_exec は削除され、目的に応じて original 系または unbundled 系の API を使い分ける構成へ変わった[4]。アプリケーションコードだけでなく、テスト補助、生成処理、デプロイスクリプトに残っている場合がある。

Bundler 2.7 には Bundler 4 の挙動を事前に再現する設定が用意されている。Ruby 本体を切り替える前に simulate_version を 4 に設定し、依存解決とテストを通すことで、Ruby 本体の差分と Bundler の差分を分離できる[4]

1
2
bundle config set --local simulate_version 4
ndle install

その後に、各プロジェクトが通常使用しているテストコマンドと配布手順を実行する。


C 拡張はビルド成功だけで判断しない

Ruby 4.0 の C API では rb_thread_fd_close が非推奨となり、何もしない関数になった。C 拡張から Ruby へファイル記述子を公開する場合は、RUBY_IO_MODE_EXTERNAL を使って IO オブジェクトを作成し、rb_io_close で閉じる方法が示されている。ファイル記述子を直接閉じても、待機中の処理を中断できず、未定義動作につながる可能性がある[2]

この変更は、拡張のコンパイルが成功しただけでは検出できない。入出力待ちが発生する条件、複数の IO オブジェクトが同じファイル記述子を参照する条件、終了処理と並行処理が重なる条件で確認する必要がある。自作拡張がなくても、依存する gem がネイティブ拡張を含む場合は、Ruby 4.0 用のビルド成果物と実行テストを別に確認する。


言語変更と新機能は移行完了後に評価する

Ruby 4.0 では、*nil が nil.to_a を呼ばなくなった。また、行頭の論理演算子 ||、&&、and、or は前行からの継続として解釈されるようになった[2]。前者は nil.to_a の呼び出し自体に依存するコード、後者は Ruby の構文を独自に解析するツールや整形規則に影響し得る。ただし、多くの既存スクリプトでは、標準添付ライブラリや Bundler の変更より影響範囲が狭い。

Ruby Box は、一つの Ruby プロセス内でクラス、モジュール、モンキーパッチ、ライブラリの定義を分離する実験的機能である。利用にはプロセス起動時の RUBY_BOX=1 が必要であり、Ruby 4.0 の文書にはネイティブ拡張の導入や一部ライブラリに関する既知の問題も記載されている[1][5]。既存アプリケーションを Ruby Box 内へ移すことは、Ruby 4.0 への通常の移行作業とは別の設計変更として扱うべきである。

ZJIT も Ruby 4.0.0 時点では実験対象であり、公式の公開告知は本番環境への投入を控えるよう述べている[1]。Ruby 4.0 への互換性移行、YJIT から ZJIT への評価、Ruby Box の導入を同時に行うと、障害の原因を切り分けにくくなる。まず既存の実行方式で Ruby 4.0 への移行を完了し、その後に性能機能や分離機構を評価する順序が妥当である。


移行作業は失敗箇所を層ごとに分離する

  1. Ruby 3.x と Ruby 4.0 の両方で、依存関係を新しい作業ディレクトリへ導入し、環境に残った gem や Bundler 設定へ依存していないことを確認する。
  2. Bundler 2.7 の simulate_version で Bundler 4 の挙動を先に適用し、Gemfile、ロックファイル、インストールオプション、Bundler API の問題を Ruby 本体の変更から分離する。
  3. CGI、SortedSet、Ractor の削除 API、Process::Status の削除演算子、ObjectSpace._id2ref を名称検索し、利用箇所だけを対象に改修する。
  4. 単体テストに加え、Net::HTTP の要求ヘッダー、例外処理、コマンド終了状態、外部サービスとの接続を確認する。
  5. ネイティブ拡張をすべて Ruby 4.0 環境で再構築し、入出力待ちと終了処理を含む実行テストを行う。
  6. 既存方式で移行が完了した後に、Ruby Box、ZJIT、Ractor の新しい構成を個別に評価する。

失敗した工程を、Ruby 本体、標準添付ライブラリ、Bundler、C 拡張のどこに属するかで分類すれば、修正対象を絞れる。反対に、Ruby の更新、依存更新、JIT の変更、並列化を一度に行うと、同じ失敗から複数の原因候補が生じる。


結論

Ruby 4.0 移行の中心は、新機能の採用ではなく、Ruby 3.x の環境が暗黙に提供していた条件を明示することである。CGI と SortedSet は依存関係へ移し、Net::HTTP の Content-Type は送信側で指定する。削除された Ractor と Process::Status の API は、処理の目的に対応する API へ置き換える。Bundler のオプションと取得元は設定として固定し、C 拡張は実行時の入出力まで確認する。

bundle install からテスト、外部通信、配布までを新しい環境で通し、各失敗を変更層ごとに切り分けられれば、Ruby 4.0 への移行は完了と判断できる。Ruby Box と ZJIT は、その状態を作った後に評価する別の変更である。


参考文献

  1. naruse, “Ruby 4.0.0 リリース,” Ruby, 2025. https://www.ruby-lang.org/ja/news/2025/12/25/ruby-4-0-0-released/
  2. Ruby Core Team, “NEWS for Ruby 4.0.0,” Ruby Documentation, 2025. https://docs.ruby-lang.org/en/master/NEWS/NEWS-4_0_0_md.html
  3. Ruby Core Team, “Process::Status,” Ruby 4.0 Documentation, 2025. https://docs.ruby-lang.org/en/4.0/Process/Status.html
  4. Hiroshi SHIBATA, “Upgrading to RubyGems/Bundler 4,” RubyGems Blog, 2025. https://blog.rubygems.org/2025/12/03/upgrade-to-rubygems-bundler-4.html
  5. Ruby Core Team, “Ruby Box: Ruby’s in-process separation of Classes and Modules,” Ruby 4.0 Documentation, 2025. https://docs.ruby-lang.org/en/4.0/Ruby/Box.html