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}