Подтвердить что ты не робот

Как Rust знает, нужно ли запускать деструктор во время стека?

В документации для mem::uninitialized указывается, почему это опасно/небезопасно использовать эту функцию: вызов drop в неинициализированной памяти - это поведение undefined.

Итак, этот код должен быть, я считаю, undefined:

let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)

Однако я написал этот фрагмент кода, который работает в безопасном Rust и, похоже, не страдает от поведения undefined:

#![feature(conservative_impl_trait)]

trait T {
    fn disp(&mut self);
}

struct A;
impl T for A {
    fn disp(&mut self) { println!("=== A ==="); }
}
impl Drop for A {
    fn drop(&mut self) { println!("Dropping A"); }
}

struct B;
impl T for B {
    fn disp(&mut self) { println!("=== B ==="); }
}
impl Drop for B {
    fn drop(&mut self) { println!("Dropping B"); }
}

fn foo() -> impl T { return A; }
fn bar() -> impl T { return B; }

fn main() {
    let mut a;
    let mut b;

    let i = 10;
    let t: &mut T = if i % 2 == 0 {
        a = foo();
        &mut a
    } else {
        b = bar();
        &mut b
    };

    t.disp();
    panic!("=== Test ===");
}

Кажется, что он выполняет правильный деструктор, игнорируя другой. Если я попытался использовать a или b (например, a.disp() вместо t.disp()), он правильно ошибается, говоря, что, возможно, я могу использовать неинициализированную память. Что меня удивило, когда король panic, он всегда запускает правильный деструктор (печатает ожидаемую строку) независимо от того, что такое значение i.

Как это происходит? Если среда выполнения может определить, какой деструктор будет выполняться, должна ли быть удалена часть из памяти, которая должна быть инициализирована для типов с drop, из документации mem::uninitialized(), как указано выше?

4b9b3361

Ответ 1

Rust (вплоть до версии 1.12) хранит логический флаг в каждом значении, тип которого реализует Drop (и, таким образом, увеличивает размер этого типа на один байт). Этот флаг определяет, следует ли запускать деструктор. Поэтому, когда вы выполняете b = bar(), он устанавливает флаг для переменной b и, следовательно, запускает b destructor. И наоборот: a.

Обратите внимание, что начиная с версии Rust версии 1.13 (на момент написания бета-компилятора) этот флаг не сохраняется в типе, а в стеке для каждой переменной или временной. Это стало возможным благодаря появлению MIR в компиляторе Rust. MIR значительно упрощает перевод кода Rust на машинный код и, таким образом, позволяет этой функции перемещать флаги drop в стек. Оптимизации, как правило, устраняют этот флаг, если они могут определить во время компиляции, когда этот объект будет удален.

Вы можете "наблюдать" этот флаг в компиляторе Rust до версии 1.12, посмотрев размер этого типа:

struct A;

struct B;

impl Drop for B {
    fn drop(&mut self) {}
}

fn main() {
    println!("{}", std::mem::size_of::<A>());
    println!("{}", std::mem::size_of::<B>());
}

печатает 0 и 1 соответственно перед флагами стека и 0 и 0 с флагами стека.

Использование mem::uninitialized по-прежнему небезопасно, поскольку компилятор все еще видит назначение переменной a и устанавливает флаг drop. Таким образом, деструктор будет вызван в неинициализированную память. Обратите внимание, что в вашем примере Drop impl не имеет доступа к какой-либо памяти вашего типа (кроме флага падения, но это вам незаметно). Поэтому вы не получаете доступ к неинициализированной памяти (размер которой равен нулю, так как ваш тип представляет собой структуру с нулевым размером). Насколько я знаю, это означает, что ваш код unsafe { std::mem::uninitialized() } на самом деле безопасен, потому что после этого не может возникнуть небезопасность памяти.

Ответ 2

Здесь есть два вопроса:

  • Как компилятор отслеживает, какая переменная инициализирована или нет?
  • Почему инициализация с помощью mem::uninitialized() приводит к Undefined Поведение?

Позвольте решать их по порядку.


Как компилятор отслеживает, какая переменная инициализирована или нет?

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

Флаг начинает "нет", переходит в "да", если переменная инициализируется, и возвращается к "нет", если переменная перемещается из.

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

Это не связано с тем, удовлетворяет ли анализ потока компилятора о потенциально неинициализированных переменных: только когда анализ потока выполняется, генерируется код.


Почему инициализация с помощью mem::uninitialized() приведет к Undefined Поведение?

При использовании mem::uninitialized() вы обещаете компилятору: не волнуйтесь, я определенно инициализирую это.

Что касается компилятора, значит, переменная полностью инициализирована, и флаг падения установлен на "да" (пока вы не выйдете из него).

Это, в свою очередь, означает, что будет называться Drop.

Использование неинициализированного объекта Undefined Behavior, а вызов компилятора Drop в неинициализированном объекте от вашего имени считается "использованием его".


Bonus:

В моих тестах ничего странного не произошло!

Обратите внимание, что Undefined Поведение означает, что все может случиться; что-то, к сожалению, также включает в себя "кажется, что работает" (или даже "работает по назначению, несмотря на шансы" ).

В частности, если вы НЕ обращаетесь к памяти объекта в Drop::drop (только при печати), то очень вероятно, что все будет работать. Однако, если вы делаете доступ к нему, вы можете увидеть странные целые числа, указатели, указывающие на дикую природу и т.д.

И если оптимизатор умный, даже без доступа к нему, он может сделать странные вещи! Поскольку мы используем LLVM, я приглашаю вас прочитать Что каждый программист C должен знать о Undefined Поведение Криса Лэттнера (отец LLVM).

Ответ 3

Во-первых, есть флаги отбрасывания - информация о времени выполнения для отслеживания, какие переменные были инициализированы. Если переменная не была назначена, drop() для нее не будет выполнена.

В стабильном состоянии флаг капли сохраняется в самом типе. Написание неинициализированной памяти на нее может привести к поведению undefined относительно того, будет ли drop() вызываться или не будет вызываться. Это скоро будет устаревшей информацией, потому что флаг кавычки в ночное время перемещается из самого типа.

В ночной Rust, если вы присваиваете переменную неинициализированную память, было бы безопасно предположить, что будет выполняться drop(). Однако любая полезная реализация drop() будет действовать на значение. Невозможно определить, правильно ли инициализирован тип или нет в реализации тэга Drop: это может привести к попытке освободить недействительный указатель или любую другую случайную вещь в зависимости от реализации типа Drop типа. Присвоение неинициализированной памяти типу с Drop в любом случае не рекомендуется.