Reputation: 73
I'm writing some GPU code for macOS using the metal
crate. In doing so, I allocate a Buffer
object by calling:
let buffer = device.new_buffer(num_bytes, MTLResourceOptions::StorageModeShared)
This FFIs to Apple's Metal API, which allocates a region of memory that both the CPU and GPU can access and the Rust wrapper returns a Buffer
object. I can then get a pointer to this region of memory by doing:
let data = buffer.contents() as *mut u32
In the colloquial sense, this region of memory is uninitialized. However, is this region of memory "uninitialized" in the Rust sense?
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;
}
Here I'm writing u32s to a region of memory returned to me by FFI. From the nomicon:
...The subtle aspect of this is that usually, when we use = to assign to a value that the Rust type checker considers to already be initialized (like x[i]), the old value stored on the left-hand side gets dropped. This would be a disaster. However, in this case, the type of the left-hand side is MaybeUninit<Box>, and dropping that does not do anything! See below for some more discussion of this drop issue.
None of the from_raw_parts
rules are violated and u32 doesn't have a drop method.
u32
s) before writing to it be sound (nonsense values aside)? The region of memory is valid and u32 is defined for all bit patterns.Now consider a type T
that does have a drop method (and you've done all the bindgen
and #[repr(C)]
nonsense so that it can go across FFI boundaries).
In this situation, should one:
.write()
?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() };
}
Furthermore, after initializing the region, how does the Rust compiler remember this region is initialized on subsequent calls to .contents()
later in the program?
In some cases, the buffer is the output of a GPU kernel and I want to read the results. All the writes occurred in code outside of Rust's control and when I call .contents()
, the pointer at the region of memory contains the correct uint32_t
values. This thought experiment should relay my concern with this.
Suppose I call C's malloc
, which returns an allocated buffer of uninitialized data. Does reading u32 values from this buffer (pointers are properly aligned and in bounds) as any type should fall squarely into undefined behavior.
However, suppose I instead call calloc
, which zeros the buffer before returning it. If you don't like calloc
, then suppose I have an FFI function that calls malloc, explicitly writes 0 uint32_t
types in C, then returns this buffer to Rust. This buffer is initialized with valid u32
bit patterns.
malloc
return "uninitialized" data while calloc
returns initialized data?Upvotes: 3
Views: 192
Reputation: 12812
This is very similar to this post on the users forum mentioned in the comment on your question. (here's some links from that post: 2 3)
The answers there aren't the most organized, but it seems like there's four main issues with uninitialized memory:
For #1, this seems to me to not be an issue, since if there was another version of the FFI function that returned initialized memory instead of uninitialized memory, it would look identical to rust.
I think most people understand #2, and that's not an issue for u32
.
#3 could be a problem, but since this is for a specific OS you may be able to ignore this if MacOS guarantees it does not do this.
#4 may or may not be undefined behavior, but it is highly undesirable. This is why you should treat it as uninitialized even if rust thinks it's a list of valid u32
s. You don't want rust to think it's valid. Therefore, you should use MaybeUninit
even for u32
.
MaybeUninit
It's correct to cast the pointer to a slice of MaybeUninit
. Your example isn't written correctly, though. assume_init
returns T
, and you can't assign that to an element from [MaybeUninit<T>]
. Fixed:
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());
}
Then, turning that slice of MaybeUninit
into a slice of T
:
let init_slice = unsafe { &mut *(as_slice as *mut [MaybeUninit<T>] as *mut [T]) };
Another issue is that &mut
may not be correct to have at all here since you say it's shared between GPU and CPU. Rust depends on your rust code being the only thing that can access &mut
data, so you need to ensure any &mut
are gone while the GPU accesses the memory. If you want to interlace rust access and GPU access, you need to synchronize them somehow, and only store *mut
while the GPU has access (or reacquire it from FFI).
The code is mainly taken from Initializing an array element-by-element in the MaybeUninit
doc, plus the very useful Alternatives section from transmute
. The conversion from &mut [MaybeUninit<T>]
to &mut [T]
is how slice_assume_init_mut
is written as well. You don't need to transmute like in the other examples since it is behind a pointer. Another similar example is in the nomicon: Unchecked Uninitialized Memory. That one accesses the elements by index, but it seems like doing that, using *
on each &mut MaybeUninit<T>
, and calling write
are all valid. I used write
since it's shortest and is easy to understand. The nomicon also says that using ptr
methods like write
is also valid, which should be equivalent to using MaybeUninit::write
.
There's some nightly [MaybeUninit]
methods that will be helpful in the future, like slice_assume_init_mut
Upvotes: 1
Reputation: 300079
There are multiple parameters to consider when you have an area of memory:
bool
whether it's initialized with valid values as not all bit-patterns are valid.Focusing on the trickier aspects, the recommendation is:
MaybeUninit
.Mutex
or AtomicXXX
or ....And that's it. Doing so will always be sound, no need to look for "excuses" or "exceptions".
Hence, in your case:
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);
Upvotes: 1