Тестирование стековых заимствований

Подытожим, что мы (упрощённо) знаем о подели памяти Rust, опираясь на предыдущие разделы:

  • Концептуально, обработка повторных заимствований в Rust осуществляется через «стек заимствований»
  • Единственный указатель на вершине стека считается «живым» (имеет эксклюзивный доступ)
  • При доступе через указатель из глубины стека, «живым» становится он, а указатели выше него удаляются из стека
  • Нельзя использовать указатели, которые были удалены из стека заимствований
  • Анализатор зависимостей гарантирует, что безопасный код соответствуем этим требованиям
  • Miri (в теории) проверяет, что сырые указатели соответствуют этим правилам во время выполнения

В прошлой части было много теории и идей. Теперь давайте перейдём к истинной сути этой книги — написанию плохого кода и тому, как заставить наши инструменты ругаться на этот код. Мы разберём груды примеров, чтобы понять, работает ли наша ментальная модель, а также попытаемся прочувствовать стековые заимствования на интуитивном уровне.

ГОЛОС ЗА КАДРОМ: обнаружение Неопределённого Поведения — это хлопотное дело. Помимо прочего, мы имеем дело с ситуациями, которые компилятор буквально полагает невозможными.

Если вы счастливчик, программа будет «казаться работающей», при этом она будет содержать бомбу замедленного действия для Более Умного Компилятора или небольшого изменения в коде. Если вы настоящий счастливчик, программа гарантированно не будет работать, так что вы сможете найти ошибку и исправить её. Но если вы не счастливчик, программа будет ломаться странным и непонятным образом.

Miri пытается работать обойти эту проблему, получая от rustc простейшее не оптимизированное представление программы и следя за её состоянием при интерпретации. «Средства динамического анализа» (sanitizers) — довольно детерминированный и надёжный подход, он он никогда не будет совершенным. Ваша тестовая программа должна дойти до точки с UB, однако во многих программах пышным цветом цветёт разного рода недетерминированное поведение (скажем, HashMap по умолчанию использует датчик случайных чисел).

Ни при каких обстоятельствах мы не должны считать, что отсутствие предупреждений 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 смогла «различить» два сырых указателя, обнаружив, что использование второго ведёт к порче первого. Давайте посмотрим, начнёт ли всё работать, когда мы удалим первое использование, которое всё сломало:

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

ОТЛИЧНО.

Я почти уверена, что к настоящему моменту мы можем получить докторскую степень по проектированию и реализации моделей памяти в языках программирования. Кому вообще нужны компиляторы, если это так просто?

ГОЛОС ЗА КАДРОМ: это не так, но я всё равно тобой горжусь.

Тестирование массивов

Поэкспериментируем с массивами и смещениями указателей (сложением и вычитанием). Будет ли работать этот код?

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 его называет, маркер).

Как только вы начинаете использовать сырые указатели, они провоцируют появление сердитых человечков, которые начинают мешать. Но это нормально, и поэтому компилятор не будет оптимизировать чтение и запись также, как в случае ссылок.

ГОЛОС ЗА КАДРОМ: Если код достаточно простой, компилятор отслеживает производные указатели и продолжает оптимизацию, если это возможно. Однако эта часть компилятора достаточно капризная.

Так в чём же реальная проблема?

Несмотря на то, что data — это один «кусок памяти» (одна локальная переменная), 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;

    // Должно быть [4, 6, 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 позволяют нам утверждать, что заимствования организуются не в виде стека, а в виде дерева, так как мы можем разбить одно большое заимствование на несколько непересекающихся маленьких, и всё будет работать.

(Мне кажется, что в самой модели заимствований всё продолжает считаться стеком, потому что стек концептуально отслеживает права доступа к каждому байту программы.)

А что, если мы напрямую преобразуем срез в указатель? Будет ли этот указатель иметь доступ к полному срезу?

#![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 есть разделяемые ссылки, которые предназначены только для чтения и могут быть свободно скопированы. Как будут работать они? Ладно, мы видели, что сырые указатели можно свободно копировать и что с ними можно работать, считая, что копии «разделяют» единственное заимствование. Нельзя ли подобным образом думать и о разделяемых ссылках?

Давайте проверим наше предположение с помощью функции, которая читает значение. (Макрос `println! может работать волшебным образом, когда речь заходит о получении и разыменовании ссылок, так что я завернула его в функцию, чтобы быть уверенной, что мы тестируем то, что нужно):

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

Ах, да, мы не делаем с сырыми указателями ничего потенциально опасного, поэтому компилятор ругается на нужный unsafe. Но, по крайней мере мы видим, что использовать разделяемые указатели совместно — вполне нормально. Добавим несколько сырых указателей:

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

ЧТО? ПРОСТО РАБОТАЕТ? Отличная система приведения типов, 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.

К сожалению, хотя с помощью двойного приведения мы можем избавиться от жалоб компилятора, это не делает операцию *разрешённой". Получая разделяемую ссылку, мы обещаем не модифицировать значение.

Это важно, потому что когда мы удаляем разделяемое заимствование из стека заимствований, изменяемые указатели перед ним могут быть уверены, что память не изменилась. Может быть, какие-то сердитые человечки и читали память, но у них не было возможности её изменить, так что изменяемые указатели могут предполагать, что последнее значение, которое они записали, всё ещё там!

Как только в стек заимствований попадает разделяемая ссылка, всё, что вставляется после неё, обладает только правами на чтение.

Однако мы можем сделать так:

#![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);
}
}

Обратите внимание, что создание изменяемого сырого указателя считается «нормальным», если мы только читаем его данные!

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? Ту, где всё было даже хуже, чем обычно?

Всё это время мы настаивали, что разделяемые ссылки нельзя использовать для изменения, но вся та глава была про то, что на самом деле мы можем менять значения разделяемых ссылок с помощью внутренней изменчивости. Пришла время исследовать простой и удобный тип 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?

Расчехляет стандартную библиотеку

pub struct Cell<T: ?Sized> {
    value: UnsafeCell<T>,
}

Что это за ерундовина, UnsafeCell?

Расчехляет дальше, просто чтобы показать стандартной библиотеке, что мы настроены серьёзно

#[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 ведёт к неопределённому поведению. UnsafeCell<T> отключает гарантию неизменяемости для &T: разделяемая ссылка &UnsaveCell<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, он вообще не может делать никаких предположений! Код, полный сердитых человечков.

Нам надо оставить UnsafeCel в наших типах указателей, чтобы компилятор понимал, что мы хотим сделать.

#![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. Хммм.

Мы должны один из двух выводов:

  • Miri несовершенна и на самом деле у нас здесь UB.
  • Наша упрощённая модель оказалсь чрезмерно упрощённой.

Я бы поставила на второй вариант, но, просто чтобы быть уверенной, сделаю версию, которая будет абсолютно надёжной в нашей упрощённой модели:

#![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 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

Одна из причин, почему первая реализация, могла быть совершенно корректной, заключается в том, что &UnsafeCell<T> по сути не отличается от *mut T. Его можно бесконечно копировать его и менять!

Так что мы, как обычно, создали два сырых указателя и использовали их по очереди. Немного подозрительно, что оба указателя получены из изменяемой ссылки, так что, кажется, при создании второго надо удалять из стека заимствований первый. Но на самом деле мы не обращаемся к содержимому изменяемой ссылки, а просто копируем адрес, так что всё нормально.

Строка let sref2 = &*mref1 — довольно хитрая штука. Синтаксически кажется, что мы разыменовали указатель, но разыменование само по себе не являтеся чем-то реальным. Сравните с &my_tuple.0: вы ничего не делаете ни с my_tuple, ни с .0, вы просто используете выражение, чтобы получить адрес в памяти, а, написав перед ним & как бы говорите: «не загружай содержимое, просто запомни адрес».

&* — это то же самое: * говорит «обсудим местоположение, на которое указывает этот указатель», а & говорит «теперь запиши этот адрес». Естественно, адрес остался тем же. Но тип указателя изменился, потому что... ну, типы!

С другой стороны, если вы пишете &**, то, по факту, загружаете значение сразу после первой операции *! Эта * такая странная!

ГОЛОС ЗА КАДРОМ: Никого не волнует, что вы знаете, что такое «л-значение», Джонатан. В 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

Так!

Что ж, на этом всё, мы, наконец, закончили обсуждать стековые заимствования!

...хотя, как нам решить проблему с Box? Конечно, мы можем писать игрушечные программы, но всё-таки — нам надо хранить Box и удерживать значения сырых указателей в течение довольно долгого времени. Наверняка всё перепутается и станет недействительным?

Хороший вопрос! Чтобы на него ответить, вернёмся, наконец, к нашей истинной задаче — написанию богом проклятых связных списков.

Подождите, мы снова станем писать связные списки? Не будем торопиться, друзья. Будем благоразумны. Я уверена, что у меня есть несколько интересных вопросов для обсуждения.