本日12/12(金)にリリースされたRust 1.92の変更点を詳しく紹介します。 もしこの記事が参考になれば記事末尾から活動を支援頂けると嬉しいです。 はてブやTwitterのいいね・RTも嬉しいです。
この記事は原文の理解や和訳のために一部生成AIを使用していますが、すべて筆者の考えに基づく文章で構成しており、 漫然と生成AIを使用しているものではありません。
- ピックアップ
- 安定化されたAPIのドキュメント
- 変更点リスト
- 関連リンク
- さいごに
- ライセンス表記
ピックアップ
個人的に注目する変更点、及び公式ブログで紹介されている内容を「ピックアップ」としてまとめました。 全ての変更点を網羅したリストは変更点リストをご覧ください。
RwLockの書き込みロックを読み込みロックに降格できるようになった
RwLock::write()を呼び出した結果の[RwLockWriteGuard]は、長期間保持していると他スレッドでのRwLock::read()をブロックしてしまいます。
そのため基本的には最低限の処理期間だけ保持しておきすぐにdropすべきです。
ただ、書き込んだあとに読み込む必要がある場合は別途RwLock::read()を呼び出す必要があるため、面倒で非効率でした。
Rust 1.92で[RwLockWriteGuard::downgrade]が使えるようになり、
ロックを取得したままRwLockReadGuardに降格させることができるようになりました。
もちろんこれを呼び出すとRwLock::read()の呼び出しはすぐに制御を返すようになります。
この機能のためにparking_lotクレートを使用していた場合は標準ライブラリだけで完結できるようになり便利ですね。
ヒープへゼロ初期化できるようになった
C言語のcallocのように、メモリをヒープに確保しつつゼロで初期化することができるようになりました。
これまではBox::uninitなどで確保したあとに明示的にゼロを書き込む必要がありました。
Rust 1.92からはBoxやRc、Arcでnew_zeroed及びnew_zeroed_sliceで直接ゼロ初期化することができます。
プラットフォームによっては自分でゼロを書き込むよりもコストが安いこともあるため、
パフォーマンスを気にするプログラムでは使うことを検討しても良いでしょう。
なお戻り値はBox<MaybeUninit<T>>の形であり、Box<T>の形にするにはunsafeであるassume_initの呼び出しが必要です。
never型の拒否リント
never型(!型)の安定化に向け作業が続いています。
Rust 1.92では前方互換性のためのリントnever_type_fallback_flowing_into_unsafeとdependency_on_unit_never_type_fallbackが
既定で拒否されるようになり、それらが検出された場合はエラーとなります。
これらは依然としてリントであるため#[allow]することもできますし、依存クレートのエラーは報告されません。
これらのリントが検出したコードはnever型が安定化した場合は壊れることになるため、出来るだけ修正するようにしてください。
このリントの影響を受けるクレートは500個に及ぶと想定されます。 とは言えこれは破壊的変更ではなく将来的にnever型を安定化するためのものであるため許容範囲と考えられます。
unused_must_useがResult<(), UninhabitedType>で警告されなくなった
unused_must_useは関数やその戻り型に#[must_use]を付けることで、関数の戻り値を無視しないよう警告するためのものです。
例えば戻り型がResultの場合は?や.expect("...")などを使って戻り値を無視しないよう教えてくれます。
しかしResultを返す一部関数において、そのエラー型が実際には「非居住(uninhabited)」、
つまりその型のいかなる値も構築出来ない(!やInfallibleなど)場合があります。
Rust 1.92からはリントunused_must_useがResult<(), UninhabitedType>やControlFlow<UninhabitedType, ()>では警告を出さなくなりました。
具体的にはResult<(), Infallible>は警告されません。これによりエラーが発生しない場合のチェックを回避できます。
use core::convert::Infallible; fn can_never_fail() -> Result<(), Infallible> { // ... Ok(()) } fn main() { can_never_fail(); }
主にエラーを関連型に持つトレイトのパターンではInfallibleをエラー型とする場合に有用です。
trait UsesAssocErrorType { type Error; fn method(&self) -> Result<(), Self::Error>; } struct CannotFail; impl UsesAssocErrorType for CannotFail { type Error = core::convert::Infallible; fn method(&self) -> Result<(), Self::Error> { Ok(()) } } struct CanFail; impl UsesAssocErrorType for CanFail { type Error = std::io::Error; fn method(&self) -> Result<(), Self::Error> { Err(std::io::Error::other("何か変")) } } fn main() { CannotFail.method(); // 警告なし CanFail.method(); // 警告:unused `Result` that must be used }
Linuxで-Cpanic=abortが有効な際に巻き戻しテーブルを出力
Rust 1.22以前は-Cpanic=abort時でもバックトレースが動作していましたが、
Rust 1.23以降は壊れており巻き戻しテーブルが出力されていませんでした。
この回避策としてRust 1.45では-Cforce-unwind-tables=yesが導入されました。
Rust 1.92では-Cpanic=abort指定時でも既定で巻き戻しテーブルを出力するようになり、
バックトレースが正しく動作するようになりました。
巻き戻しテーブルが不要な場合は-Cforce-unwind-tables=noにより明示的に出力を無効化してください。
#[macro_export]の入力を検証
最近のリリースではコンパイラでの組み込み属性の処理方法に関して多くの変更が入っています。 組み込み属性に関するエラーメッセージと警告が大幅に改善され、100以上の組み込み属性において診断結果の一貫性が向上するはずです。
安定化されたAPIのドキュメント
安定化されたAPIのドキュメントを独自に訳して紹介します。リストだけ見たい方は安定化されたAPIをご覧ください。
NonZero<u{N}>::div_ceil
impl NonZero<u8> { #[stable(feature = "unsigned_nonzero_div_ceil", since = "1.92.0")] #[rustc_const_stable( feature = "unsigned_nonzero_div_ceil", since = "1.92.0" )] #[must_use = "this returns the result of the operation, \ without modifying the original"] #[inline] pub const fn div_ceil(self, rhs: Self) -> Self { /* 実装は省略 */ } }
selfをrhsで割った値を計算し、結果を正の無限大方向に切り上げる。
結果は必ずゼロ以外になる。
サンプル
let one = NonZero::new(1u8).unwrap(); let max = NonZero::new(u8::MAX).unwrap(); assert_eq!(one.div_ceil(max), one); let two = NonZero::new(2u8).unwrap(); let three = NonZero::new(3u8).unwrap(); assert_eq!(three.div_ceil(two), two);
use std::num::NonZero; let one = NonZero::new(1u8).unwrap(); let max = NonZero::new(u8::MAX).unwrap(); assert_eq!(one.div_ceil(max), one); let two = NonZero::new(2u8).unwrap(); let three = NonZero::new(3u8).unwrap(); assert_eq!(three.div_ceil(two), two);
Location::file_as_c_str
impl<'a> Location<'a> { #[must_use] #[inline] #[stable(feature = "file_with_nul", since = "1.92.0")] #[rustc_const_stable(feature = "file_with_nul", since = "1.92.0")] pub const fn file_as_c_str(&self) -> &'a CStr { /* 実装は省略 */ } }
元ファイルの名前をヌル終端のCStrとして返す。
CやC++の__FILE__やstd::source_location::file_nameなど、
ヌル終端のconst char*を期待するAPIとの連携に役立つ。
RwLockWriteGuard::downgrade
impl<'rwlock, T: ?Sized> RwLockWriteGuard<'rwlock, T> { #[stable(feature = "rwlock_downgrade", since = "1.92.0")] pub fn downgrade(s: Self) -> RwLockReadGuard<'rwlock, T> { /* 実装は省略 */ } }
書き込みロックされたRwLockWriteGuardから、読み込みロックされたRwLockReadGuardに格下げする。
RwLockWriteGuardがある時点でRwLockは既に書き込み用にロックされているため、この操作は失敗しない。
格下げ後、他の場所でもデータの読み取りが出来るようになる。
サンプル
downgradeはRwLockWriteGuardの所有権を受け取り、RwLockReadGuardを返す。
use std::sync::{RwLock, RwLockWriteGuard}; let rw = RwLock::new(0); let mut write_guard = rw.write().unwrap(); *write_guard = 42; let read_guard = RwLockWriteGuard::downgrade(write_guard); assert_eq!(42, *read_guard);
downgradeはRwLockの状態を排他的モードから共有モードへと原子的に変更する。このため、格下げを行った直後に他の書き込みスレッドが割り込んでデータを書き換えることはできない。
use std::sync::{Arc, RwLock, RwLockWriteGuard}; let rw = Arc::new(RwLock::new(1)); // ロックを書き込みモードにする let mut main_write_guard = rw.write().unwrap(); let rw_clone = rw.clone(); let evil_handle = std::thread::spawn(move || { // メインスレッドが`main_read_guard`をドロップするまで戻らない let mut evil_guard = rw_clone.write().unwrap(); assert_eq!(*evil_guard, 2); *evil_guard = 3; }); *main_write_guard = 2; // 原子的に書き込みガードを読み込みガードに格下げする let main_read_guard = RwLockWriteGuard::downgrade(main_write_guard); // `downgrade`は原子的であるため、書き込みスレッドが保護されたデータを変更していることはありえない assert_eq!(*main_read_guard, 2, "`downgrade` was not atomic");
use std::sync::{Arc, RwLock, RwLockWriteGuard};
let rw = Arc::new(RwLock::new(1));
// ロックを書き込みモードにする
let mut main_write_guard = rw.write().unwrap();
let rw_clone = rw.clone();
let evil_handle = std::thread::spawn(move || {
// メインスレッドが`main_read_guard`をドロップするまで戻らない
let mut evil_guard = rw_clone.write().unwrap();
assert_eq!(*evil_guard, 2);
*evil_guard = 3;
});
*main_write_guard = 2;
// 原子的に書き込みガードを読み込みガードに格下げする
let main_read_guard = RwLockWriteGuard::downgrade(main_write_guard);
// `downgrade`は原子的であるため、書き込みスレッドが保護されたデータを変更していることはありえない
assert_eq!(*main_read_guard, 2, "`downgrade` was not atomic");
drop(main_read_guard);
evil_handle.join().unwrap();
let final_check = rw.read().unwrap();
assert_eq!(*final_check, 3);
Box::new_zeroed
impl<T> Box<T> { #[cfg(not(no_global_oom_handling))] #[inline] #[stable(feature = "new_zeroed_alloc", since = "1.92.0")] #[must_use] pub fn new_zeroed() -> Box<mem::MaybeUninit<T>> { /* 実装は省略 */ } }
中身の初期化されていない新しいBox型を生成する。メモリは0で埋められる。
このメソッドの正しい使い方と誤った使い方についてはMaybeUninit::zeroedの例を参照。
サンプル
let zero = Box::<u32>::new_zeroed(); let zero = unsafe { zero.assume_init() }; assert_eq!(*zero, 0)
Box::new_zeroed_slice
impl<T> Box<[T]> { #[cfg(not(no_global_oom_handling))] #[stable(feature = "new_zeroed_alloc", since = "1.92.0")] #[must_use] pub fn new_zeroed_slice(len: usize) -> Box<[mem::MaybeUninit<T>]> { /* 実装は省略 */ } }
中身の初期化されていない新しいBox型のスライスを生成する。メモリは0で埋められる。
このメソッドの正しい使い方と誤った使い方についてはMaybeUninit::zeroedの例を参照。
サンプル
let values = Box::<[u32]>::new_zeroed_slice(3); let values = unsafe { values.assume_init() }; assert_eq!(*values, [0, 0, 0])
Rc::new_zeroed
impl<T> Rc<T> { #[cfg(not(no_global_oom_handling))] #[stable(feature = "new_zeroed_alloc", since = "1.92.0")] #[must_use] pub fn new_zeroed() -> Rc<mem::MaybeUninit<T>> { /* 実装は省略 */ } }
中身の初期化されていない新しいRc型を生成する。メモリは0で埋められる。
このメソッドの正しい使い方と誤った使い方についてはMaybeUninit::zeroedの例を参照。
サンプル
use std::rc::Rc; let zero = Rc::<u32>::new_zeroed(); let zero = unsafe { zero.assume_init() }; assert_eq!(*zero, 0)
Rc::new_zeroed_slice
impl<T> Rc<[T]> { #[cfg(not(no_global_oom_handling))] #[stable(feature = "new_zeroed_alloc", since = "1.92.0")] #[must_use] pub fn new_zeroed_slice(len: usize) -> Rc<[mem::MaybeUninit<T>]> { /* 実装は省略 */ } }
中身の初期化されていない新しい参照カウント型スライスを生成する。メモリは0で埋められる。
このメソッドの正しい使い方と誤った使い方についてはMaybeUninit::zeroedの例を参照。
サンプル
use std::rc::Rc; let values = Rc::<[u32]>::new_zeroed_slice(3); let values = unsafe { values.assume_init() }; assert_eq!(*values, [0, 0, 0])
Arc::new_zeroed
impl<T> Arc<T> { #[cfg(not(no_global_oom_handling))] #[inline] #[stable(feature = "new_zeroed_alloc", since = "1.92.0")] #[must_use] pub fn new_zeroed() -> Arc<mem::MaybeUninit<T>> { /* 実装は省略 */ } }
中身の初期化されていない新しいArc型を生成する。メモリは0で埋められる。
このメソッドの正しい使い方と誤った使い方についてはMaybeUninit::zeroedの例を参照。
サンプル
use std::sync::Arc; let zero = Arc::<u32>::new_zeroed(); let zero = unsafe { zero.assume_init() }; assert_eq!(*zero, 0)
Arc::new_zeroed_slice
impl<T> Arc<[T]> { #[cfg(not(no_global_oom_handling))] #[inline] #[stable(feature = "new_zeroed_alloc", since = "1.92.0")] #[must_use] pub fn new_zeroed_slice(len: usize) -> Arc<[mem::MaybeUninit<T>]> { /* 実装は省略 */ } }
中身の初期化されていない新しい原子的参照カウント型スライスを生成する。メモリは0で埋められる。
このメソッドの正しい使い方と誤った使い方についてはMaybeUninit::zeroedの例を参照。
サンプル
use std::sync::Arc; let values = Arc::<[u32]>::new_zeroed_slice(3); let values = unsafe { values.assume_init() }; assert_eq!(*values, [0, 0, 0])
btree_map::Entry::insert_entry
impl<'a, K: Ord, V, A: Allocator + Clone> Entry<'a, K, V, A> { #[inline] #[stable(feature = "btree_entry_insert", since = "1.92.0")] pub fn insert_entry(self, value: V) -> OccupiedEntry<'a, K, V, A> { /* 実装は省略 */ } }
エントリーの値を設定してOccupiedEntryを返す。
サンプル
use std::collections::BTreeMap; let mut map: BTreeMap<&str, String> = BTreeMap::new(); let entry = map.entry("遊園地").insert_entry("わーい".to_string()); assert_eq!(entry.key(), &"遊園地");
btree_map::VacantEntry::insert_entry
impl<'a, K: Ord, V, A: Allocator + Clone> VacantEntry<'a, K, V, A> { #[stable(feature = "btree_entry_insert", since = "1.92.0")] pub fn insert_entry(mut self, value: V) -> OccupiedEntry<'a, K, V, A> { /* 実装は省略 */ } }
VacantEntryのキーに値を設定してOccupiedEntryを返す。
サンプル
use std::collections::BTreeMap; use std::collections::btree_map::Entry; let mut map: BTreeMap<&str, u32> = BTreeMap::new(); if let Entry::Vacant(o) = map.entry("遊園地") { let entry = o.insert_entry(37); assert_eq!(entry.get(), &37); } assert_eq!(map["遊園地"], 37);
変更点リスト
言語
MaybeUninitの表現と有効性について文書化- 安全なコードでの共用体フィールドに対する
&raw [mut | const]を許容 - 自動トレイトや
Sizedに対し、where句による境界よりも関連型の項目境界を優先して適用するようになった [X; 0]においてXが定数でサイズ不定型の場合、Xを実体化しないようになった#[track_caller]と#[no_mangle]の組み合わせに対応(各宣言で#[track_caller]の指定が必要)- never型に関する警告
never_type_fallback_flowing_into_unsafeとdependency_on_unit_never_type_fallbackを既定拒否化 - トレイトオブジェクトを除き、同じ関連項目に対して複数の境界を指定できるようになった
- 一貫性(coherence)における高階領域の扱いを少し強化した
Result<(), Uninhabited>(例えばResult<(), !>)やControlFlow<Uninhabited, ()>に対してunused_must_use警告が出なくなった。起こり得ないエラーを確認する必要がなくなる
コンパイラ
mips64el-unknown-linux-muslabi64で動的にリンクするようにした- PDBにコマンドライン引数を埋め込むための現行コードを削除 コマンドライン情報は通常デバッグツールには不要であり、今回削除したコードはPDBのデバッグ情報を使わないターゲットでもインクリメンタルビルドに問題を引き起こしていた
ライブラリ
iter::Repeat::lastとcountは無限に繰り返すのではなくパニックするようになった。
安定化されたAPI
NonZero<u{N}>::div_ceilLocation::file_as_c_strRwLockWriteGuard::downgradeBox::new_zeroedBox::new_zeroed_sliceRc::new_zeroedRc::new_zeroed_sliceArc::new_zeroedArc::new_zeroed_slicebtree_map::Entry::insert_entrybtree_map::VacantEntry::insert_entryimpl Extend<proc_macro::Group> for proc_macro::TokenStreamimpl Extend<proc_macro::Literal> for proc_macro::TokenStreamimpl Extend<proc_macro::Punct> for proc_macro::TokenStreamimpl Extend<proc_macro::Ident> for proc_macro::TokenStream
以下のAPIが定数文脈で使えるようになった。
Cargo
Rustdoc
- トレイト項目がrustdoc検索に現れた場合、その実装メソッドは非表示。以前は「last」で検索すると、
Iterator::lastに加えstd::vec::IntoIter::lastのような実装メソッドの両方が表示されていた。これらの実装メソッドは非表示化され、BTreeSet::lastなど固有メソッドにより多くの枠を割けるようになる
- 検索で使える識別子に関する規則を緩和。以前はRustコード中で有効な識別子しか検索できなかったが、識別子の一部として有効であれば検索できるようになった。例えば数字で始まる検索も行える
互換性メモ
- Linuxにおいて、既定で巻き戻しテーブルを生成することで
-C panic=abort指定時のバックトレースを修正。巻き戻しテーブルを除外したままにしたい場合は-C force-unwind-tables=noを指定してビルドすること
- コンパイラ組み込み属性とその診断向けである大掛かりな整理の一環として、将来の互換性に関する警告
invalid_macro_export_argumentsを既定拒否に昇格し、依存関係でも報告するようになった
関連リンク
さいごに
次のリリースのRust 1.93は1/23(金)にリリースされる予定です。
Rust 1.93ではスライスを配列化するメソッドが使えるようになったり、
VecDequeの先頭または末尾から条件に一致する場合だけ値を取り除くメソッドが使えるようになったりするようです。