I'm doing something with MaybeUninit
and FFI in Rust that seems to work, but I suspect may be unsound/relying on undefined behavior.
My aim is to have a struct MoreA
extend a struct A
, by including A
as an initial field. And then to call some C code that writes to the struct A
. And then finalize MoreA
by filling in its additional fields, based on what's in A
.
In my application, the additional fields of MoreA
are all integers, so I don't have to worry about assignments to them dropping the (uninitialized) previous values.
Here's a minimal example:
use core::fmt::Debug;
use std::mem::MaybeUninit;
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
struct A(i32, i32);
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
struct MoreA {
head: A,
more: i32,
}
unsafe fn mock_ffi(p: *mut A) {
// write doesn't drop previous (uninitialized) occupant of p
p.write(A(1, 2));
}
fn main() {
let mut b = MaybeUninit::<MoreA>::uninit();
unsafe { mock_ffi(b.as_mut_ptr().cast()); }
let b = unsafe {
let mut b = b.assume_init();
b.more = 3;
b
};
assert_eq!(&b, &MoreA { head: A(1, 2), more: 3 });
}
Is the code let b = unsafe { ... }
sound? It runs Ok and Miri doesn't complain.
But the MaybeUninit
docs say:
Moreover, uninitialized memory is special in that the compiler knows that it does not have a fixed value. This makes it undefined behavior to have uninitialized data in a variable even if that variable has an integer type, which otherwise can hold any fixed bit pattern.
Also, the Rust book says that Behavior considered undefined includes:
Producing an invalid value, even in private fields and locals. "Producing" a value happens any time a value is assigned to or read from a place, passed to a function/primitive operation or returned from a function/primitive operation. The following values are invalid (at their respective type):
... An integer (i*/u*) or ... obtained from uninitialized memory.
On the other hand, it doesn't seem possible to write to the more
field before calling assume_init
. Later on the same page:
There is currently no supported way to create a raw pointer or reference to a field of a struct inside MaybeUninit. That means it is not possible to create a struct by calling MaybeUninit::uninit::() and then writing to its fields.
If what I'm doing in the above code example does trigger undefined behavior, what would solutions be?
I'd like to avoid boxing the A value (that is, I'd like to have it be directly included in
MoreA
).I'd hope also to avoid having to create one
A
to pass tomock_ffi
and then having to copy the results intoMoreA
.A
in my real application is a large structure.
I guess if there's no sound way to get what I'm after, though, I'd have to choose one of those two fallbacks.
If struct A is of a type that can hold the bit-pattern 0 as a valid value, then I guess a third fallback would be:
- Start with
MaybeUninit::zeroed()
rather thanMaybeUninit::uninit()
.
let b = unsafe { ... }
code withlet b = unsafe { (*b.as_mut_ptr()).more = 3; b.assume_init() }
. The docs on MaybeUninit::as_mut_ptr only say that reading from the pointer or turning it into a ref are undefined, before the struct is initialized. That leaves open that writing to it is ok. (But in my application, that won't work because I need to set themore
field based on what's in the other fields, so I'd need to read and write.) – Airspeedmore
to 0, then callassume_init
, then mutate themore
field as needed. So far as I can understand the docs, this should be defined behavior (if my ffi call does indeed initialize the A part of the structure). – AirspeedMaybeUninit
. Given that your struct isrepr(C)
, you can use an intermediate struct for the purpose and then transmute it. Playground example. – Overgrowthunsafe { (*b.as_mut_ptr()).more = 3; b.asume_init() }
, where themore
field has an int type (so no issues about its being dropped). – Airspeedi32
. It would be UB if it was abool
or most other types. – Overgrowth