当前位置: 首页 > 知识库问答 >
问题:

为什么Rust允许通过空指针调用函数?

呼延河
2023-03-14

我在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() {
    caller::<F>();
}

fn main() {
    create(foo);
    
    create(|| println!("Okay..."));
    
    let val = 42;
    create(|| println!("This will seg fault: {}", val));
}

我无法解释为什么调用foo的方法是在调用方(…)中投射空指针 到类型为F实例。我本以为函数只能通过相应的函数指针来调用,但鉴于指针本身为空,显然不能这样。话虽如此,我显然误解了Rust类型系统的一个重要部分。

操场上的例子


共有3个答案

庄欣然
2023-03-14

鉴于生锈是建立在LLVM之上的,并且您所做的是保证UB的,您可能会碰到类似https://kristerw.blogspot.com/2017/09/why-undefined-behavior-may-call-never.html.这是安全生锈能够消除所有UB的许多原因之一。

楚弘益
2023-03-14

fn foo(){...}的类型不是函数指针fn(),它实际上是特定于foo的唯一类型。只要您携带该类型(这里是F),编译器就知道如何调用它,而不需要任何额外的指针(这种类型的值不携带数据)。不捕获任何东西的闭包也是如此。只有当最后一个闭包试图查找val时,它才会变得危险,因为您将一个0放在(大概)指向val的指针应该在的地方。

您可以通过size_of观察到这一点,在前两次调用中,闭包的大小为零,但在最后一次调用中,闭包中捕获的东西的大小为8(至少在操场上)。如果大小为0,则程序不必从NULL指针加载任何内容。

指向引用的NULL指针的有效强制转换仍然是未定义的行为,但这是因为类型欺诈,而不是因为内存访问欺诈:拥有真正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!
    }
}

翁烨霖
2023-03-14

这个程序实际上根本没有构造函数指针——它总是直接调用foo和这两个闭包。

每个Rust函数,无论是闭包还是fn项,都有一个唯一的匿名类型。此类型根据需要实现Fn/FnMut/FnOnce特征。fn项的匿名类型是零大小的,就像没有捕获的闭包类型一样。

因此,表达式create(foo)foo的类型实例化create的参数F——这不是函数指针类型fn(),而是一个匿名的零大小类型,仅用于foo。在错误消息中,rustc调用此类型fn(){foo},您可以看到此错误消息。

内部create::

最后,在调用者中:

同样的逻辑也适用于闭包||println!("好的..."),它像foo有一个匿名的零大小类型,这次称为类似于[closure@src/main.rs: 2:14:2:36]

第二个闭包就不那么幸运了——它的类型不是零大小的,因为它必须包含对变量val的引用。这次,FnMut::call_mut(closure)实际上需要取消引用closure来完成它的工作。所以它崩溃了2

1像这样构造空引用在技术上是未定义的行为,因此编译器不对该程序的总体行为做出promise。但是,将0替换为具有F对齐方式的其他“地址”,可以避免像fn(){foo}这样的零大小类型的问题,并给出相同的行为!)

2同样,构造一个空(或悬挂)引用是实际承担责任的操作——在那之后,任何事情都会发生。一个段落错误只是一种可能性-一个未来版本的rustc,或者在一个稍微不同的程序上运行的相同版本,可能会做完全不同的事情!

 类似资料:
  • 可能重复: 通过空类指针调用类方法 显然,不会有人打电话给他们。这是标准吗?或者只是一些编译器优化,因为这个指针没有在show()成员函数中使用?

  • 为什么允许使用memcpy指针更改常量变量? 此代码: 编译时只包含一个警告: 警告:传递“memcpy”的参数 1 将丢弃指针目标类型中的“const”限定符 [默认启用] 但运行得很好,并更改了常量变量的值。

  • 问题内容: 除了JSONP,为什么要遵循相同的域策略? 问题答案: 出于安全原因,已实施“同源起源策略”;引用维基百科的相关句子: 这种机制对现代Web应用程序具有特殊的意义,因为Web服务器广泛依赖于HTTP cookie来维护经过身份验证的用户会话,因为服务器基于HTTP cookie信息进行操作以揭示敏感信息或执行状态更改操作。 必须在客户端维护不相关站点提供的内容之间的严格分隔,以防止丢失

  • 问题内容: 如JDK文档中所指定,Hashtable不允许空键或值。HashMap允许一个null键和任意数量的null值。为什么是这样? 问题答案: Hashtable是较老的类,通常不鼓励使用Hashtable。也许他们看到了对null键的需求,更重要的是对null值的需求,并将其添加到HashMap实现中。 HashMap是较新的,并且具有更高级的功能,这些基本上只是对Hashtable功能

  • 通过将函数传递给函数call by pointer方法call by pointer将参数的地址复制到形式参数中。 在函数内部,该地址用于访问调用中使用的实际参数。 这意味着对参数所做的更改会影响传递的参数。 要通过指针传递值,参数指针将像任何其他值一样传递给函数。 因此,您需要将函数参数声明为指针类型,如以下函数swap() ,该函数swap()其参数指向的两个整数变量的值。 // functi

  • 问题内容: 我刚接触PHP,但是多年来我一直在使用类似的语言进行编程。我被以下内容弄糊涂了: 它产生了语法错误:这就是调用。 但这很好用: 碰了一会儿之后,我被告知您不能在默认属性中调用函数。你必须在做。我的问题是:为什么?这是“功能”还是草率的实现?有什么根据? 问题答案: 编译器代码建议这是设计使然,尽管我不知道其背后的官方原因是什么。我也不确定要可靠地实现此功能需要花费多少精力,但是目前完成