我在Rust中尝试了函数指针魔术,最后得到了一个代码片段,对于它为什么编译甚至更多,为什么运行,我完全没有解释。
fn foo() { println!("This is really weird...");}fn caller<F>() where F: FnMut() { let closure_ptr = 0 as *mut F; let closure = unsafe { &mut *closure_ptr }; closure();}fn create<F>(_: F) where F: FnMut() { let func_ptr = caller::<F> as fn(); func_ptr();}fn main() { create(foo); create(|| println!("Okay...")); let val = 42; create(|| println!("This will seg fault: {}", val));}
我无法解释为什么 foo
通过将nullptr
in 强制转换caller(...)
为类型F的实例来进行调用。我本以为只能通过相应的函数指针来调用函数,但是鉴于指针本身为null,显然不是这种情况。话虽如此,看来我显然误解了Rust的类型系统的重要部分。
有人有答案吗?
该程序实际上从不构造指向任何东西的函数指针,而是caller
-总是直接调用foo
这两个闭包。
每个Rust函数,无论是闭包还是fn
项目,都具有唯一的匿名类型。这种类型的实现Fn
/ FnMut
/ FnOnce
特征,适当的。项的匿名类型fn
为零大小,就像没有捕获的闭包类型一样。
因此,表达式create(foo)
实例化create
的参数F
与foo
的类型-这不是函数指针类型fn()
,但匿名,零大小的类型只是为了foo
。在错误消息中,rustc称为这种类型fn() {foo}
,因为您可以看到此错误消息。
在内部create::<fn() {foo}>
(使用错误消息中的名称),该表达式caller::<F> as fn()
构造了一个指向的函数指针caller::<fn() {foo}>
。调用此函数指针与caller::<F>()
直接调用相同,这也是整个程序中唯一的函数指针。
最后,在caller::<fn() {foo}>
表达式中closure()
减掉FnMut::call_mut(closure)
。因为closure
具有类型&mut F
where F
只是零大小的类型fn() {foo}
,所以它本身的0
值closure
根本就不会用1,并且程序会foo
直接调用。
同样的逻辑也适用于闭包|| println!("Okay...")
,它foo
具有一个匿名的零尺寸类型,这次称为[closure@src/main.rs:2:14: 2:36]
。
第二个闭包并不是那么幸运-它的类型不是零大小的,因为它必须包含对变量的引用val
。这一次,FnMut::call_mut(closure)
实际上需要取消引用closure
才能完成其工作。因此它崩溃了2。
的类型fn foo() {...}
不是函数指针fn()
,实际上是特定于的唯一类型foo
。只要您携带该类型(此处为F
),编译器就知道如何调用它而无需任何额外的指针(此类类型的值不携带数据)。不捕获任何内容的闭包以相同的方式工作。仅当最后一个闭包尝试查找时,它才会出现错误,val
因为您将0
指针假定放置在了某个位置(大概)val
。
您可以使用观察到这一点size_of
,在前两个调用中,的大小closure
为零,但是在最后一个调用中,在闭包中捕获到的东西的大小为8(至少在操场上)。如果大小为0,则程序不必从NULL
指针加载任何内容。
NULL
指向引用的指针的有效转换仍然是未定义的行为,但是由于类型为shenanigans而不是由于内存访问shenanigans:具有实际上NULL
是非法的引用,因为类型之类的内存布局Option<&T>
依赖于以下假设:参考永远不会NULL
。这是一个如何出错的示例:
unsafe fn null<T>(_: T) -> &'static mut T { &mut *(0 as *mut T)}fn foo() { println!("Hello, world!");}fn main() { unsafe { let x = null(foo); x(); // prints "Hello, world!" let y = Some(x); println!("{:?}", y.is_some()); // prints "false", y is None! }}
尽管这完全取决于UB,但这是我认为在两种情况下可能会发生的情况:
类型F
是没有数据的闭包。这等效于一个功能,这意味着它F
是一个功能项。这意味着编译器可以优化对an的任何调用,以优化F
对产生的任何函数的调用F
(而无需创建函数指针)。见这对于这些东西的不同名称的例子。
编译器会看到它val
始终为42,因此可以将其优化为一个常数。如果是这种情况,那么传入的闭包create
仍然是没有捕获项目的闭包,因此我们可以遵循#1中的思想。
另外,我说这是UB,但是请注意关于UB的一些重要方面:如果调用UB,并且编译器以一种意想不到的方式利用它,它不是在试图弄乱您,而是在尝试优化代码。毕竟,UB是关于编译器未进行优化的事情,因为您违反了它的某些期望。因此,编译器以这种方式进行优化是完全合乎逻辑的。编译器不采用这种方式进行优化,而是利用UB,这完全是合乎逻辑的。
这是“有效的”,因为fn() {foo}
和第一个闭包都是零大小的类型。扩展答案:
如果此程序最终在Miri(未定义行为检查器)中执行,则最终将失败,因为已取消引用NULL指针。即使对于零大小的类型,也无法取消引用NULL指针。但是,未定义的行为可以做任何事情,因此编译器对此行为不做任何保证,这意味着它可以在以后的Rust版本中破坏。
error: Undefined Behavior: memory access failed: 0x0 is not a valid pointer --> src/main.rs:7:28 |7| let closure = unsafe { &mut *closure_ptr }; |^^^^^^^^^^^^^^^^^ memory access failed: 0x0 is not a valid pointer | = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information = note: inside `caller::<fn() {foo}>` at src/main.rs:7:28note: inside `create::<fn() {foo}>` at src/main.rs:13:5 --> src/main.rs:13:5 |13 | func_ptr(); | ^^^^^^^^^^note: inside `main` at src/main.rs:17:5 --> src/main.rs:17:5 |17 | create(foo); | ^^^^^^^^^^^
这个问题可以很容易地通过写来解决let closure_ptr = 1 as *mut F;
,然后它只会在第22行出现第二个闭包而失败。
error: Undefined Behavior: inbounds test failed: 0x1 is not a valid pointer --> src/main.rs:7:28 |7| let closure = unsafe { &mut *closure_ptr }; |^^^^^^^^^^^^^^^^^ inbounds test failed: 0x1 is not a valid pointer | = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information = note: inside `caller::<[closure@src/main.rs:22:12: 22:55 val:&i32]>` at src/main.rs:7:28note: inside `create::<[closure@src/main.rs:22:12: 22:55 val:&i32]>` at src/main.rs:13:5 --> src/main.rs:13:5 |13 | func_ptr(); | ^^^^^^^^^^note: inside `main` at src/main.rs:22:5 --> src/main.rs:22:5 |22 | create(|| println!("This will seg fault: {}", val)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
为什么它没有抱怨foo
或|| println!("Okay...")
?好吧,因为它们不存储任何数据。当指代一个函数时,您不会得到一个函数指针,而是一个表示该特定函数的零大小类型-这有助于进行单态化,因为每个函数都是不同的。可以从对齐的悬挂指针创建不存储任何数据的结构。
但是,如果您明确地说该函数是函数指针,create::<fn()>(foo)
则该程序将停止工作。