How to mutex part of a structure (references to its elements)

Hi there,

given a structure like this:

#[repr(C)]
struct Ext {
    data1: [u8; 20],
    data2: [u8; 5],
}

This structure has a fixed format and can not be enhanced. However, the structure shall be able to be shared across threads and data1 and data2 shall be able to be mutually exclusive accessed independently from each other.
As I can‘t introduce Mutex on Ext I thought a wrapping structure would be fine, but how would I design such thing?

My first attempts does not compile as I need to store Ext as well as references to parts of Ext wrapped in a Mutex. Like so:

struct Wrapper {
    ext: Ext,
    data1: Mutex<&[u8; 20]>,
    data2: Mutex<&[u8; 5]>,
}

keeping the lifetime issues aside with the given definition I‘m not successful with any proper solution to achieve the goal that I‘d like to lock access to data1 independent of data2. If I store a &reference in the Mutex locking the same also returns a &Mut & reference as contained value but not the actual reference to the data as mutable right? But will a Mutex<&mut [u8; 20]> then return a &mut &mut reference when locked?

So any hint would be welcome. The solution should ensure that I can safely share the whole structure Ext across threads and also safely request Mutual exclusive access to independent members, given the definition of Ext cannot be changed as it has a fixed memory layout.

Thanks in advance.

Hmm. One possibility would be to separate the mutex from the data that it guards, and then ensure that the appropriate mutex is always locked when accessing the data. Unfortunately you'll need some unsafe code, since the compiler can no longer see which mutex corresponds to which data.

pub struct Wrapper {
    ext: UnsafeCell<Ext>,
    mutex1: Mutex<PhantomData<[u8; 20]>>,
    mutex2: Mutex<PhantomData<[u8; 5]>>,
}

Complete code in Playground

This seems a bit too complicated. I hope that someone else can come up with a simpler or safer way.

1 Like

That contains a tiny amount of data, just copy the contents from Ext to a Rust struct with the two mutexes. And if/when you need an Ext again, just lock the fields and make a new Ext. If that can't work I think we'll need a better explanation of what external API you're dealing with.

1 Like

Hey thanks to both of you for your first feedback. It seem not that obvious to solve :/.

But sure let's give some more insights, hoping for a good, safe and not to complex solution :wink:

The Ext structure given was a simplified example. In reality it covers roughly 1kB of data. Also "API" might not be the right term here. It's kind of a memory based "interface" between the CPU and the GPU of the Raspberry Pi. So the memory address of the Ext structure is handed over to the firmware running on the GPU and from this point onwards, both the CPU and GPU will update the contents of the structure. Who is actually "owning" the data is partly controlled with some flags inside the structure. However, it's quite crucial that the update of the contents of the structure need to happen "in place" at this very memory location, for this reason the option to copy the memory into an owned struct and than, when ready copy the data back. As the update of the Ext structure on the CPU side (the part I'm writing in Rust :wink: ) will happen in separate threads I'd like to ensure "full" safety by granting specific mutual exclusive access to the data only if a thread needed this as to ensure that not two threads are updating at the same time. Hope this may give some more insights what's going on to elaborate on a possible solution.

The only idea that comes to my mind would be to not directly secure the data with a mutex but rather use a related Semaphore to secure data access, but this feels a bit of an unnecessary "indirection".

Thx in advance for your time.

Rust and LLVM assume they see all modifications of Rust's types (all except UnsafeCell), and can make code behave as if nothing has been modified if the optimizer doesn't see any code that could be doing the modifications. It's especially dangerous for data behind a shared-immutable borrow. Any modifications to &[u8] are immediately Undefined Behavior. And modifications to &mut [u8] by another thread/another device break Rust exclusivity and mutable aliasing rules, so that's UB too.

So you probably can't use a regular Rust type for this at all. You'd need something like *mut u8 for the buffer and https://doc.rust-lang.org/stable/std/ptr/fn.read_volatile.html or [UnsafeCell<u8>].

1 Like

Hi thanks for the additional reference.
As the detailed data that is represented in memory is also expressed as some sort of structure this would require something like *mut SharedFoo - right ?
To adhere to Rust internal guarantees I need to wrap this raw pointer into an UnsafeCell like UnsafeCell<*mut SharedFoo> ?

If i would use just UnsafeCell<SharedFoo> and SharedFoo does have to have a fixed memory location, would this location preserved within UnsafeCell?

From an highlevel point of view the SharedFoo is currently created like this:

let shared_foo = unsafe { special_malloc(core::mem::size_of::<SharedFoo>, align_4kb) as * mut SharedFoo };

So usage of UnsafeCell would be:

let cell = UnsafeCell::new(shared_foo);

or should it be

let cell = unsafe { UnsafeCell::new(&mut *shared_foo) };

???
It has to be ensured the memory address the shared_foo data is located at does not change by this.

Unfortunately Rust does not have the concept of volatile memory, which is probably the best way to describe the memory region shared between CPU and GPU threads. Instead Rust has read_volatile() and write_volatile() operations. These are the only operations that are safe to use on the shared memory region, as they inform rustc that it does not have knowledge of read and write accesses by other entities (i.e. the GPU) that have access to the same items. As you should expect, use of these volatile access operations suppresses all optimizations relative to data in such memory areas.

2 Likes

You might find some valuable tips in the embedded book.

This sounds like a memory-mapped register. You need to use raw pointers and volatile read/write to access memory-mapped registers.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.