Rust FFI和别名:C -> Rust -> C -> Rust调用堆栈中使用C值进行别名处理:未定义行为?

3

我正试图理解 Rust 在以下情况下的别名规则:

假设我们在 C 中进行了一次内存分配。我们将指向此分配的指针传递给 Rust。Rust 函数对此分配执行某些操作,然后回调到 C 代码(没有任何参数),其中另一个 Rust 函数以同样的分配为参数被调用。目前,让我们假设只有第一个 Rust 函数获得可变引用。

调用栈如下:

Some C Code (owns data)
  Rust(pointer as &mut)
    Some C code (does not get parameters from Rust)
      Rust(pointer as &)

作为一个简单的例子,假设有以下两个文件:test.c
#include <stdio.h>
#include <stdlib.h>

void first_rust_function(int * ints);
void another_rust_function(const int * ints);
int * arr;
void called_from_rust() {
        another_rust_function(arr);
}
int main(int argc, char ** argv) {
        arr = malloc(3*sizeof(int));
        arr[0]=3;
        arr[1]=4;
        arr[2]=53;
        first_rust_function(arr);
        free(arr);
}

test.rs

use std::os::raw::c_int;
extern "C" { fn called_from_rust(); }
#[no_mangle]
pub extern "C" fn first_rust_function(ints : &mut [c_int;3]) {
    ints[1] = 7;
    unsafe { called_from_rust() };
}
#[no_mangle]
pub extern "C" fn another_rust_function(ints : &[c_int;3]) {
    println!("Second value: {}", ints[1])
}

(为了完整性:执行此代码会打印“Second value: 7”)
请注意,从Rust调用C的回调函数(`called_from_rust()`)没有任何参数。因此,Rust编译器不具有任何人可能从指向的值中读取的信息。
我的直觉告诉我这是未定义行为,但我不确定。
我快速查看了Stacked Borrows,并且该模型被违反了。在上面的示例中,只有保护程序Rule (protector)被破坏,但如果first_rust_function(ints : &mut [c_int;3])在调用called_from_rust()之后仍然使用ints,则还将违反其他规则。
然而,我没有找到任何官方文档说明Stacked Borrows是Rust编译器使用的别名模型,并且在Stacked Borrows下考虑为未定义的所有内容都实际上在Rust中是未定义的。朴素地看,这看起来足够类似于将&mut强制转换为&,因此它可能实际上是合理的,但是鉴于called_from_rust()没有将引用作为参数传递,我不认为这种推理适用。
这带我来到实际问题:
1. 上述代码是否会引起未定义的行为(为什么或为什么不会)? 2. 如果是未定义的:如果called_from_rust()将指针作为参数并将其传递下去,行为是否定义良好:void called_from_rust(const int * i) { another_rust_function(i); }? 3. 如果两个Rust函数都使用&mut [c_int;3]会怎样?

C语言绑定应该为任何可变引用参数包括restrict,以绑定Rust函数。 - Aiden4
1个回答

3
上述代码是否会导致未定义的行为?
是的,您违反了Rust的指针别名规则。依赖堆栈借用规则有些可疑,因为正如您所暗示的那样,我认为它尚未被正式采用为Rust的内存访问模型(即使它只是对当前语义的形式化)。然而,LLVM的noalias属性是一个实际和具体的规定,Rust编译器在&mut参数上使用它。
“noalias”表示在函数执行期间,基于参数或返回值的指针值访问的内存位置不会通过不基于参数或返回值的指针值访问。...

因此,由于您在another_rust_function中通过一个非基于first_rust_function中的ints的指针访问了ints [1],这是一种违规行为。鉴于这种未定义的行为,我相信编译器有权使代码输出“第二个值:4”。


如果called_from_rust()将指针作为参数并将其传递给void called_from_rust(const int * i) { another_rust_function(i); },那么行为是否定义良好?
是的,这将使其定义良好。您可以看到,因为Rust借用检查器可以在called_from_rust()中看到该值,并防止在该调用周围不当使用ints
如果两个Rust函数都使用&mut [c_int;3],会怎样呢?
如果您使用了上面的修复方法,其中第二次借用是基于第一次的,那么就没有问题。但如果没有使用这种方法,那么情况就更糟了。

非常感谢!我不确定自己是否完全理解了一切,但这个确认让我感到放心。 - soulsource
noalias 比 Stacked Borrows 标准化程度 _低_。 noalias 是一种实现细节,而 Stacked Borrows 虽然尚未正式标准化,但已经得到了认可。 - Chayim Friedman
“noalias是一项实现细节” - 当然,但它确实被使用了,并且已经记录了它的使用情况。在Rust没有正式规范任何内容的情况下,它可以作为指针别名的未定义行为的实用指南。 - kmdreko
Stacked Borrows比noalias更严格,因此,虽然破坏noalias是UB,但我认为破坏Stacked Borrows也是UB的(而Miri遵循这个模型)。 - Chayim Friedman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接