[FFI] converting *mut c_void to an owned struct in idiomatic way?

I am trying to implement a CLI which communicates with a running process through shared memory via libc::shmget, etc API.
My intension is to create a struct that represent the layout of the shared memory and expose a set of safe APIs to rw to this share memory.

struct SHMRegion {
  .... // layout not relevant here
};

impl SHMRegion {
    pub fn new() -> Result<Self, Error<String>> {
        unsafe {
            // some libc shm* calls
            ...
            let ptr = shmat(id, NULL, 0);
            // ??? should return a Self ???
        }
    }
}

How can a pointer be transformed into an owned struct? Is it valid in the first place?

What kind of semantics do you want, and what guarantees does the long-running process guarantee about shared memory?

If you're OK with having data that's accurate as of the call, but will become out of date in the future, I would recommend having SHMRegion implement Clone and using (&*ptr).clone().

If you want continually updating memory, that will be a bit trickier. A direct struct, like SHMRegion, is stored by-value on the stack wherever it exists. If you want to return an SHMRegion that represents the current data, and you don't want to copy it, I would recommend making a wrapper struct around a pointer to the actual data, and returning that. Something like

struct SHMView {
    inner: NonNull<SHMRegion>,
}
// ...
impl SHMRegion {
    pub fn new() -> Result<SHMView, Error<String>> {
        // ...
        let ptr = // ...
        SHMView {
            inner: NonNull::new(ptr).expect("shmat should not return null pointer")
        }
}

Then you'd provide accessor methods on SHMView which do the appropriate synchronization to prevent race methods with the long-running process and then manually read the fields of the inner SHMRegion. I'd recommend NonNull (see its docs), but *mut SHMRegion or *const SHMRegion would also be reasonable.


As for being valid, I believe it is. You will want to ensure you're avoiding all race conditions with shared memory writing and reading, but I assume you're handling that in some way.

If the CLI and the long-running process are different binaries, you will need #[repr(C)] on SHMRegion to guarantee that it has the same memory layout. You'll probably want that anyways, but maybe you could get away with not having it if they're literally the same program?

And as always for this kind of thing, the nomicon is an excellent resource for ensuring you're doing unsafe things soundly. Data Layout - The Rustonomicon might be useful, as well as Races - The Rustonomicon (this is about in-process data races, but most or all of it applies to what you're doing as well since you're sharing data between processes).

Making this code correct won't be a particularly small endeavor, but it's definitely doable. If you're not locked in, there are some alternatives that might be easier to implement safely (https://www.reddit.com/r/rust/comments/9d7pen/ipc_what_solutions_exist/ is a relatively recent post on this) but that's just if you're looking for a general solution. Shared memory isn't bad for this, you just have to ensure the code is sound.

1 Like

Got working code with the view approach. Thanks a lot for the detailed answer!

1 Like