shmem_bind/
lib.rs

1use std::{
2    error::Error,
3    fmt::Display,
4    ops::{Deref, DerefMut},
5    ptr::{self, drop_in_place, NonNull},
6};
7
8use libc::{
9    c_char, c_void, close, ftruncate, mmap, munmap, shm_open, shm_unlink, MAP_SHARED, O_CREAT,
10    O_RDWR, PROT_WRITE, S_IRUSR, S_IWUSR,
11};
12
13pub struct Builder {
14    id: String,
15}
16
17impl Builder {
18    pub fn new(id: &str) -> Self {
19        Self {
20            id: String::from(id),
21        }
22    }
23
24    pub fn with_size(self, size: i64) -> BuilderWithSize {
25        BuilderWithSize { id: self.id, size }
26    }
27}
28
29pub struct BuilderWithSize {
30    id: String,
31    size: i64,
32}
33impl BuilderWithSize {
34    /// Ensures a shared memory using the specified `size` and `flink_id` and mapping it to the
35    /// virtual address of the process memory.
36    ///
37    /// In case of success, a `ShmemConf` is returned, representing the configuration of the
38    /// allocated shared memory.
39    ///
40    /// If the shared memory with the given `flink_id` is not present on the system, the call to
41    /// `open` would create a new shared memory and claims its ownership which is later used for
42    /// cleanup of the shared memory.
43    ///
44    /// # Examples
45    /// ```
46    /// use std::mem;
47    /// use shmem_bind::{self as shmem,ShmemError};
48    ///
49    /// fn main() -> Result<(),ShmemError>{
50    ///     // shared_mem is the owner
51    ///     let shared_mem = shmem::Builder::new("flink_test")
52    ///         .with_size(mem::size_of::<i32>() as i64)
53    ///         .open()?;
54    ///     {
55    ///         // shared_mem_barrow is not the owner
56    ///         let shared_mem_barrow = shmem::Builder::new("flink_test")
57    ///             .with_size(mem::size_of::<i32>() as i64)
58    ///             .open()?;
59    ///
60    ///         // shared_mem_barrow goes out of scope, the shared memory is unmapped from virtual
61    ///         // memory of the process.
62    ///     }
63    ///     // shared_mem goes out of scope, the shared memory is unmapped from virtual memory of
64    ///     // the process. after that, the shared memory is unlinked from the system.
65    ///     Ok(())
66    /// }
67    ///```
68    pub fn open(self) -> Result<ShmemConf, ShmemError> {
69        let (fd, is_owner) = unsafe {
70            let storage_id: *const c_char = self.id.as_bytes().as_ptr() as *const c_char;
71
72            // open the existing shared memory if exists
73            let fd = shm_open(storage_id, O_RDWR, S_IRUSR | S_IWUSR);
74
75            // shared memory didn't exist
76            if fd < 0 {
77                // create the shared memory
78                let fd = shm_open(storage_id, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
79                if fd < 0 {
80                    return Err(ShmemError::CreateFailedErr);
81                }
82
83                // allocate the shared memory with required size
84                let res = ftruncate(fd, self.size);
85                if res < 0 {
86                    return Err(ShmemError::AllocationFailedErr);
87                }
88
89                (fd, true)
90            } else {
91                (fd, false)
92            }
93        };
94
95        let null = ptr::null_mut();
96        let addr = unsafe { mmap(null, self.size as usize, PROT_WRITE, MAP_SHARED, fd, 0) };
97
98        Ok(ShmemConf {
99            id: self.id,
100            is_owner,
101            fd,
102            addr: NonNull::new(addr as *mut _).ok_or(ShmemError::NullPointerErr)?,
103            size: self.size,
104        })
105    }
106}
107
108/// A representation of a ***mapped*** shared memory.
109#[derive(Debug)]
110pub struct ShmemConf {
111    /// `flink_id` of the shared memory to be created on the system
112    id: String,
113    /// Wether or not this `ShmemConf` is the owner of the shared memory.
114    /// This field is set to true when the shared memory is created by this `ShmemConf`
115    is_owner: bool,
116    /// File descriptor of the allocated shared memory 
117    fd: i32,
118    /// Pointer to the shared memory
119    addr: NonNull<()>,
120    /// Size of the allocation
121    size: i64,
122}
123
124impl ShmemConf {
125    /// Converts `ShmemConf`'s raw pointer to a boxed pointer of type `T`.
126    ///
127    /// # Safety
128    ///
129    /// This function is unsafe because there is no guarantee that the referred T is initialized.
130    /// The caller must ensure that the value behind the pointer is initialized before use.
131    ///
132    /// # Examples
133    /// ```
134    /// use std::mem;
135    /// use shmem_bind::{self as shmem,ShmemError};
136    ///
137    /// type NotZeroI32 = i32;
138    ///
139    /// fn main() -> Result<(),ShmemError>{
140    ///     let shared_mem = shmem::Builder::new("flink_test_boxed")
141    ///         .with_size(mem::size_of::<NotZeroI32>() as i64)
142    ///         .open()?;
143    ///
144    ///     let boxed_val = unsafe {
145    ///         // the allocated shared_memory is not initialized and thus, not guaranteed to be a
146    ///         // valid `NotZeroI32`
147    ///         let mut boxed_val = shared_mem.boxed::<NotZeroI32>();
148    ///         // manually initialize the value in the unsafe block
149    ///         *boxed_val = 5;
150    ///         boxed_val
151    ///     };
152    ///
153    ///     assert_eq!(*boxed_val, 5);
154    ///
155    ///     let shared_mem = shmem::Builder::new("flink_test_boxed")
156    ///         .with_size(mem::size_of::<NotZeroI32>() as i64)
157    ///         .open()?;
158    ///
159    ///     let mut boxed_barrow_val = unsafe { shared_mem.boxed::<NotZeroI32>() };
160    ///
161    ///     assert_eq!(*boxed_barrow_val, 5);
162    ///
163    ///     // changes to boxed_barrow_val would reflect to boxed_val as well since they both point
164    ///     // to the same location.
165    ///     *boxed_barrow_val = 3;
166    ///     assert_eq!(*boxed_val, 3);
167    ///     
168    ///     Ok(())
169    /// }
170    ///
171    /// ```
172    pub unsafe fn boxed<T>(self) -> ShmemBox<T> {
173        ShmemBox {
174            ptr: self.addr.cast(),
175            conf: self,
176        }
177    }
178}
179
180/// # Safety
181///
182/// Shared memory is shared between processes.
183/// If it can withstand multiple processes mutating it, it can sure handle a thread or two!
184unsafe impl<T: Sync> Sync for ShmemBox<T> {}
185unsafe impl<T: Send> Send for ShmemBox<T> {}
186
187/// A safe and typed wrapper for shared memory
188///
189/// `ShmemBox<T>` wraps the underlying pointer to the shared memory and implements `Deref` and
190/// `DerefMut` for T
191///
192/// When ShmemBox<T> goes out of scope, the cleanup process of the shared memory is done.
193#[derive(Debug)]
194pub struct ShmemBox<T> {
195    ptr: NonNull<T>,
196    conf: ShmemConf,
197}
198
199impl<T> ShmemBox<T> {
200    /// Owns the shared memory. this would result in shared memory cleanup when this pointer goes
201    /// out of scope.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use std::mem;
207    /// use shmem_bind::{self as shmem,ShmemError,ShmemBox};
208    ///
209    /// fn main() -> Result<(),ShmemError>{
210    ///     // shared memory is created. `shared_mem` owns the shared memory
211    ///     let shared_mem = shmem::Builder::new("flink_test_own")
212    ///         .with_size(mem::size_of::<i32>() as i64)
213    ///         .open()?;
214    ///     let mut boxed_val = unsafe { shared_mem.boxed::<i32>() };
215    ///     
216    ///     // leaking the shared memory to prevent `shared_mem` from cleaning it up.
217    ///     ShmemBox::leak(boxed_val);
218    ///     
219    ///     // shared memory is already present on the machine. `shared_mem` does not own the
220    ///     // shared memory.
221    ///     let shared_mem = shmem::Builder::new("flink_test_own")
222    ///         .with_size(mem::size_of::<i32>() as i64)
223    ///         .open()?;
224    ///     let boxed_val = unsafe { shared_mem.boxed::<i32>() };
225    ///
226    ///     // own the shared memory to ensure it's cleanup when the shared_mem goes out of scope.
227    ///     let boxed_val = ShmemBox::own(boxed_val);
228    ///
229    ///     // boxed_val goes out of scope, the shared memory is cleaned up
230    ///     Ok(())
231    /// }
232    ///
233    /// ```
234    pub fn own(mut shmem_box: Self) -> Self {
235        shmem_box.conf.is_owner = true;
236
237        shmem_box
238    }
239
240    /// Leaks the shared memory and prevents the cleanup if the ShmemBox is the owner of the shared
241    /// memory.
242    /// This function is useful when you want to create a shared memory which lasts longer than the
243    /// process creating it.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use std::mem;
249    /// use shmem_bind::{self as shmem,ShmemError,ShmemBox};
250    ///
251    /// fn main() -> Result<(),ShmemError>{
252    ///     // shared memory is created. `shared_mem` owns the shared memory
253    ///     let shared_mem = shmem::Builder::new("flink_test_leak")
254    ///         .with_size(mem::size_of::<i32>() as i64)
255    ///         .open()?;
256    ///     let mut boxed_val = unsafe { shared_mem.boxed::<i32>() };
257    ///     
258    ///     // leaking the shared memory to prevent `shared_mem` from cleaning it up.
259    ///     ShmemBox::leak(boxed_val);
260    ///     
261    ///     // shared memory is already present on the machine. `shared_mem` does not own the
262    ///     // shared memory.
263    ///     let shared_mem = shmem::Builder::new("flink_test_leak")
264    ///         .with_size(mem::size_of::<i32>() as i64)
265    ///         .open()?;
266    ///     let boxed_val = unsafe { shared_mem.boxed::<i32>() };
267    ///
268    ///     // own the shared memory to ensure it's cleanup when the shared_mem goes out of scope.
269    ///     let boxed_val = ShmemBox::own(boxed_val);
270    ///
271    ///     // boxed_val goes out of scope, the shared memory is cleaned up
272    ///     Ok(())
273    /// }
274    ///
275    /// ```
276    pub fn leak(mut shmem_box: Self) {
277        // disabling cleanup for shared memory
278        shmem_box.conf.is_owner = false;
279    }
280}
281
282impl<T> Drop for ShmemBox<T> {
283    fn drop(&mut self) {
284        if self.conf.is_owner {
285            // # Safety
286            //
287            // if current process is the owner of the shared_memory,i.e. creator of the shared
288            // memory, then it should clean up after, that is, it should drop the inner T
289            unsafe { drop_in_place(self.ptr.as_mut()) };
290        }
291    }
292}
293impl Drop for ShmemConf {
294    fn drop(&mut self) {
295        // # Safety
296        //
297        // if current process is the owner of the shared_memory,i.e. creator of the shared
298        // memory, then it should clean up after.
299        // the procedure is as follow:
300        // 1. unmap the shared memory from processes virtual address space.
301        // 2. unlink the shared memory completely from the os if self is the owner
302        // 3. close the file descriptor of the shared memory
303        if unsafe { munmap(self.addr.as_ptr() as *mut c_void, self.size as usize) } != 0 {
304            panic!("failed to unmap shared memory from the virtual memory space")
305        }
306
307        if self.is_owner {
308            let storage_id: *const c_char = self.id.as_bytes().as_ptr() as *const c_char;
309            if unsafe { shm_unlink(storage_id) } != 0 {
310                panic!("failed to reclaim shared memory")
311            }
312        }
313
314        if unsafe { close(self.fd) } != 0 {
315            panic!("failed to close shared memory file descriptor")
316        }
317    }
318}
319
320impl<T> Deref for ShmemBox<T> {
321    type Target = T;
322
323    fn deref(&self) -> &Self::Target {
324        unsafe { self.ptr.as_ref() }
325    }
326}
327
328impl<T> DerefMut for ShmemBox<T> {
329    fn deref_mut(&mut self) -> &mut Self::Target {
330        unsafe { self.ptr.as_mut() }
331    }
332}
333
334#[derive(Debug)]
335pub enum ShmemError {
336    CreateFailedErr,
337    AllocationFailedErr,
338    NullPointerErr,
339}
340impl Display for ShmemError {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        write!(f, "{self:?}")
343    }
344}
345impl Error for ShmemError {}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn ownership() {
353        #[derive(Debug)]
354        struct Data {
355            val: i32,
356        }
357
358        let shmconf = Builder::new("test-shmem-box-ownership")
359            .with_size(std::mem::size_of::<Data>() as i64)
360            .open()
361            .unwrap();
362        let mut data = unsafe { shmconf.boxed::<Data>() };
363        assert_eq!(data.val, 0);
364        data.val = 1;
365
366        ShmemBox::leak(data);
367
368        let shmconf = Builder::new("test-shmem-box-ownership")
369            .with_size(std::mem::size_of::<Data>() as i64)
370            .open()
371            .unwrap();
372        let data = unsafe { shmconf.boxed::<Data>() };
373        assert_eq!(data.val, 1);
374
375        let _owned_data = ShmemBox::own(data);
376    }
377
378    #[test]
379    fn multi_thread() {
380        struct Data {
381            val: i32,
382        }
383        // create new shared memory pointer with desired size
384        let shared_mem = Builder::new("test-shmem-box-multi-thread.shm")
385            .with_size(std::mem::size_of::<Data>() as i64)
386            .open()
387            .unwrap();
388
389        // wrap the raw shared memory ptr with desired Boxed type
390        // user must ensure that the data the pointer is pointing to is initialized and valid for use
391        let data = unsafe { shared_mem.boxed::<Data>() };
392
393        // ensure that first process owns the shared memory (used for cleanup)
394        let mut data = ShmemBox::own(data);
395
396        // initiate the data behind the boxed pointer
397        data.val = 1;
398
399        let new_val = 5;
400        std::thread::spawn(move || {
401            // create new shared memory pointer with desired size
402            let shared_mem = Builder::new("test-shmem-box-multi-thread.shm")
403                .with_size(std::mem::size_of::<Data>() as i64)
404                .open()
405                .unwrap();
406
407            // wrap the raw shared memory ptr with desired Boxed type
408            // user must ensure that the data the pointer is pointing to is initialized and valid for use
409            let mut data = unsafe { shared_mem.boxed::<Data>() };
410            data.val = new_val;
411        })
412        .join()
413        .unwrap();
414        // assert that the new process mutated the shared memory
415        assert_eq!(data.val, new_val);
416    }
417}