Stacked Borrows のテスト
前セクションの Rust 向け(簡略化された)メモリモデルの TL;DR:
- Rust は概念的には「借用スタック」を維持することで再借用を扱います
- スタックの一番上にあるものだけが「live」(排他的アクセスを持つ状態)です
- より下にあるものにアクセスすると、それが「live」になり、その上にあるものは pop されます
- 借用スタックから pop されたポインターを使うことは許されません
- 借用チェッカーは、安全なコードがこれに従うことを保証します
- Miri は理論上、実行時に生ポインターがこれに従うことをチェックします
ここまで理論や考え方がたくさん出てきましたが、この本の真の核心に進みましょう。つまり、悪いコードを書いて、ツールに怒鳴らせることです。これから 大量 の例を見て、私たちのメンタルモデルが理にかなっているかを確認し、Stacked Borrows の直感をつかんでいきます。
ナレーター: 実践において未定義動作を捕まえるのは厄介な仕事です。結局のところ、コンパイラーが文字どおり「起こらない」と 仮定している 状況を扱っているからです。
運がよければ、今日のところは「動いているように見える」でしょう。しかし、それはより賢いコンパイラーやコードへのわずかな変更に対する時限爆弾になります。本当に 運がよければ、確実にクラッシュしてくれるので、単に間違いを見つけて修正できます。しかし運が悪ければ、奇妙で不可解な形で壊れます。
Miri は、rustc が持つプログラムの最も素朴で最適化されていない見方を取得し、解釈しながら追加の状態を追跡することで、これに対処しようとします。「サニタイザー」としては、これはかなり決定的で堅牢なアプローチですが、決して 完璧 にはなりません。テストプログラムが実際にその UB を伴う実行を持っている必要がありますし、十分に大きいプログラムでは、あらゆる種類の非決定性を導入するのは非常に簡単です(HashMap はデフォルトで RNG を使います!)。
Miri がプログラムの実行を承認したからといって、UB が存在しないという絶対確実な主張として受け取ることは決してできません。また、本当はそうではないのに、Miri が何かを UB だと 考える 可能性もあります。しかし、物事がどのように機能するかについてのメンタルモデルがあり、Miri もそれに同意しているように見えるなら、それは私たちが正しい道筋にいる良い兆候です。
基本的な借用
前のセクションでは、借用チェッカーがこのコードを好まないことを見ました。
let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;
// 順序を入れ替えています!
*ref1 += 1;
*ref2 += 2;
println!("{}", data);
ref2 を *mut に置き換えると何が起こるか見てみましょう。
unsafe {
let mut data = 10;
let ref1 = &mut data;
let ptr2 = ref1 as *mut _;
// 順序を入れ替えています!
*ref1 += 1;
*ptr2 += 2;
println!("{}", data);
}
cargo run
Compiling miri-sandbox v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.71s
Running `target\debug\miri-sandbox.exe`
13
Rustc はこれに完全に満足しているようです。警告はなく、プログラムは期待どおりの結果を生成しました! では、miri(厳格モード)がこれをどう考えるか見てみましょう。
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running cargo-miri.exe target\miri
error: Undefined Behavior: no item granting read access
to tag <untagged> at alloc748 found in borrow stack.
--> src\main.rs:9:9
|
9 | *ptr2 += 2;
| ^^^^^^^^^^ no item granting read access to tag <untagged>
| at alloc748 found in borrow stack.
|
= help: this indicates a potential bug in the program:
it performed an invalid operation, but the rules it
violated are still experimental
良いですね! 物事がどのように機能するかについての直感的なモデルは持ちこたえました。コンパイラーは私たちの代わりに問題を捕まえられませんでしたが、miri は捕まえてくれました。
もう少し複雑なもの、以前に触れた &mut -> *mut -> &mut -> *mut のケースを試してみましょう。
unsafe {
let mut data = 10;
let ref1 = &mut data;
let ptr2 = ref1 as *mut _;
let ref3 = &mut *ptr2;
let ptr4 = ref3 as *mut _;
// 最初の生ポインターに先にアクセスする
*ptr2 += 2;
// 次に「借用スタック」の順序でアクセスする
*ptr4 += 4;
*ref3 += 3;
*ptr2 += 2;
*ref1 += 1;
println!("{}", data);
}
cargo run
22
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting read access
to tag <1621> at alloc748 found in borrow stack.
--> src\main.rs:13:5
|
13 | *ptr4 += 4;
| ^^^^^^^^^^ no item granting read access to tag <1621>
| at alloc748 found in borrow stack.
|
おお、確かに! 厳格モードでは、miri は 2 つの生ポインターを「見分ける」ことができ、2 番目のポインターを使うと 1 番目のポインターが無効化されます。すべてを台無しにしている最初の使用を取り除くと、すべてがうまくいくか見てみましょう。
unsafe {
let mut data = 10;
let ref1 = &mut data;
let ptr2 = ref1 as *mut _;
let ref3 = &mut *ptr2;
let ptr4 = ref3 as *mut _;
// 「借用スタック」の順序でアクセスする
*ptr4 += 4;
*ref3 += 3;
*ptr2 += 2;
*ref1 += 1;
println!("{}", data);
}
cargo run
20
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
20
いいですね。
ええ、この時点でもう全員、プログラミング言語のメモリモデル設計と実装で博士号を取れると私はかなり確信しています。コンパイラーなんていったい誰が 必要 とするのでしょう。こんなの 簡単 です。
ナレーター: 簡単ではありませんでした。それでも、あなたのことを誇りに思います。
配列のテスト
配列とポインターオフセット(add と sub)をいじってみましょう。これは動くはずですよね?
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0 番目の要素への参照
let ptr2_at_0 = ref1_at_0 as *mut i32; // 0 番目の要素へのポインター
let ptr3_at_1 = ptr2_at_0.add(1); // 1 番目の要素へのポインター
*ptr3_at_1 += 3;
*ptr2_at_0 += 2;
*ref1_at_0 += 1;
// [3, 3, 0, ...] になるはず
println!("{:?}", &data[..]);
}
cargo run
[3, 3, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting read access
to tag <1619> at alloc748+0x4 found in borrow stack.
--> src\main.rs:8:5
|
8 | *ptr3_at_1 += 3;
| ^^^^^^^^^^^^^^^ no item granting read access to tag <1619>
| at alloc748+0x4 found in borrow stack.
大学院への出願書類を破り捨てる
何が起こったのでしょうか? 私たちは借用スタックをまったく問題なく使っています! ptr -> ptr と進むと何か奇妙なことが起こるのでしょうか? ポインターをコピーするだけにして、すべて同じ場所を指すようにしたらどうなるでしょう。
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0 番目の要素への参照
let ptr2_at_0 = ref1_at_0 as *mut i32; // 0 番目の要素へのポインター
let ptr3_at_0 = ptr2_at_0; // 0 番目の要素へのポインター
*ptr3_at_0 += 3;
*ptr2_at_0 += 2;
*ref1_at_0 += 1;
// [6, 0, 0, ...] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]
いいえ、これは問題なく動きます。たまたま運がよかったのかもしれないので、ポインターを本当にひどくごちゃごちゃにしてみましょう。
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0番目の要素への参照
let ptr2_at_0 = ref1_at_0 as *mut i32; // 0番目の要素へのポインター
let ptr3_at_0 = ptr2_at_0; // 0番目の要素へのポインター
let ptr4_at_0 = ptr2_at_0.add(0); // 0番目の要素へのポインター
let ptr5_at_0 = ptr3_at_0.add(1).sub(1); // 0番目の要素へのポインター
// ポインターの使用をめちゃくちゃに入り混ぜたもの
*ptr3_at_0 += 3;
*ptr2_at_0 += 2;
*ptr4_at_0 += 4;
*ptr5_at_0 += 5;
*ptr3_at_0 += 3;
*ptr2_at_0 += 2;
*ref1_at_0 += 1;
// [20, 0, 0, ...] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]
違います!Miri は、他の生ポインターから派生した生ポインターに関しては、実際にははるかに寛容です。それらはすべて同じ「借用」(あるいは miri が呼ぶところの tag)を共有します。
生ポインターを使い始めると、それらは自由に自分自身の小さな怒れる男たちへと分裂して、自分たち同士で好き勝手できます。これが問題ないのは、コンパイラーがそれを理解しており、参照に対して行うのと同じようには読み書きを最適化しないためです。
ナレーター: コードが十分に単純であれば、コンパイラーは派生ポインターをすべて追跡し、可能なところでは最適化を続けられますが、参照に対して使える推論よりもずっと壊れやすくなります。
では、本当の問題は何でしょうか?
data は 1 つの「アロケーション」(ローカル変数)ですが、ref1_at_0 が借用しているのは最初の要素だけです。Rust では、借用を分割して、アロケーションの特定の部分にだけ適用できるようになっています!試してみましょう。
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0番目の要素への参照
let ref2_at_1 = &mut data[1]; // 1番目の要素への参照
let ptr3_at_0 = ref1_at_0 as *mut i32; // 0番目の要素へのポインター
let ptr4_at_1 = ref2_at_1 as *mut i32; // 1番目の要素へのポインター
*ptr4_at_1 += 4;
*ptr3_at_0 += 3;
*ref2_at_1 += 2;
*ref1_at_0 += 1;
// [3, 3, 0, ...] になるはず
println!("{:?}", &data[..]);
}
error[E0499]: cannot borrow `data[_]` as mutable more than once at a time
--> src\main.rs:5:21
|
4 | let ref1_at_0 = &mut data[0]; // Reference to 0th element
| ------------ first mutable borrow occurs here
5 | let ref2_at_1 = &mut data[1]; // Reference to 1th element
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 | let ptr3_at_0 = ref1_at_0 as *mut i32; // Ptr to 0th element
| --------- first borrow later used here
|
= help: consider using `.split_at_mut(position)` or similar method
to obtain two mutable non-overlapping sub-slices
しまった!Rust は、これらの借用が互いに素であることを証明するために配列のインデックスを追跡しませんが、安全に動作すると仮定できる方法でスライスを複数の部分に分割するための split_at_mut を提供しています。
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let slice1 = &mut data[..];
let (slice2_at_0, slice3_at_1) = slice1.split_at_mut(1);
let ref4_at_0 = &mut slice2_at_0[0]; // 0番目の要素への参照
let ref5_at_1 = &mut slice3_at_1[0]; // 1番目の要素への参照
let ptr6_at_0 = ref4_at_0 as *mut i32; // 0番目の要素へのポインター
let ptr7_at_1 = ref5_at_1 as *mut i32; // 1番目の要素へのポインター
*ptr7_at_1 += 7;
*ptr6_at_0 += 6;
*ref5_at_1 += 5;
*ref4_at_0 += 4;
// [10, 12, 0, ...] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]
おお、動きます!スライスは、コンパイラーと miri に「この範囲内のすべてのメモリーについて大きな借用を取るよ」と適切に伝えるので、すべての要素を変更できることがわかるのです。
また、split_at_mut のような操作が許可されているということは、借用はスタックというよりツリーに近い場合がある、ということも示しています。というのも、1 つの大きな借用を、互いに素な小さな借用たちに分割しても、すべてが問題なく動くからです。
(実際の Stacked Borrows モデルでは、スタックが概念的にはプログラムの各バイトに対する権限を追跡しているので、すべては依然としてスタックなのだと思いますが……?)
スライスを直接ポインターに変換したらどうなるでしょうか?そのポインターはスライス全体にアクセスできるのでしょうか?
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let slice1_all = &mut data[..]; // 配列全体に対するスライス
let ptr2_all = slice1_all.as_mut_ptr(); // 配列全体に対するポインター
let ptr3_at_0 = ptr2_all; // 0番目の要素へのポインター(同じもの)
let ptr4_at_1 = ptr2_all.add(1); // 1番目の要素へのポインター
let ref5_at_0 = &mut *ptr3_at_0; // 0番目の要素への参照
let ref6_at_1 = &mut *ptr4_at_1; // 1番目の要素への参照
*ref6_at_1 += 6;
*ref5_at_0 += 5;
*ptr4_at_1 += 4;
*ptr3_at_0 += 3;
// お楽しみとして、ループですべての要素を変更する
// (これにはどの生ポインターでも使える。それらは借用を共有している!)
for idx in 0..10 {
*ptr2_all.add(idx) += idx;
}
// お楽しみとして、これと同じコードの安全なバージョン
for (idx, elem_ref) in slice1_all.iter_mut().enumerate() {
*elem_ref += idx;
}
// [8, 12, 4, 6, 8, 10, 12, 14, 16, 18] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]
いいですね!ポインターは単なる整数ではありません。ポインターには関連付けられたメモリー範囲があり、Rust ではその範囲を狭めることが許されています!
共有参照のテスト
これまでの例では、できるだけ単純にするために、可変参照だけを使い、読み取り・変更・書き込みの操作(+=)だけを行うよう、かなり慎重にしてきました。
しかし Rust には、読み取り専用で自由にコピーできる共有参照があります。それらはどのように動作するべきでしょうか?さて、生ポインターは自由にコピーでき、それらが 1 つの借用を「共有する」と言うことで扱えることを見てきました。共有参照も同じように考えられるのではないでしょうか?
値を読み取る関数で試してみましょう(println! は auto-ref/deref まわりで少し魔法のように振る舞うことがあるので、確実に意図したものをテストできるように関数でラップしています)。
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let sref2 = &mref1;
let sref3 = sref2;
let sref4 = &*sref2;
// 共有参照の読み取りをランダムに入り混ぜたもの
opaque_read(sref3);
opaque_read(sref2);
opaque_read(sref4);
opaque_read(sref2);
opaque_read(sref3);
*mref1 += 1;
opaque_read(&data);
}
cargo run
warning: unnecessary `unsafe` block
--> src\main.rs:6:1
|
6 | unsafe {
| ^^^^^^ unnecessary `unsafe` block
|
= note: `#[warn(unused_unsafe)]` on by default
warning: `miri-sandbox` (bin "miri-sandbox") generated 1 warning
10
10
10
10
10
11
そういえば、raw pointer で何もしていませんでしたが、少なくともすべての共有参照が相互に入れ替えて使っても問題ないことは確認できます。では、raw pointer も混ぜてみましょう。
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let ptr2 = mref1 as *mut i32;
let sref3 = &mref1;
let ptr4 = sref3 as *mut i32;
*ptr4 += 4;
opaque_read(sref3);
*ptr2 += 2;
*mref1 += 1;
opaque_read(&data);
}
cargo run
error[E0606]: casting `&&mut i32` as `*mut i32` is invalid
--> src\main.rs:11:16
|
11 | let ptr4 = sref3 as *mut i32;
| ^^^^^^^^^^^^^^^^^
おっと、実際には & ではなく & &mut をいじっていました! Rust は、それが問題にならない場面では、それをうまく覆い隠してくれます。let sref3 = &*mref1 で正しく再借用しましょう。
cargo run
error[E0606]: casting `&i32` as `*mut i32` is invalid
--> src\main.rs:11:16
|
11 | let ptr4 = sref3 as *mut i32;
| ^^^^^^^^^^^^^^^^^
だめです。Rust はまだそれを許してくれません! 共有参照は *const にしかキャストできず、それは読み取りしかできません。ですが、もし単に……こう……したら……?
let ptr4 = sref3 as *const i32 as *mut i32;
cargo run
14
17
えっ。OK、そうですか、わかりました? 素晴らしいキャストシステムですね Rust。まるで *const が、C API を記述するためと、正しい使い方をぼんやり示唆するためにしか実際には存在しない、かなり役に立たない型であるかのようです(実際そうですし、そうしています)。miri はどう考えるでしょう?
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting write access to
tag <1621> at alloc742 found in borrow stack.
--> src\main.rs:13:5
|
13 | *ptr4 += 4;
| ^^^^^^^^^^ no item granting write access to tag <1621>
| at alloc742 found in borrow stack.
残念ながら、ダブルキャストでコンパイラの文句を回避することはできますが、それでこの操作が実際に許可されるわけではありません。共有参照を取得するとき、私たちはその値を変更しないと約束しています。
これが重要なのは、共有借用が借用スタックからポップされたとき、その下にある mutable pointer は、メモリが変更されていないと仮定できることを意味するからです。メモリを読んでいる小さな怒れる男たちがいたかもしれません(そのため書き込みはコミットされる必要がありました)が、彼らはそれを変更できなかったので、mutable pointer は自分たちが最後に書き込んだ値がまだそこにあると仮定できます!
共有参照がひとたび借用スタック上に置かれると、その上にプッシュされるものはすべて読み取り権限しか持ちません。
ただし、これは可能です。
#![allow(unused)]
fn main() {
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let ptr2 = mref1 as *mut i32;
let sref3 = &*mref1;
let ptr4 = sref3 as *const i32 as *mut i32;
opaque_read(&*ptr4);
opaque_read(sref3);
*ptr2 += 2;
*mref1 += 1;
opaque_read(&data);
}
}
実際に読み取りしか行わない限り、mutable raw pointer を作成しても依然として「問題ない」ことに注目してください!
cargo run
10
10
13
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
10
10
13
念のため、共有参照が通常どおりポップされることも確認してみましょう。
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let ptr2 = mref1 as *mut i32;
let sref3 = &*mref1;
*ptr2 += 2;
opaque_read(sref3); // 間違った順序で読み取り?
*mref1 += 1;
opaque_read(&data);
}
cargo run
12
13
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: trying to reborrow for SharedReadOnly
at alloc742, but parent tag <1620> does not have an appropriate
item in the borrow stack
--> src\main.rs:13:17
|
13 | opaque_read(sref3); // Read in the wrong order?
| ^^^^^ trying to reborrow for SharedReadOnly
| at alloc742, but parent tag <1620>
| does not have an appropriate item
| in the borrow stack
|
おや、特定のタグではなく SharedReadOnly についての、少し違うエラーメッセージまで得られました。これは納得できます。いったん何らかの共有参照があると、基本的にそれ以外のすべては大きな SharedReadOnly のスープのようなものなので、それらを区別する必要はないのです!
内部可変性のテスト
本の本当にひどい章を覚えていますか。RefCell と Rc で linked list を作ろうとして、この忌々しい linked list を書こうとすると、あらゆることがいつも以上にひどくなったあの章です。
私たちは共有参照はミューテーションに使えないと主張し続けてきましたが、その章は、内部可変性によって共有参照越しに実際にはミューテーションできる、という話がすべてでした。素敵でシンプルな std::cell::Cell 型を試してみましょう。
#![allow(unused)]
fn main() {
use std::cell::Cell;
unsafe {
let mut data = Cell::new(10);
let mref1 = &mut data;
let ptr2 = mref1 as *mut Cell<i32>;
let sref3 = &*mref1;
sref3.set(sref3.get() + 3);
(*ptr2).set((*ptr2).get() + 2);
mref1.set(mref1.get() + 1);
println!("{}", data.get());
}
}
ああ、なんて美しい混乱でしょう。miri がこれに唾を吐くのを見るのは楽しみです。
cargo run
16
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
16
待って、本当に? それは問題ないのですか? なぜ? どうして? そもそも Cell とは何なのでしょう?
stdlib の南京錠を叩き壊す
pub struct Cell<T: ?Sized> {
value: UnsafeCell<T>,
}
UnsafeCell とは一体何なのでしょう?
本気だと stdlib に示すために、さらに別の南京錠を叩き壊す
#[lang = "unsafe_cell"]
#[repr(transparent)]
#[repr(no_niche)]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
ああ、これは魔法使いの魔法です。なるほど。たぶん。#[lang = "unsafe_cell"] は、文字どおり UnsafeCell は UnsafeCell だと言っているだけです。鍵を壊すのはやめて、std::cell::UnsafeCell の実際のドキュメントを確認しましょう。
Rust における内部可変性の中核となるプリミティブ。
&T参照を持っている場合、通常 Rust では、コンパイラは&Tがイミュータブルなデータを指しているという知識に基づいて最適化を行います。たとえばエイリアスを通じて、または&Tを&mut Tに transmute することでそのデータをミューテーションすることは、未定義動作と見なされます。UnsafeCell<T>は&Tに対するイミュータビリティ保証をオプトアウトします。共有参照&UnsafeCell<T>は、ミューテーション中のデータを指していてもよいです。これは「内部可変性」と呼ばれます。
ああ、これは本当にただの魔法使いの魔法です。 UnsafeCell は基本的に、コンパイラに「おい聞いてくれ、このメモリではちょっとふざけたことをやるから、いつものエイリアシングの仮定は一切するな」と伝えるものです。大きな「注意: 小さな怒れる男たち横断中」の標識を立てるようなものです。
UnsafeCell を追加すると miri がどう満足するのか見てみましょう:
use std::cell::UnsafeCell;
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = UnsafeCell::new(10);
let mref1 = data.get_mut(); // 内容への可変参照を取得
let ptr2 = mref1 as *mut i32;
let sref3 = &*ptr2;
*ptr2 += 2;
opaque_read(sref3);
*mref1 += 1;
println!("{}", *data.get());
}
cargo run
12
13
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: trying to reborrow for SharedReadOnly
at alloc748, but parent tag <1629> does not have an appropriate
item in the borrow stack
--> src\main.rs:15:17
|
15 | opaque_read(sref3);
| ^^^^^ trying to reborrow for SharedReadOnly
| at alloc748, but parent tag <1629> does
| not have an appropriate item in the
| borrow stack
|
待って、何だって? 魔法の言葉は唱えたじゃないですか! 連邦政府承認済みの儀式強化用ヤギの血をこんなに用意したのに、どうすればいいんですか?
まあ、確かに唱えました。しかしその後で、get_mut を使ってその呪文を完全に捨ててしまいました。これは UnsafeCell の中を覗き込み、結局それに対する正真正銘の &mut i32 を作ってしまうものです!
考えてみてください: もしコンパイラが &mut i32 が UnsafeCell の中を見ている可能性があると仮定しなければならないなら、エイリアシングについて何の仮定もできなくなってしまいます! あらゆるものが小さな怒れる男たちでいっぱいかもしれないのです。
つまり必要なのは、ポインタ型の中に UnsafeCell を保持して、コンパイラがこちらの意図を理解できるようにすることです。
#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = UnsafeCell::new(10);
let mref1 = &mut data; // *外側*への可変参照
let ptr2 = mref1.get(); // 内側への生ポインタを取得
let sref3 = &*mref1; // *外側*への共有参照を取得
*ptr2 += 2; // 生ポインタで変更
opaque_read(&*sref3.get()); // 共有参照から読み取り
*sref3.get() += 3; // 共有参照経由で書き込み
*mref1.get() += 1; // 可変参照で変更
println!("{}", *data.get());
}
}
cargo run
12
16
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
12
16
動きました! これで結局、この血を全部捨てずに済みそうです。
いや、ちょっと待ってください。ここではまだ順序が少しおかしなことになっています。最初に ptr2 を作り、その後で可変ポインタから sref3 を作りました。そして共有ポインタより先に生ポインタを使っています。これは全部……間違っているように見えます。
いや待ってください、Cell の例でも同じことをしていましたね。うーん。
次の 2 つのどちらかを結論せざるを得ません:
- Miri は不完全で、これは実際にはまだ UB である。
- 私たちの単純化したモデルは、実は単純化しすぎである。
私は後者に賭けますが、念のため、stacked borrows の単純化モデルにおいて間違いなく完全に堅牢なバージョンを作ってみましょう:
#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = UnsafeCell::new(10);
let mref1 = &mut data;
// 借用が*間違いなく*完全に積み重なるよう、この 2 つを入れ替える
let sref2 = &*mref1;
// 最大限安全にするため、共有参照から ptr を派生させる!
let ptr3 = sref2.get();
*ptr3 += 3;
opaque_read(&*sref2.get());
*sref2.get() += 2;
*mref1.get() += 1;
println!("{}", *data.get());
}
}
cargo run
13
16
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
13
16
さて、最初の実装が実際には正しいかもしれない理由の 1 つは、本気で考えてみると、エイリアシングに関する限り &UnsafeCell<T> は実際には *mut T と何も違わないからです。無限にコピーできるし、それを通じて変更できます!
つまり、ある意味では単に 2 つの生ポインタを作り、それらを普通に交換可能なものとして使っただけです。どちらも可変参照から派生しているのは少し怪しいので、2 つ目を作った時点で 1 つ目は borrow stack からポップされるべきなのかもしれません。しかし、実際には可変参照の内容にアクセスしているわけではなく、そのアドレスをコピーしているだけなので、それは本当に必要というわけではありません。
let sref2 = &*mref1 のような行は、ひっかけめいたものです。構文的にはデリファレンスしているように見えますが、デリファレンス単体は実際には何かの動作なのでしょうか? &my_tuple.0 を考えてみてください。実際には my_tuple や .0 に対して何かをしているわけではありません。それらを使ってメモリ上の位置を参照し、その前に & を置くことで「これをロードするな、アドレスだけ書き留めろ」と言っているだけです。
&* も同じです: * は単に「このポインタが指している場所について話そう」と言っていて、& は単に「ではそのアドレスを書き留めろ」と言っているだけです。もちろんそれは元のポインタが持っていた値と同じです。ただしポインタの型は変わっています。なぜなら、ええと、型です!
とはいえ、&** とすると、最初の * で実際に値をロードしています! * は奇妙です!
ナレーター: 君が「lvalue」という単語を知っていることなんて、誰も気にしていないよ、Jonathan。Rust ではそれを places と呼ぶんだ。これはまったく別物で、しかもずっとクールなんだよ?
Box をテストする
ところで、このものすごく長い脱線を始めた理由を覚えていますか? 覚えていない? 変ですね。
それは、Box と生ポインタを混ぜたからでした。Box は、指しているメモリの一意な所有権を主張するので、&mut にある程度似ています。その主張をテストしてみましょう:
unsafe {
let mut data = Box::new(10);
let ptr1 = (&mut *data) as *mut i32;
*data += 10;
*ptr1 += 1;
// 21 になるはず
println!("{}", data);
}
cargo run
21
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting read access
to tag <1707> at alloc763 found in borrow stack.
--> src\main.rs:7:5
|
7 | *ptr1 += 1;
| ^^^^^^^^^^ no item granting read access to tag <1707>
| at alloc763 found in borrow stack.
|
はい、miri はこれを嫌がります。正しい順序で行えば大丈夫か確認しましょう:
#![allow(unused)]
fn main() {
unsafe {
let mut data = Box::new(10);
let ptr1 = (&mut *data) as *mut i32;
*ptr1 += 1;
*data += 10;
// 21 になるはず
println!("{}", data);
}
}
cargo run
21
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
21
はい!
やれやれ皆さん、これで stacked borrows について話したり考えたりするのはついに終わりです!
……待って、Box でこの問題をどう解決するんですか? もちろん、このようなおもちゃのプログラムを書くことはできますが、Box をどこかに保存して、生ポインタを潜在的に長い間保持しておく必要があります。きっと何かが混ざって無効化されてしまうのでは?
素晴らしい質問です! それに答えるために、ついに私たちの真の使命へ戻ることになります: くそったれな連結リストを書くことです。 待って、また連結リストを書かなきゃいけないの? 皆さん、早まらないでください。冷静になりましょう。ちょっと待ってください、私が議論すべき他の興味深い問題がきっとあるはずで—