GitHubのトレンドを眺めていたら、rust-lang/rust そのものが上がっていました。ライブラリやツールではなく、コンパイラと標準ライブラリの本体です。確認した時点でスターは 119,985。10月1日に 1.99.0 が出たばかりでした。
私は業務の小さなサービスで Rust を使っていますが、ツールチェーンを上げるときは、リリースノートを読んで「うちには関係なさそうだな」で終わらせてしまうことが多いんですよね。以前、別の言語の処理系を上げたときに、ログの文字列を比べていたテストが一斉に落ちて、原因を探すのに思った以上に時間を取られた経験があります。リリースノートには書いてあったのに、自分のコードに当てはめて読めていなかったんです。
そこで今回は、1.99.0 のリリースノート(リポジトリの RELEASES.md)から、手元のコードに効きそうな項目を拾い、ひとつ前の 1.98.1 と同じコードで並べて確かめてみました。
試した環境はこんな感じです。
- 使い捨ての Docker コンテナ(
debian:bookworm-slim、x86_64)。メモリ 4GB・CPU 2 に制限し、ホストのフォルダはマウントしない - rustup 1.29.1 で、
1.99.0(rustcb940084d7、LLVM 23.1.1)と1.98.1(rustc48a229cea、LLVM 22.1.8)を入れて切り替え - 単一ファイルの確認は
rustc --edition 2024、Cargo の確認はcargo newした新規プロジェクトで、1ケースずつ別のプロセスで実行
入れるところから(ソースからはビルドしない)
最初に考えたのは、リポジトリを clone してコンパイラをビルドする方法でした。ただ、README の Quick Start は The Book の「Installation」を読むよう案内していて、ソースからのインストールは「not recommended」とはっきり書かれています。CPU 2・メモリ 4GB のコンテナで本体をビルドするのは現実的ではないので、今回は rustup の公式インストーラ(sh.rustup.rs)で配布版を入れました。
もう一つの選択肢として、手元にキャッシュされていた rust:1-bookworm イメージ(2.21GB)を使う手もありました。こちらを選ばなかったのは、2つの版を同じ rustup で並べたかったからです。版の違い以外の条件は、できるだけ揃えておきたいところです。
# リンカ用の gcc などを入れる(13.9秒)
$ apt-get install -y -qq curl ca-certificates gcc libc6-dev file
# rustup の公式インストーラで 1.99.0 を入れる(minimal プロファイル。所要 7,983ms)
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.99.0
# 比較用に 1.98.1 も入れる(所要 7,321ms)
$ rustup toolchain install 1.98.1 --profile minimal
1.98.1-x86_64-unknown-linux-gnu installed - rustc 1.98.1 (48a229cea 2026-09-01)
$ rustc +1.99.0 -Vv # 抜粋
rustc 1.99.0 (b940084d7 2026-09-28)
release: 1.99.0
LLVM version: 23.1.1
$ du -sh /root/.rustup/toolchains/*
577M /root/.rustup/toolchains/1.98.1-x86_64-unknown-linux-gnu
593M /root/.rustup/toolchains/1.99.0-x86_64-unknown-linux-gnu
minimal プロファイルなら、1つの版が8秒前後で入りました。展開後はそれぞれ 600MB 弱です。cargo new hello からの cargo run も問題なく、Hello, world! まで 0.1 秒ほどでした。
存在しないメソッドを呼んだときの提案が変わった
最初に試したのは、Compiler の項にあった「存在しないメソッドを提案するとき、似た名前より doc alias の完全一致を優先する」という変更です。他の言語から来た人が、つい別の名前でメソッドを呼んでしまう場面を想定しています。
count に #[doc(alias = "size")] を付け、あえて sizes という似た名前のメソッドも並べてから、b.size() を呼んでみました。
// 1.99.0: 存在しないメソッド名の提案で、doc alias の完全一致を似た名前より優先する
struct Basket {
items: Vec<u32>,
}
impl Basket {
#[doc(alias = "size")]
fn count(&self) -> usize {
self.items.len()
}
fn sizes(&self) -> Vec<usize> {
vec![self.items.len()]
}
}
fn main() {
let b = Basket { items: vec![1, 2, 3] };
println!("{}", b.size());
}
// rustc +1.98.1 の help(似た名前の sizes を勧める)
help: there is a method `sizes` with a similar name
|
19 | println!("{}", b.sizes());
| +
// rustc +1.99.0 の help(doc alias の count を勧める)
help: there is a method `count` with a similar name
|
19 - println!("{}", b.size());
19 + println!("{}", b.count());
|
1.98.1 は、名前が似ているだけで型も意味も違う sizes を勧めてきます。言われたとおり直すと、今度は戻り値の型でエラーになるパターンです。1.99.0 では doc alias を書いておいた count が出てきました。
細かいところですが、1.99.0 でも文言は「with a similar name」のままでした。size と count は似ていないので、この文だけ読むと少し戸惑うかもしれません。また、どちらの版でも std::os::unix::fs::MetadataExt が size を持っているという note が一緒に出ます。標準ライブラリの側も気になったので Vec に length() を呼んでみましたが、こちらは両方の版で len を勧めてきて、差はありませんでした。
半角カナの Debug 出力と、落ちたテスト
今回いちばん「やっておいてよかった」と思ったのがこれです。Libraries の項に「escape_debug_ext で U+FF9E と U+FF9F をエスケープしない」という1行があります。コードポイントだけ見るとピンとこないんですが、半角カナの濁点「゙」と半濁点「゚」です。
業務システムでは、古いシステムから来た半角カナのデータを扱うことがまだあります。そこで、{:?} の出力を文字列で固定したテストを、1.98.1 で通る形で書いてから 1.99.0 で回してみました。
// src/lib.rs(1.98.1 で通るように書いたテスト)
pub fn label() -> &'static str {
"ガギパ"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_output_is_stable() {
assert_eq!(format!("{:?}", label()), r#""カ\u{ff9e}キ\u{ff9e}ハ\u{ff9f}""#);
}
}
// 同じ文字列を println!("{:?}", s) した結果(別の小さなプログラムで確認)
1.98.1: "カ\u{ff9e}キ\u{ff9e}ハ\u{ff9f}"
1.99.0: "ガギパ"
// cargo +1.98.1 test
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
// cargo +1.99.0 test(終了コード 101)
assertion `left == right` failed
left: "\"ガギパ\""
right: "\"カ\\u{ff9e}キ\\u{ff9e}ハ\\u{ff9f}\""
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1.98.1 では、濁点と半濁点だけが \u{ff9e} の形にエスケープされていました。「カ」はそのままなので、半角カナ全体ではなく、この2文字だけの扱いです。1.99.0 では見たままの文字で出るようになり、私が書いたテストはきれいに落ちました。
正直なところ、このテスト自体が Debug の出力という「人が読むための表示」に依存していて、書き方として改善の余地があります。最初の書き方は表示の文字列を丸ごと固定するものでしたが、本来は label() が返す値そのものを比べるべきでした。ただ、ログの断片やエラーメッセージを丸ごと文字列で比べるテストは、現場では珍しくないと思っています。半角カナを含むデータを {:?} で出している箇所があれば、上げる前に一度テストを回しておくと安心です。
使い切った範囲の表示と、古い定数の書き方
もう一つ、エッジケースの話です。Libraries の項に「使い切った RangeInclusive(a..=b)の start() と end() は、これまでと違う値を返すことがある」とあります。これまでも保証された挙動ではないので破壊的変更ではない、という扱いです。
1..=3 を by_ref() で最後まで回してから、中身をのぞいてみました。あわせて Compatibility Notes にあった「古い整数モジュールの完全な非推奨化」も、std::i32::MAX と core::u8::MIN を書いて確かめています。
# 使い切った 1..=3 を表示(Debug / start / end / is_empty / next)
1.98.1: debug=3..=3 (exhausted) start=3 end=3 is_empty=true next=None
1.99.0: debug=4..=3 start=4 end=3 is_empty=true next=None
# 使い切った範囲で配列 [10, 20, 30, 40, 50] をスライス
1.98.1: slice_by_exhausted_range=Ok([])
1.99.0: slice_by_exhausted_range=Ok([])
# let a = std::i32::MAX; let b = core::u8::MIN;
1.98.1: 警告なし(compile_rc=0)
1.99.0: warning: use of deprecated constant `std::i32::MAX`: replaced by the `MAX` associated constant on this type
warning: use of deprecated constant `std::u8::MIN`: replaced by the `MIN` associated constant on this type
start() は 3 から 4 に変わり、Debug 表示からは「(exhausted)」という目印が消えました。is_empty() と next() の結果は同じで、このスライスもどちらの版も空でした。今思えば、使い切った範囲の start() を読むコードは書いたことがありません。ただ、ループを途中で抜けたあとの範囲を、デバッグ用にログへ出しているケースはありそうです。その場合、表示が 4..=3 になるので、読み手が一瞬混乱するかもしれません。
古い定数の方は、1.99.0 で警告が出るようになりました。面白いのは、core::u8::MIN と書いた行の警告が std::u8::MIN という名前で出ていたことです。直し方は警告の文のとおり、u8::MIN のような型の関連定数に置き換えるだけです。
Cargo 側:CI=1 と —profile debug
Cargo の項からは2つ試しました。一つは「環境変数 CI があるとき、incremental compilation を既定で無効にする」という変更です。CI のキャッシュに target/ を載せている人には、地味に効いてきそうです。
新規プロジェクトを条件ごとに1つずつ作り、cargo build 後の target/debug/incremental の中身を数えました。
| 条件 | incremental の中身 | du -sb の値 |
|---|---|---|
| 1.98.1・CI=1 | 1件 | 127,692 |
| 1.99.0・CI=1 | 0件(空のディレクトリだけ残る) | 4,096 |
| 1.99.0・CI なし | 1件 | 128,003 |
| 1.99.0・CI=1 と CARGO_INCREMENTAL=1 | 1件 | 127,664 |
1.99.0 で CI=1 を付けたときだけ、incremental の中身が空になりました。CARGO_INCREMENTAL=1 を明示すると、CI=1 のままでも元に戻ります。なお、hello world 程度では、ビルド時間の差は測れていません。
もう一つは、新しく組み込みになった debug プロファイルです。
$ cargo +1.98.1 build --profile debug
error: profile name `debug` is reserved
To configure the default development profile, use the name `dev` as in [profile.dev]
See https://doc.rust-lang.org/cargo/reference/profiles.html for more on configuring profiles.
rc=101
$ cargo +1.99.0 build --profile debug
Compiling prof_1_99_0 v0.1.0 (/work/prof_1_99_0)
Finished `debug` profile [unoptimized + debuginfo] target(s) in 0.08s
rc=0
1.98.1 では予約語として弾かれていた名前が、1.99.0 では通るようになりました。リリースノートによると、今のところ dev との違いはなく、dev を開発中の速い繰り返し向けに寄せていくための準備だそうです。CI の設定に --profile debug を書くと 1.98 系では落ちるので、版が混在するうちは使わない方が無難だと感じました。
期待したのに、差が出なかったもの
全部の項目で差が見えたわけではありません。Compiler の項に「すべての gnu と musl のターゲットで static_position_independent_executables を有効にする」とあったので、RUSTFLAGS='-C target-feature=+crt-static' で hello を静的リンクし、file コマンドで見比べました。
結果は、1.98.1 も 1.99.0 も「static-pie linked」でした(サイズは 5,476,624 バイトと 5,507,496 バイト)。少なくとも今回の x86_64-unknown-linux-gnu では、前の版から static-pie になっていたことになります。どのターゲットで挙動が変わったのかまでは、今回は確かめていません。
最後に、ここまでの差を一覧にしておきます。
| 試したこと | 1.98.1 | 1.99.0 |
|---|---|---|
| doc alias 付きのメソッドを別名で呼ぶ | 似た名前の sizes を提案 | alias を持つ count を提案 |
| 半角カナ「ガギパ」の Debug 出力 | 濁点・半濁点だけ \u{ff9e} などにエスケープ | そのまま表示(固定したテストは失敗) |
| 使い切った 1..=3 | 3..=3 (exhausted)、start()=3 | 4..=3、start()=4 |
| std::i32::MAX | 警告なし | deprecated の警告 |
| String::from_utf8_lossy_owned | E0658(unstable) | “a�b” を返す |
| CI=1 で cargo build | incremental あり | incremental なし |
| cargo build —profile debug | 予約語エラー(101) | 成功 |
| crt-static で hello をビルド | static-pie | static-pie(差なし) |
試してみての所感
コンテナの起動から片付けまでは73秒でした。2つの版を入れて同じコードを流すだけなら、思っていたよりずっと手軽です。
自分の現場に持ち帰るなら、まず半角カナを含む値を {:?} で文字列化しているテストやログを洗い出します。次に、CI で target/ をキャッシュしているジョブは、incremental が既定で切れる前提でキャッシュの中身を見直すつもりです。キャッシュする意味が薄れるのか、CARGO_INCREMENTAL=1 で戻すべきなのかは、プロジェクトの大きさ次第だと思います。私はまず実際のビルド時間を測ってから決めます。
doc alias は、これまで rustdoc の検索のためだけに書くものだと思っていました。コンパイラの提案にも効くとなると、社内ライブラリで「よく間違えられる名前」を alias に書いておく価値が出てきます。一方で --profile debug は、チームの版が揃うまで様子見です。リリースノートを読むだけで終わらせず、自分のコードで1行ずつ当ててみる。これだけで、落ちるテストに上げる前に気づけるのは大きいと感じました。