我正在使用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编译器如何知道两者在可靠性方面的区别?
2条答案
按热度按时间0aydgbwb1#
当您拥有内存区域时,需要考虑多个参数:
bool
这样的类型,它是否用有效的值初始化,因为不是所有的位模式都是有效的。针对更棘手的方面,建议如下:
MaybeUninit
。Mutex
、AtomicXXX
或......。就是这样,这样做永远是正确的,不需要寻找“借口”或“例外”。
因此,在您的情况下:
dbf7pr2w2#
这与您问题的评论中提到的this post on the users forum非常相似。(下面是该帖子的一些链接:第一个电子第一个电子第二个电子第一个)
这里的答案并不是最有条理的,但似乎有四个关于未初始化内存的主要问题:
1.操作系统可能会覆盖它
1.阅读释放内存时存在安全漏洞
对于#1,我认为这不是一个问题,因为如果有另一个版本的FFI函数返回初始化内存而不是未初始化内存,那么它看起来与rust相同。
我想大多数人都理解第二点,这对
u32
来说不是问题。3可能是一个问题,但由于这是针对特定操作系统的,如果MacOS保证它不会这样做,您可以忽略此问题。
4可能是也可能不是未定义的行为,但它是非常不受欢迎的。这就是为什么即使rust认为它是一个有效的
u32
列表,您也应该将其视为未初始化。您不希望rust认为它是有效的。因此,即使对于u32
,您也应该使用MaybeUninit
。MaybeUninit
将指针强制转换为
MaybeUninit
的切片是正确的。但您的示例编写不正确。assume_init
返回T
,并且您不能将其赋值给[MaybeUninit<T>]
中的元素。修复:然后,将
MaybeUninit
的切片转换为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