【Rust】不変参照から可変参照に変換する(未定義動作ではない(けど安全でもない))方法

2023-03-20

Rustで参照から可変参照を拝借するにはワイルドに unsafeを使うことになるんだけど(悪かった自慢)、Clippyにかけたら未定義動作だと言って怒られたので修正する。

未定義動作版

以前状態管理や相互参照をどうするかで行ったpeep関数:

pub unsafe fn peep<'a, T: ?Sized>(t: &T) -> &'a mut T {
&mut *(t as *const T as *mut T)
}

をClippyにかけるとエラーが出る:

error: casting `&T` to `&mut T` may cause undefined behavior, consider instead using an `UnsafeCell`
--> unsafe_util.rs:2:10
|
2 | &mut *(t as *const T as *mut T)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_ref_to_mut

&T から &mut T へのキャストは未定義動作なので、代わりにUnsafeCellを使え、と言われている。

UnsafeCell経由版

UnsafeCell を使えとのことだけど selfの可変参照を(コンパイラを騙して)取得したいので、UnsafeCell として保持しておくわけにはいかなかった。 なので参照→UnsafeCell→可変参照とキャストしてやる:

use std::cell::UnsafeCell;

pub unsafe fn peep<'a, T>(t: &T) -> &'a mut T {
get_mut(get_shared(t))
}

// Safety: the caller must ensure that there are no references that
// point to the *contents* of the `UnsafeCell`.
unsafe fn get_mut<'a, T>(ptr: &UnsafeCell<T>) -> &'a mut T {
unsafe { &mut *ptr.get() }
}

fn get_shared<T>(ptr: &T) -> &UnsafeCell<T> {
let t = ptr as *const T as *const UnsafeCell<T>;
// SAFETY: `T` and `UnsafeCell<T>` have the same memory layout
unsafe { &*t }
}

コードは UnsafeCell in std::cell - Rust のメモリレイアウトの節のget_mut()get_shared()を参考にした。

UnsafeCell<T> はその内部型Tと同じメモリ内表現を持ちます。 この保証の結果、TからUnsafeCell<T>への変換が可能になります。

共有の UnsafeCell<T>の中身への *mut Tポインタを獲得する唯一の有効な方法は .get().raw_get() を使用する方法だということに注意してください。 &mut T参照を獲得するには、このポインタをデリファレンスするか、 排他的な UnsafeCell<T>に対する.get_mut()を呼び出すかのどちらかです。

本来のUnsafeCellの使い方は?

正確に理解できているとは言い難いんだけど上記でコンパイラやClippyに怒られないのは騙しているだけで、競合による危険性は変わりはない(メモリレイアウトが保証されてるので未定義動作ではないという程度で)。

UnsafeCellは本来は「内部可変性」、外には不変に見せる(なので不変参照で複数に共有できる)けど内部では内容を書き換える、という用途に使用する。 例えばlet t: UnsafeCell<T> = ...;というふうに所有、不変の借用&UnsafeCell<T>が共有できて、その上で内部可変性で裏で&mut Tに変換して内容を書き換える、ということが未定義動作ではなくできるよ、ということだと思う (その上で安全性は自分で保証する必要がある)。

let t = UnsafeCell::new(0i32);

// 不変参照は複数可能
let p = &t;
let q = &t;

// 内部可変性を利用して書き換える
unsafe { *get_mut(p) += 123 };

// 他方にも反映される(実体は同じなので)
unsafe { *q.get() } //=> 123

まあ&UnsafeCellで受け渡すのもアレなので構造体struct Foo{}などで包んだ&Foo で受け渡して、 impl Foo{} 内部のメソッドで密かに内部を変更するというのが筋だろうと思う:

struct Foo {
t: UnsafeCell<i32>,
}

impl Foo {
fn new(t: i32) -> Self {
Foo { t: UnsafeCell::new(t) }
}

fn inner_add(&self, v: i32) {
unsafe { *get_mut(&self.t) += v };
}

fn get(&self) -> i32 {
unsafe { *self.t.get() }
}
}

let foo = Foo::new(0);
foo.inner_add(123);
foo.get() //=> 123

さらにはCellRefCellを使って、UnsafeCellは使わないべきではある。