I expect that the compiler does not know that the hardware might change the values in buffer after writing its address into an mmio register and starting the task, possibly allowing the compiler to optimize out reads from the buffer since "it is never written to".
What would be the correct way to handle this? Do I need implement a wrapper which implements all reads from the buffer as 'volatile_read'? Is there a better way to achieve this?
UnsafeCell is a magic type that tells the compiler the memory may change in mysterious ways. I presume this could be expressed as a buffer of &[UnsafeCell<u8>] or &UnsafeCell<[u8]>, but I'm not sure what are the rules for going from/to a &mut [u8] slice with it.
I’m not an unsafe expert, but I think you’re looking for something like this:
let buffer = std::cell::Cell::from_mut(); // Tell the compiler the buffer might change
But it feels like UnsafeCell shouldn’t be required here, as you have a mutable reference already: the compiler will assume that no other code changes the buffer so that this code is free to. The only real question is how to tell the compiler that you’re giving that right to modify to the external hardware until the end of scope.
If you pass the buffer's raw pointer to the mmio register with a volatile write, then the compiler understands that the raw pointer has escaped and that it might be written to by unknown sources. The only danger is that you reassert uniqueness by touching the mutable reference, but as long as you don't touch it until the writes have finished, it is fine.
I think I would need to expose the volatile as part of the buffer type to the user, if possible I would like to prevent this.
I'm not convinced about this. If the compiler decided that no one writes into the buffer and removes the entire buffer (but still writes some address into the mmio register, the hardware would overwrite memory it is not supposed to.
Since you took your time I still want to answer your points:
The external system shouldn't write to the pointer without triggering another task, the only methods triggering those tasks always overwrite the pointer to a valid buffer
Both start_hardware_task_writing_to_buffer and busy_wait_for_task_to_finish do not inspect the buffer. Actually both are just a volatile write to a specific register.
I guess this is just a problem of me oversimplifying the code a bit. write_buffer_address_into_mmio_register only gets a raw pointer to the buffer and is marked unsafe, so it shouldn't need a drop guard. The rest are volatile writes. I would argue the entire function itself acts as the drop guard of the buffer as it only returns when the write has finished.
Again, probably caused by oversimplification. External access to the register would require unsafe code so this should be fine. Small side note: I don`t think there is something like a "MMIO operation". At least in my context "MMIO" just stands for "memory mapped input/output" which is just a specific way to implement communication between cpu and peripherals without requiring special cpu instructions. Maybe you meant a "DMA Transfer"?
I try to implement (most) of the states of the hardware in the typesystem. I still haven't figured out a few tricky ones in a safe manner though (looking at you "power saving by turning off ram sections").
Do you have any reference to this? This would obviously be perfect for this use case I could not find any documentation on this in the write_volatile section. I would have expected it there, since its a property of the volatile write which signals the escape of the whole buffer, if I understood your sentence right.
Thank you for the answer, but I'm not yet entirely convinced. Consider the following scenarios: function A takes a buffer &mut[u8], creates a raw pointer from it, and passes it to function B which then performs the volatile write.
How would it be obvious to the compiler that B performs a volatile write with the buffer address (without in lining B)?
Or is it a property of creating the raw pointer which actually makes the compiler assume it cannot reason about the buffer contents anymore?
It’s letting a raw pointer escape the scope of analysis. In your first scenario, assuming functions a and b are analyzed separately, a must assume that b might change the buffer, and b knows it might change because of the volatile write. If b gets inlined, then a can see the volatile write directly.