RustのUnsafeCellとFreeze
概要: UnsafeCell
の存在目的について説明する。
UnsafeCell
とは
UnsafeCell
とは、その名前の通り、内部可変性(interior mutability)を実現するためのunsafeはプリミティブである。
UnsafeCell
の代表的な用途は内部可変性を実現するための安全なラッパーであるCell
や RefCell
である。それらの他に、並行性を扱うモジュール(std::sync
, std::thread
, std::sys
)の内部で使われている。
UnsafeCell
自体は以下のように単なるラッパー構造体である。
pub struct UnsafeCell<T: ?Sized> { value: T, }
したがって UnsafeCell
の特異性はコンパイラによる特殊な扱いに由来することになる。
UnsafeCell
の扱い
UnsafeCell
は主に以下の点で特殊である。
T
に対して非変である。 (cf.PhantomData
は共変)- これをメンバとして含む型は
Freeze
フラグが外される。
Freeze
は core::marker
内に定義されているプライベートなマーカートレイトである。その定義は以下のようになっている。(1.17.0時点では Freeze
はまだ存在せず、コンパイラに組込みのフラグで管理されている。)
#[lang = "freeze"] unsafe trait Freeze {} unsafe impl Freeze for .. {} impl<T: ?Sized> !Freeze for UnsafeCell<T> {} unsafe impl<T: ?Sized> Freeze for PhantomData<T> {} unsafe impl<T: ?Sized> Freeze for *const T {} unsafe impl<T: ?Sized> Freeze for *mut T {} unsafe impl<'a, T: ?Sized> Freeze for &'a T {} unsafe impl<'a, T: ?Sized> Freeze for &'a mut T {}
impl Tr for .. {}
については過去の記事を参照。これにより、Freeze
はおよそ「UnsafeCell
をメンバとして持たない型」として定義されることになる。 ([T; 0]
という例外はある。)
Freeze
の扱い
Freeze
は主に次のような役割をもつ。
static
アイテムがmut
ではなく、かつ型がFreeze
である場合、このアイテムは.rodata
などの読み取り専用セクションに配置される場合がある。- 参照が
mut
ではなく、かつ参照先の型がFreeze
である場合、この参照は「エイリアスを持たない」かつ「読み取り専用」という情報がLLVMに渡される。
なお、「エイリアスを持たない」というのは正確にいうと全く正しくないが、LLVMの最適化フラグとしてのnoaliasは、「エイリアスによるデータ競合が発生しない」という意味であるため、問題ない。
逆に、真にエイリアスを持たないはずの &mut
にはnoaliasがついていない。これはLLVMの最適化バグへのワークアラウンドによるものである。
まとめ
mut
がついていないのにメモリ領域が書き換えられる可能性がある場合は、該当部分を UnsafeCell
でラップする必要がある。通常は UnsafeCell
は Cell
や RefCell
などのより高級なライブラリを経由して呼び出されるので、この内容をより具体的に把握する必要はない。