本日9/6(金)にリリースされたRust 1.81の変更点を詳しく紹介します。 もしこの記事が参考になれば記事末尾から活動を支援頂けると嬉しいです。
ピックアップ
個人的に注目する変更点を「ピックアップ」としてまとめました。 全ての変更点を網羅したリストは変更点リストをご覧ください。
no_std環境でもErrorトレイトが使えるようになった
これまでErrorトレイトはlibstd環境でのみ使えるものでしたが、Rust 1.81からは#![no_std]環境でも使えるようになりました。
組み込み環境でもエラーを扱いやすくなり、ライブラリでもlibstd環境とそれ以外で実装を切り替えずに済むようになります。嬉しいですね。
警告等が発生することを強制させられるようになった
式や文で警告やエラーなどのリントが発生することを強制させることができる#[expect]属性が使えるようになりました。
使い方は#[allow]や#[deny]と同じで、リントの名前を指定します。
#[allow]はリントが発生しても警告・エラーが出ないようにするものですが、
#[expect]はリントが発生しないとコンパイルエラーになります。
個人的には最近tokio::spawnに指定するタスクが終了しないことを明示するために使おうとしたことがあります。
rust-lang/rust#107953で紹介されているstatic_unreachable!()マクロで使われています。
macro_rules! static_unreachable { () => { #[deny(unfulfilled_lint_expectations)] #[expect(unreachable_code)] { unreachable!() } }; } tokio::spawn(async { loop { // 適当な処理 // break; } // ループがbreakすると次のエラーが出る // error: this lint expectation is unfulfilled static_unreachable!() // セミコロンは付けない });
ソートの実装が切り替わった
これまでは安定ソートにティムソート由来のアルゴリズムが、不安定ソートに[pdqsort]由来のアルゴリズムが使われていました。 Rust 1.81からは安定ソートはdriftsort由来に、不安定ソートはipnsort由来に切り替わりました。
置き換えのPRによると、実行時間とコンパイル時間、そしてバイナリサイズのバランスと取ったものだそうです。
なお、Ordが正しく全順序を実装していない、つまりa.cmp(b)とb.cmp(a)の結果が異なる場合にパニックする可能性があるため注意してください。
core::panic::PanicInfoとstd::panic::PanicInfoの互換性がなくなった
std::panic::PanicInfoがstd::panic::PanicHookInfoの別名となり、core::panic::PanicInfoとは互換性がなくなりました。
core::panic::PanicInfoは#[panic_handler]に渡されるもので、内部のパニック文言は形式化済みの文字列です。
一方std::panic::PanicInfoはstd::panic::set_hookで設定する関数に渡されるもので、
panic_anyにより文字列以外にも任意の値が入る可能性があります。
この2つは同じ役割のようでいて異なる性質を持っていたため、今回分離されることとなりました。
std::panic::PanicInfoを使っている場合は今後警告が出るようになるため置き換えが必要です。
extern "C"関数で未捕捉のパニックがプログラムを強制終了させるようになった
Rust 1.71ではパニックした際に呼び出し元に巻き戻しが伝播するextern "C-unwind"が追加されましたが、
extern "C"関数でパニックした際の挙動は未定義でした。
Rust 1.81からはextern "C"関数で捕捉されなかったパニックがある場合、プログラムが強制終了するようになりました。
extern "C"関数は慎重に使い、必要に応じてextern "C-unwind"へ置き換えるようにしましょう。
安定化されたAPIのドキュメント
安定化されたAPIのドキュメントを独自に訳して紹介します。リストだけ見たい方は安定化されたAPIをご覧ください。
core::error
めちゃくちゃ長いので割愛
hint::assert_unchecked
#[track_caller] #[inline(always)] #[doc(alias = "assume")] #[stable(feature = "hint_assert_unchecked", since = "1.81.0")] #[rustc_const_stable(feature = "hint_assert_unchecked", since = "1.81.0")] pub const unsafe fn assert_unchecked(cond: bool) { /* 実装は省略 */ }
condが成立するという健全性をコンパイラに約束させる。
これにより最適化の仕事が楽になるかもしれないが、 生成されるコードが遅くなる可能性もある。いずれにせよ、この関数を呼び出すとコンパイルが長くなる可能性が高い。
他ではllvm.assumeや
Cの__builtin_assumeで見たことがあるかもしれない。
これは正確性要件を健全性要件に昇格させるものである。よほどの理由がない限り、このようなことはすべきではない。
使い方
これは特定の状況下で使われる局所最適化のための手法であり、関数は何もしなくても良い。 使う場合はその価値を示す再現可能なベンチマークを書き、最適化がより 賢くなり不要になった際には関数の使用を削除することを想定すべきである。
条件が複雑になるほど、この関数が有効である可能性は低くなる。例えば、
assert_unchecked(foo.is_sorted())はコンパイラが活用できる可能性がない程度には複雑である。
また基本的な特性を示す必要もない。例えば、コンパイラはcount_onesの値域を知っているため、
let n = u32::count_ones(x); assert_unchecked(n <= u32::BITS);としてもメリットはない。
assert_uncheckedは論理的にはif !cond { unreachable_unchecked(); }と同等である。
assert_unchecked(false)と書きたくなったら、直接unreachable_unchecked()と書くべきである。
安全性
condはtrueでなくてはならない。この関数をfalseで呼び出すと即座に未定義動作を引き起こす。
サンプル
use core::hint; /// # 安全性 /// /// `p`は非NULLで有効でなければならない pub unsafe fn next_value(p: *const i32) -> i32 { // 安全性:呼び出し側の不変条件により`p`がNULLでないことは保証される unsafe { hint::assert_unchecked(!p.is_null()) } if p.is_null() { return -1; } else { // 安全性:呼び出し側の不変条件により`p`が有効であることは保証される unsafe { *p + 1 } } }
assert_unchecked抜きの上記関数では最適化ありでも次のコードを生成する
next_value: test rdi, rdi je .LBB0_1 mov eax, dword ptr [rdi] inc eax ret .LBB0_1: mov eax, -1 ret
assertを追加すると最適化での無駄な検査を省くことができる
next_value: mov eax, dword ptr [rdi] inc eax ret
この例は実際に使用されるものとは大きく異なる。検査と同じassertをすぐ隣に書くことは冗長であり、 ポインタの逆参照には非NULLという大前提がある。ただし挙動の関連性がそれほど明確でない場合でも、 最適化がどのような変更を加えられるかを示している。
fs::exists
#[stable(feature = "fs_try_exists", since = "1.81.0")] #[inline] pub fn exists<P: AsRef<Path>>(path: P) -> io::Result<bool> { /* 実装は省略 */ } }
パスが存在する実体を示す場合にOk(true)を返す。
この関数は宛先ファイルについて情報を問い合わせるためにシンボリックリンクを辿る。
壊れたシンボリックリンクの場合はOk(false)を返す。
Path::existsメソッドとは異なり、この関数はパスが存在するかしないかを検証した場合のみOk(true)またはOk(false)を返す。
存在を確認できない場合や拒否された場合はErr(_)が伝播される。例えば親ディレクトリの1つで一覧する権限が拒否されている場合などが該当する。
この関数はexists()メソッドのいくつかの落とし穴を避けるが、TOCTOUバグ(※訳注:検査時から使用時までの時間差によって生じるバグ)を防げるわけではない。
この関数はTOCTOUバグが問題にならないシナリオでのみ使用すべきである。
サンプル
use std::fs; assert!(!fs::exists("存在しない.txt").expect("存在しない.txtの存在を検査できなかった")); assert!(fs::exists("/root/ひみつ.txt").is_err());
AtomicBool::fetch_not
#[cfg(target_has_atomic_load_store = "8")] impl AtomicBool { #[inline] #[stable(feature = "atomic_bool_fetch_not", since = "1.81.0")] #[cfg(target_has_atomic = "8")] #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces pub fn fetch_not(&self, order: Ordering) -> bool { /* 実装は省略 */ } }
真偽値に対して論理否定を行う。
現在の値に対して論理否定を行い、新しい値を設定する。
古い値を返す。
fetch_notはこの操作のメモリ順序を表すOrdering引数を取る。
全ての順序モードが使用可能である。Acquireを使用するとこの操作のストア部分がRelaxedになり、
Releaseを使用するとロード部分がRelaxedになる。
注記:このメソッドはu8上での原子操作に対応するプラットフォームでのみ利用可能である。
サンプル
use std::sync::atomic::{AtomicBool, Ordering}; let hoge = AtomicBool::new(true); assert_eq!(hoge.fetch_not(Ordering::SeqCst), true); assert_eq!(hoge.load(Ordering::SeqCst), false); let hoge = AtomicBool::new(false); assert_eq!(hoge.fetch_not(Ordering::SeqCst), false); assert_eq!(hoge.load(Ordering::SeqCst), true);
Duration::abs_diff
impl Duration { #[stable(feature = "duration_abs_diff", since = "1.81.0")] #[rustc_const_stable(feature = "duration_abs_diff", since = "1.81.0")] #[rustc_allow_const_fn_unstable(const_option)] #[must_use = "this returns the result of the operation, \ without modifying the original"] #[inline] pub const fn abs_diff(self, other: Duration) -> Duration { /* 実装は省略 */ } }
selfとotherの絶対差を計算する。
サンプル
use std::time::Duration; assert_eq!(Duration::new(100, 0).abs_diff(Duration::new(80, 0)), Duration::new(20, 0)); assert_eq!(Duration::new(100, 400_000_000).abs_diff(Duration::new(110, 0)), Duration::new(9, 600_000_000));
IoSlice::advance
impl<'a> IoSlice<'a> { #[stable(feature = "io_slice_advance", since = "1.81.0")] #[inline] pub fn advance(&mut self, n: usize) { /* 実装は省略 */ } }
スライスの内部カーソルを進める。
複数のバッファのカーソルを進めるためにはIoSlice::advance_slicesを参照。
パニック
スライスの末尾を超えて進めようとするとパニックする。
サンプル
use std::io::IoSlice; use std::ops::Deref; let data = [1; 8]; let mut buf = IoSlice::new(&data); // 3バイトを既読にする buf.advance(3); assert_eq!(buf.deref(), [1; 5].as_ref());
IoSlice::advance_slices
impl<'a> IoSlice<'a> { #[stable(feature = "io_slice_advance", since = "1.81.0")] #[inline] pub fn advance_slices(bufs: &mut &mut [IoSlice<'a>], n: usize) { /* 実装は省略 */ } }
スライスのスライスを進める。
完全に進められたIoSliceを削除してスライスを縮小する。
IoSliceのカーソルが途中にある場合、そのカーソルから開始されるように変更される。
例えば8バイトのIoSlice2つのスライスがある場合に10バイト進めると、
2バイト進められた2番目のIoSliceのみが含まれる結果になる。
パニック
スライスの末尾を超えて進めようとするとパニックする。
サンプル
use std::io::IoSlice; use std::ops::Deref; let buf1 = [1; 8]; let buf2 = [2; 16]; let buf3 = [3; 8]; let mut bufs = &mut [ IoSlice::new(&buf1), IoSlice::new(&buf2), IoSlice::new(&buf3), ][..]; // 10バイトを書き込み済みにする IoSlice::advance_slices(&mut bufs, 10); assert_eq!(bufs[0].deref(), [2; 14].as_ref()); assert_eq!(bufs[1].deref(), [3; 8].as_ref());
IoSliceMut::advance
impl<'a> IoSliceMut<'a> { #[stable(feature = "io_slice_advance", since = "1.81.0")] #[inline] pub fn advance(&mut self, n: usize) { /* 実装は省略 */ } }
スライスの内部カーソルを進める。
複数のバッファのカーソルを進めるためにはIoSliceMut::advance_slicesを参照。
パニック
スライスの末尾を超えて進めようとするとパニックする。
サンプル
use std::io::IoSliceMut; use std::ops::Deref; let mut data = [1; 8]; let mut buf = IoSliceMut::new(&mut data); // 3バイトを既読にする buf.advance(3); assert_eq!(buf.deref(), [1; 5].as_ref());
IoSliceMut::advance_slices
impl<'a> IoSliceMut<'a> { #[stable(feature = "io_slice_advance", since = "1.81.0")] #[inline] pub fn advance_slices(bufs: &mut &mut [IoSliceMut<'a>], n: usize) { /* 実装は省略 */ } }
スライスのスライスを進める。
完全に進められたIoSliceMutを削除してスライスを縮小する。
IoSliceMutのカーソルが途中にある場合、そのカーソルから開始されるように変更される。
例えば8バイトのIoSliceMut2つのスライスがある場合に10バイト進めると、
2バイト進められた2番目のIoSliceMutのみが含まれる結果になる。
パニック
スライスの末尾を超えて進めようとするとパニックする。
サンプル
use std::io::IoSliceMut; use std::ops::Deref; let mut buf1 = [1; 8]; let mut buf2 = [2; 16]; let mut buf3 = [3; 8]; let mut bufs = &mut [ IoSliceMut::new(&mut buf1), IoSliceMut::new(&mut buf2), IoSliceMut::new(&mut buf3), ][..]; // 10バイトを既読にする IoSliceMut::advance_slices(&mut bufs, 10); assert_eq!(bufs[0].deref(), [2; 14].as_ref()); assert_eq!(bufs[1].deref(), [3; 8].as_ref());
PanicHookInfo
#[stable(feature = "panic_hook_info", since = "1.81.0")] #[derive(Debug)] pub struct PanicHookInfo<'a> { /* 定義は省略 */ }
パニックに関する情報を提供する構造体。
PanicHookInfo構造体はset_hook関数で設定されたパニックフックに渡される。
サンプル
use std::panic; panic::set_hook(Box::new(|panic_info| { println!("パニック発生:{panic_info}"); })); panic!("致命的なシステム障害");
PanicInfo::message
impl<'a> PanicInfo<'a> { #[must_use] #[stable(feature = "panic_info_message", since = "1.81.0")] pub fn message(&self) -> PanicMessage<'_> { /* 実装は省略 */ } }
panic!マクロに渡されたメッセージ。
サンプル
このメソッドの戻り値はDisplayを実装しているため、write!()などのマクロに直接渡すことができる。
#[panic_handler] fn panic_handler(panic_info: &PanicInfo<'_>) -> ! { write!(DEBUG_OUTPUT, "パニック:{}", panic_info.message()); loop {} }
PanicMessage
#[stable(feature = "panic_info_message", since = "1.81.0")] pub struct PanicMessage<'a> { /* 定義は省略 */ }
panic!()マクロに渡されたメッセージ。
この型のDisplay実装はpanic!()マクロに渡された引数でメッセージを形式化する。
PanicInfo::messageメソッドも参照。
変更点リスト
言語
extern "C"関数内で捕捉されないパニックが発生した場合に強制終了- 省略されたselfのライフタイム内にある複数の
&の曖昧性を修正 - リント用の
#[expect]を安定化(RFC 2383)。これは#[allow]と似ているが、指定されたリントが満たされない場合に警告を発する - メソッド解決を変更し、メソッド候補を拒絶するのではなく、隠れた型を制約するようにした
elided_lifetimes_in_associated_constantを拒否に引き上げoffset_from:ポインタが同一のアドレスを指すことを常に許容- トレイトシステムにおいて型の絞り込み中に不透明型に制約することを許容
- 様々なサイズ不定型へのキャスト時に不透明型に制約することを許容
- 予約語を使ったライフタイムをマクロ展開前に拒絶
コンパイラ
- ポインタからトレイトオブジェクトへの型変換をより厳格化
- 脱出する束縛変数がある場合でも別名引数の正当性を検査
- 効果のないコード生成オプション
-Cinline-threshold=...を非推奨化 - 型の大きさに基づく制限を再実装
transmuteの大きさ検査でアライメントを適切に考慮- リント
box_pointersを削除 - インタプリタ実行時、bool・charが型変換で使用される際にその妥当性を確認
- 入れ子項目を含む関数のコード網羅率計測を改善
- ターゲットの変更
- ティア3の
no_stdなXtensaターゲットを追加:xtensa-esp32-none-elf・xtensa-esp32s2-none-elf・xtensa-esp32s3-none-elf - ティア3の
stdなXtensaターゲットを追加:xtensa-esp32-espidf・xtensa-esp32s2-espidf・xtensa-esp32s3-espidf - ティア3のi686向けRedox OSターゲットを追加:
i686-unknown-redox arm64ec-pc-windows-msvcをティア2に昇格wasm32-wasip2をティア2に昇格loongarch64-unknown-linux-muslをホストツール付きティア2に昇格- LoongArch向けLinuxターゲットに完全なツールとプロファイラを有効化
wasm32-wasiの使用について常に警告を出す(下記の互換性メモを参照)- Rustのティア付けされたプラットフォーム対応の詳細はPlatform Supportのページ(※訳注:英語)を参照
- ティア3の
ライブラリ
- core向け
PanicInfoとstd向けPanicInfoを分割(下記の互換性メモを参照) {Rc,Arc}::make_mut()をサイズ不定型に一般化- ソートの実装を、安定版は
driftsortに、不安定版はipnsortに置き換え。すべてのslice::sort*とslice::select_nth*メソッドで大幅な性能向上が期待される。詳細は研究プロジェクト(※訳注:英語ページ)を参照 create_dir_allが空のパスを尊重する動作を文書化- 複数のスレッドが同時にパニックした際、既定のパニック処理が出力を混在させるのを修正
- ファイル名末尾に空白やピリオドがある場合、
Commandでのバッチファイルへの引数のエスケープが機能しない問題を修正(CVE-2024-43402)
安定化されたAPI
core::errorhint::assert_uncheckedfs::existsAtomicBool::fetch_notDuration::abs_diffIoSlice::advanceIoSlice::advance_slicesIoSliceMut::advanceIoSliceMut::advance_slicesPanicHookInfoPanicInfo::messagePanicMessage
以下のAPIが定数文脈で使えるようになった。
char::from_u32_unchecked(function)char::from_u32_unchecked(method)CStr::count_bytesCStr::from_ptr
Cargo
--allow-dirtyが指定された場合でも生成された.cargo_vcs_info.jsonが常に含まれるようになった- パッケージ生成時、存在しないファイルを示す
package.license-fileとpackage.readmeを阻却 --profileフラグと同時に--release・--debugフラグを指定することを阻却Cargo.tomlでのlib.pluginキーへの対応を削除。Rustプラグイン機能は4年以上も非推奨であり1.75.0で削除済み
互換性メモ
コンパイラが
wasm32-wasiターゲットの使用に警告を発するようになり、代わりにwasm32-wasip1ターゲットへの切り替えを求められる。どちらのターゲットも同一でありwasm32-wasiが改名されただけである。このWASIターゲットへの変更は2025年1月にwasm32-wasiを削除するために行われているstd::panic::PanicInfoをstd::panic::PanicHookInfoに改名した。古い名前は別名として引き続き機能するものの、Rust 1.82.0からは非推奨の警告が発されるようになる。core::panic::PanicInfoは変わらないままであるが別の型となった。これは、これらの型は異なる役割を持つからである。
std::panic::PanicHookInfoは(パニックが任意のデータを伴う可能性がある)std環境におけるパニック時のフックへの引数であり、一方core::panic::PanicInfoは(パニックが形式化済み文言のみを伴う)no_std環境における#[panic_handler]への引数である。これらの型を分離することにより、std::panic::PanicHookInfo::payload_as_str()やcore::panic::PanicInfo::message()のような、より便利なメソッドを追加できるようになる。
- 新しいソートの実装では、
Ordの実装(または指定された比較関数)が、トレイトが要求する全順序を実装していない場合にパニックする可能性がある。Ordの親トレイト(PartialOrdやEq、及びPartialEq)も一貫している必要がある。以前の実装が問題に「気付く」ことはなかったが、新しい実装では矛盾を検出する可能性が高く、故意に未ソートなデータを返すのではなくパニックを引き起こす。 - トレイト解決の内部評価順序の変更により、ごくまれに新しく致命的なオーバーフローエラーを引き起こす可能性がある
内部の変更
これらの変更がユーザーに直接利益をもたらすわけではないものの、コンパイラ及び周辺ツール内部では重要なパフォーマンス改善をもたらす。
関連リンク
さいごに
次のリリースのRust 1.82は10/18(金)にリリースされる予定です。 Rust 1.82では非安全属性や存在型でのライフタイムの指定、また最低限の網羅的パターンが使えるようになる予定です。