Как 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()
, как указано выше?
Ответы
Ответ 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
в любом случае не рекомендуется.