rust 在FFI的上下文中,“未初始化”是什么意思?

jdgnovmf  于 2023-02-19  发布在  其他
关注(0)|答案(2)|浏览(149)

我正在使用metal crate为macOS编写一些GPU代码。在此过程中,我通过调用以下命令分配了一个Buffer对象:

let buffer = device.new_buffer(num_bytes, MTLResourceOptions::StorageModeShared)

这个FFI连接到Apple的Metal API,后者分配一个CPU和GPU都可以访问的内存区域,Rust Package 器返回一个Buffer对象。然后,我可以通过以下操作获得一个指向该内存区域的指针:

let data = buffer.contents() as *mut u32

在通俗的意义上,这个内存区域是未初始化的。但是,在Rust意义上,这个内存区域是"未初始化的"吗?
∮这是声音吗?∮

let num_bytes = num_u32 * std::mem::size_of::<u32>();
let buffer = device.new_buffer(num_bytes, MTLResourceOptions::StorageModeShared);
let data = buffer.contents() as *mut u32;

let as_slice = unsafe { slice::from_raw_parts_mut(data, num_u32) };

for i in as_slice {
  *i = 42u32;
}

这里我将u32s写入FFI返回给我的内存区域。从nomicon
......这种情况的微妙之处在于,通常当我们使用=赋给Rust类型检查器认为已经初始化的值(如x [i])时,存储在左侧的旧值会被丢弃,这将是一场灾难,然而,在本例中,左侧的类型是MaybeUninit,丢弃它不会做任何事情!有关此跌落问题的更多讨论,请参见下文。 See below for some more discussion of this drop issue.
没有违反任何from_raw_parts规则,并且u32没有drop方法。

  • 不管怎样,这是声音吗?
  • 在写入之前从该区域读取(如u32 s)是否合理(无意义值除外)?内存区域有效,并且为所有位模式定义了u32。

最佳实践

现在考虑一个类型T,它确实有一个drop方法(您已经完成了所有bindgen#[repr(C)]的无意义操作,以便它可以跨越FFI边界)。
在这种情况下,应:

  • 通过用指针扫描区域并调用.write()?来初始化Rust中的缓冲区。
  • 执行:
let as_slice = unsafe { slice::from_raw_parts_mut(data as *mut MaybeUninit<T>, num_t) };

for i in as_slice {
  *i = unsafe { MaybeUninit::new(T::new()).assume_init() };
}

此外,在初始化该区域之后,Rust编译器如何记住该区域在程序中稍后对.contents()的后续调用中被初始化?
∮思想实验∮
在某些情况下,缓冲区是GPU内核的输出,我想读取结果。所有的写入都发生在Rust控制之外的代码中,当我调用.contents()时,内存区域的指针包含正确的uint32_t值。这个思想实验应该传达我对这一点的关注。
假设我调用C的malloc,它返回一个未初始化数据的已分配缓冲区,从这个缓冲区读取u32值(指针正确对齐并在边界内)作为任何类型都应该落入未定义行为。
然而,假设我调用calloc,它在返回缓冲区之前将缓冲区清零,如果你不喜欢calloc,那么假设我有一个FFI函数调用malloc,显式地在C中写入0个uint32_t类型,然后将这个缓冲区返回给Rust,这个缓冲区 * 是 * 用有效的u32位模式初始化的。

  • 从Rust的Angular 来看,malloc是否返回"未初始化"的数据,而calloc是否返回初始化的数据?
  • 如果情况不同,Rust编译器如何知道两者在可靠性方面的区别?
0aydgbwb

0aydgbwb1#

当您拥有内存区域时,需要考虑多个参数:

  • 它的大小是最明显的。
  • 它的一致性仍然有些明显。
  • 它是否被初始化--特别是对于bool这样的类型,它是否用有效的值初始化,因为不是所有的位模式都是有效的。
  • 无论是并发读/写。

针对更棘手的方面,建议如下:

  • 如果内存可能未初始化,请使用MaybeUninit
  • 如果内存可能同时被读/写,请使用同步方法--可以是MutexAtomicXXX或......。

就是这样,这样做永远是正确的,不需要寻找“借口”或“例外”。
因此,在您的情况下:

let num_bytes = num_u32 * std::mem::size_of::<u32>();
assert!(num_bytes <= isize::MAX as usize);

let buffer = device.new_buffer(num_bytes, MTLResourceOptions::StorageModeShared);

let data = buffer.contents() as *mut MaybeUninit<u32>;

//  Safety:
//  - `data` is valid for reads and writes.
//  - `data` points to `num_u32` elements.
//  - Access to `data` is exclusive for the duration.
//  - `num_u32 * size_of::<u32>() <= isize::MAX`.
let as_slice = unsafe { slice::from_raw_parts_mut(data, num_u32) };

for i in as_slice {
    i.write(42);  //  Yes you can write `*i = MaybeUninit::new(42);` too,
                  //  but why would you?
}

// OR with nightly:

as_slice.write_slice(some_slice_of_u32s);
dbf7pr2w

dbf7pr2w2#

这与您问题的评论中提到的this post on the users forum非常相似。(下面是该帖子的一些链接:第一个电子第一个电子第二个电子第一个)
这里的答案并不是最有条理的,但似乎有四个关于未初始化内存的主要问题:

  1. Rust假定它已初始化
  2. Rust假定内存是类型的有效位模式
    1.操作系统可能会覆盖它
    1.阅读释放内存时存在安全漏洞
    对于#1,我认为这不是一个问题,因为如果有另一个版本的FFI函数返回初始化内存而不是未初始化内存,那么它看起来与rust相同。
    我想大多数人都理解第二点,这对u32来说不是问题。

3可能是一个问题,但由于这是针对特定操作系统的,如果MacOS保证它不会这样做,您可以忽略此问题。

4可能是也可能不是未定义的行为,但它是非常不受欢迎的。这就是为什么即使rust认为它是一个有效的u32列表,您也应该将其视为未初始化。您不希望rust认为它是有效的。因此,即使对于u32,您也应该使用MaybeUninit

MaybeUninit

将指针强制转换为MaybeUninit的切片是正确的。但您的示例编写不正确。assume_init返回T,并且您不能将其赋值给[MaybeUninit<T>]中的元素。修复:

let as_slice = unsafe { slice::from_raw_parts_mut(data as *mut MaybeUninit<T>, num_t) };

for i in as_slice {
  i.write(T::new());
}

然后,将MaybeUninit的切片转换为T的切片:

let init_slice = unsafe { &mut *(as_slice as *mut [MaybeUninit<T>] as *mut [T]) };

另一个问题是&mut可能根本不正确,因为你说它是GPU和CPU共享的。Rust依赖于你的Rust代码是唯一可以访问&mut数据的东西,所以你需要确保当GPU访问内存时任何&mut都消失了。如果你想交错Rust访问和GPU访问,你需要以某种方式同步它们。并且仅在GPU具有访问权限时存储*mut(或从FFI重新获取)。

注解

代码主要取自MaybeUninit文档中的逐个元素初始化一个数组,以及transmute中非常有用的Alternatives部分。从&mut [MaybeUninit<T>]&mut [T]的转换也是slice_assume_init_mut的编写方式。你不需要像其他示例那样进行转换,因为它在指针后面。另一个类似的示例在nomicon中:Unchecked Uninitialized Memory。那个是通过索引访问元素的,但是看起来像是这样做的,在每个&mut MaybeUninit<T>上使用*,并且调用write都是有效的。我使用write是因为它最短并且容易理解。nomicon还说使用ptr方法像write也是有效的,这应当等同于使用MaybeUninit::write
有一些夜间[MaybeUninit]方法将来会很有用,比如slice_assume_init_mut

相关问题