Read user type as &[u8; X]?

The goal I'm trying to achieve is to read a generic UserType which is Sized into a bunch of "backup" registers on an embedded device to keep the value stored between full resets of the device.

I'm trying to figure out how I can read the given UserType as a list of &[u8; X] without doing any form of memcpy. The best I found so far is core::mem::transmute but it says in the documentation that underneath the hood it does a memcpy.

NOTE: I'm fully aware this is unsafe and has aliasing and padding issues. This is for an embedded project and I'll be handling the padding for the "last bytes on the last 32bit register" myself.

If you transmute a reference, it will only memcpy the reference itself, which is no problem. But at that level, you might as well just do a pointer cast, &*(bytes as *const [u8; X] as *const UserType).

Make sure your types are #[repr(C)], otherwise the byte-wise layout is not guaranteed.

What do you mean exactly by that? In my case when the device resets I need to "reconstruct" the user type/value back from the 32bit registers. I'm hoping internally the type value byte strcuture, while possibly not defined cleanly (LLVM etc.) is at least constant :smiley:

Also you should use &[MaybeUninit<u8>; X] so that you dont cause UB if there are padding bytes. Notr that uninitalized u8 may be UB to construct, but this hasn't been decided yet. To be on the safe side use MaybeUninit<u8> instead of u8

1 Like

See this section in the reference about representations. The default makes no guarantees about alignment, padding, nor even field order. Yes, the compiler has to be internally consistent -- but this could change from run to run, and it has changed before between compiler versions. On the other hand, repr(C) notes:

The second purpose is to create types that you can soundly perform operations on that rely on data layout such as reinterpreting values as a different type.

Ah I see. That's OK tho. This is meant for resets of the device, it doesn't need to be consistent between re-uploads of the program. It's also not a serialization (strictly speaking), but more of a cache. Main use cases will be things like counters, measurement results being stored between hardware sleeps on the embedded device.

I just wanted to make it easier to work with than by accesing 16 individual u32 registers.

Actually testing this out now, but won't this fail on can't cast because it's not primitive types?

Obligatory mention: ::zerocopy

Sadly I need #[nostd]

Got this far with it:

Didn't do the coercing into 32bit chunks with padding yet, but seems promising. I didn't check if transmute does indeed do a copy in --release mode, but it does require Copy trait.

You can cast pointers regardless of what they're pointing to. You just can't cast non-primitive values.

OK I think this is the "best" solution so far. If you see a way to simplify further shoot but I think it's "good enough" (NOTE: we're not caring about padding here)

Reading and writing to padding is UB so even if you don't care about padding, you must make sure that either there are no padding bytes or that you don't read or write to them.

More importantly, transmuting a shared reference into a unique reference is UB (no matter how you do it). I.e. you are not allowed to write to a &T no matter how you do it. (Fix)

Well I do need to care about padding since the memory I'm writing the data to is a bunch of u32 registers. This means that it'll always be possible that the user data (e.g. type) will be using say just 1 byte of the last u32 register in line and when I'm trying to read it back directly from the registers I'd be writing out of bounds depending on alignment/padding.

Is there a way to force a type to be repr(C) for example? That'd make things a bit more defined.

No, but you could make a trait like so

unsafe trait IsReprC {}

And force user to implement it, and document that it is only safe to implement if the type has the layout requirements that you want.

1 Like