Rust 如何知道在堆栈展开期间是否 运行 析构函数?
How does Rust know whether to run the destructor during stack unwind?
mem::uninitialized
的文档指出了为什么 dangerous/unsafe 使用该函数:在未初始化的内存上调用 drop
是未定义的行为。
所以我认为这段代码应该是未定义的:
let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)
然而,我写了这段代码,它在安全的 Rust 中工作并且似乎没有遭受未定义行为的影响:
#![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
king,但无论 i
的值是什么,它总是 运行 是正确的析构函数(打印预期的字符串)。
这是怎么发生的?如果 运行time 可以确定 运行 的哪个析构函数,是否应该从 mem::uninitialized()
的文档中删除关于实现 Drop
的类型强制需要初始化的内存部分作为链接以上?
使用 drop flags.
Rust(直到并包括版本 1.12)在其类型实现 Drop
的每个值中存储一个布尔标志(因此将该类型的大小增加一个字节)。该标志决定是否 运行 析构函数。因此,当您执行 b = bar()
时,它会为 b
变量设置标志,因此只有 运行s b
的析构函数。反之亦然 a
.
请注意,从 Rust 版本 1.13(在编写 beta 编译器时)开始,该标志不存储在类型中,而是存储在每个变量或临时变量的堆栈中。 Rust 编译器中 MIR 的出现使这成为可能。 MIR 显着简化了 Rust 代码到机器代码的翻译,从而使该功能能够将丢弃标志移动到堆栈。如果优化可以在编译时确定何时删除哪个对象,则通常会消除该标志。
您可以 "observe" 通过查看类型的大小,在 1.12 版之前的 Rust 编译器中使用此标志:
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
impl 不会访问您类型的任何内存(丢弃标志除外,但您不可见)。因此,您没有访问未初始化的内存(无论如何,它的大小为零字节,因为您的类型是零大小的结构)。据我所知,这意味着您的 unsafe { std::mem::uninitialized() }
代码实际上是安全的,因为之后不会发生内存不安全。
首先,有drop flags - 用于跟踪哪些变量已被初始化的运行时信息。如果一个变量没有被赋值,drop()
将不会被执行。
在稳定版中,丢弃标志当前存储在类型本身中。向其写入未初始化的内存可能会导致未定义的行为,即 drop()
是否会被调用。这很快就会成为过时的信息,因为丢弃标志在 nightly 中从类型本身中移出。
在 nightly Rust 中,如果将未初始化的内存分配给变量,可以安全地假设 drop()
将被执行。但是,drop()
的任何有用实现都将对该值进行操作。无法检测类型是否在 Drop
特征实现中正确初始化:它可能导致尝试释放无效指针或任何其他随机事物,具体取决于 Drop
的实现方式。无论如何,将未初始化的内存分配给具有 Drop
的类型是不明智的。
这里隐藏了两个问题:
- 编译器如何跟踪哪个变量已初始化或未初始化?
- 为什么使用
mem::uninitialized()
进行初始化会导致未定义的行为?
让我们按顺序解决它们。
How does the compiler track which variable is initialized or not?
编译器注入所谓的 "drop flags":对于每个 Drop
必须 运行 在范围末尾的变量,在堆栈上注入一个布尔标志,说明这个变量是否需要处理掉
标志从 "no" 开始,如果变量已初始化,则移动到 "yes",如果变量从 "no" 移动,则返回到 "no"。
最后,当删除这个变量的时候,检查标志并在必要时删除它。
这与编译器的流分析是否抱怨可能未初始化的变量无关:只有当满足流分析时才会生成代码。
Why may initializing with mem::uninitialized()
lead to Undefined Behavior?
使用mem::uninitialized()
时,您向编译器承诺:别担心,我肯定会初始化这个。
对于编译器而言,变量因此被完全初始化,drop 标志被设置为"yes"(直到你移出它)。
这反过来意味着 Drop
将被调用。
使用未初始化的对象是未定义的行为,编译器代表您对未初始化的对象调用 Drop
算作 "using it".
奖金:
In my tests, nothing weird happened!
请注意,未定义行为意味着任何事情都可能发生;不幸的是,anything 还包括 "seems to work"(甚至 "works as intended despite the odds")。
特别是,如果您在 Drop::drop
中不访问对象的内存(只是打印),那么很可能一切都会正常工作。但是,如果您访问它,您可能会看到奇怪的整数、指向野外的指针等...
如果优化器很聪明,即使不访问它,它也可能会做一些奇怪的事情!由于我们使用的是 LLVM,因此我邀请您阅读 Chris Lattner(LLVM 之父)的 What every C programmer should know about Undefined Behavior。
mem::uninitialized
的文档指出了为什么 dangerous/unsafe 使用该函数:在未初始化的内存上调用 drop
是未定义的行为。
所以我认为这段代码应该是未定义的:
let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)
然而,我写了这段代码,它在安全的 Rust 中工作并且似乎没有遭受未定义行为的影响:
#![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
king,但无论 i
的值是什么,它总是 运行 是正确的析构函数(打印预期的字符串)。
这是怎么发生的?如果 运行time 可以确定 运行 的哪个析构函数,是否应该从 mem::uninitialized()
的文档中删除关于实现 Drop
的类型强制需要初始化的内存部分作为链接以上?
使用 drop flags.
Rust(直到并包括版本 1.12)在其类型实现 Drop
的每个值中存储一个布尔标志(因此将该类型的大小增加一个字节)。该标志决定是否 运行 析构函数。因此,当您执行 b = bar()
时,它会为 b
变量设置标志,因此只有 运行s b
的析构函数。反之亦然 a
.
请注意,从 Rust 版本 1.13(在编写 beta 编译器时)开始,该标志不存储在类型中,而是存储在每个变量或临时变量的堆栈中。 Rust 编译器中 MIR 的出现使这成为可能。 MIR 显着简化了 Rust 代码到机器代码的翻译,从而使该功能能够将丢弃标志移动到堆栈。如果优化可以在编译时确定何时删除哪个对象,则通常会消除该标志。
您可以 "observe" 通过查看类型的大小,在 1.12 版之前的 Rust 编译器中使用此标志:
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
impl 不会访问您类型的任何内存(丢弃标志除外,但您不可见)。因此,您没有访问未初始化的内存(无论如何,它的大小为零字节,因为您的类型是零大小的结构)。据我所知,这意味着您的 unsafe { std::mem::uninitialized() }
代码实际上是安全的,因为之后不会发生内存不安全。
首先,有drop flags - 用于跟踪哪些变量已被初始化的运行时信息。如果一个变量没有被赋值,drop()
将不会被执行。
在稳定版中,丢弃标志当前存储在类型本身中。向其写入未初始化的内存可能会导致未定义的行为,即 drop()
是否会被调用。这很快就会成为过时的信息,因为丢弃标志在 nightly 中从类型本身中移出。
在 nightly Rust 中,如果将未初始化的内存分配给变量,可以安全地假设 drop()
将被执行。但是,drop()
的任何有用实现都将对该值进行操作。无法检测类型是否在 Drop
特征实现中正确初始化:它可能导致尝试释放无效指针或任何其他随机事物,具体取决于 Drop
的实现方式。无论如何,将未初始化的内存分配给具有 Drop
的类型是不明智的。
这里隐藏了两个问题:
- 编译器如何跟踪哪个变量已初始化或未初始化?
- 为什么使用
mem::uninitialized()
进行初始化会导致未定义的行为?
让我们按顺序解决它们。
How does the compiler track which variable is initialized or not?
编译器注入所谓的 "drop flags":对于每个 Drop
必须 运行 在范围末尾的变量,在堆栈上注入一个布尔标志,说明这个变量是否需要处理掉
标志从 "no" 开始,如果变量已初始化,则移动到 "yes",如果变量从 "no" 移动,则返回到 "no"。
最后,当删除这个变量的时候,检查标志并在必要时删除它。
这与编译器的流分析是否抱怨可能未初始化的变量无关:只有当满足流分析时才会生成代码。
Why may initializing with
mem::uninitialized()
lead to Undefined Behavior?
使用mem::uninitialized()
时,您向编译器承诺:别担心,我肯定会初始化这个。
对于编译器而言,变量因此被完全初始化,drop 标志被设置为"yes"(直到你移出它)。
这反过来意味着 Drop
将被调用。
使用未初始化的对象是未定义的行为,编译器代表您对未初始化的对象调用 Drop
算作 "using it".
奖金:
In my tests, nothing weird happened!
请注意,未定义行为意味着任何事情都可能发生;不幸的是,anything 还包括 "seems to work"(甚至 "works as intended despite the odds")。
特别是,如果您在 Drop::drop
中不访问对象的内存(只是打印),那么很可能一切都会正常工作。但是,如果您访问它,您可能会看到奇怪的整数、指向野外的指针等...
如果优化器很聪明,即使不访问它,它也可能会做一些奇怪的事情!由于我们使用的是 LLVM,因此我邀请您阅读 Chris Lattner(LLVM 之父)的 What every C programmer should know about Undefined Behavior。