Skip to content

Commit 53345ce

Browse files
authored
docs(fspy): document shared-memory facade ownership semantics (#521)
## Motivation Define how shared-memory owners and opened views behave before replacing the platform backends. ## Changes - Documents the behavior of `create`, `open`, owner drop, and opened-view lifetime. - Changes `open` to take only the identifier and derive the mapped size from the operating-system object. - Removes the redundant shared-memory size from `ChannelConf`. - Explains that Windows may keep a section name openable after owner drop. - Explains how the channel lock file prevents late sender creation on every platform.
1 parent cb580c2 commit 53345ce

4 files changed

Lines changed: 56 additions & 18 deletions

File tree

crates/fspy_shared/src/ipc/channel/mod.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ pub struct ChannelConf {
5151
lock_file_path: Box<NativeStr>,
5252
#[wincode(with = "ArcStrSchema")]
5353
shm_id: Arc<str>,
54-
shm_size: usize,
5554
}
5655

5756
/// Creates a mpsc IPC channel with one receiver and a `ChannelConf` that can be passed around processes and used to create multiple senders
@@ -65,11 +64,8 @@ pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> {
6564

6665
let shm = fspy_shm::create(capacity)?;
6766

68-
let conf = ChannelConf {
69-
lock_file_path: lock_file_path.as_os_str().into(),
70-
shm_id: shm.id().into(),
71-
shm_size: capacity,
72-
};
67+
let conf =
68+
ChannelConf { lock_file_path: lock_file_path.as_os_str().into(), shm_id: shm.id().into() };
7369

7470
let receiver = Receiver::new(lock_file_path, shm)?;
7571
Ok((conf, receiver))
@@ -87,7 +83,7 @@ impl ChannelConf {
8783
let lock_file = File::open(self.lock_file_path.to_cow_os_str())?;
8884
lock_file.try_lock_shared()?;
8985

90-
let shm = fspy_shm::open(&self.shm_id, self.shm_size)?;
86+
let shm = fspy_shm::open(&self.shm_id)?;
9187
// SAFETY: `shm` is a freshly opened shared memory region with valid pointer and size.
9288
// Exclusive write access is ensured by the shared file lock held by this sender.
9389
let writer = unsafe { ShmWriter::new(shm) };

crates/fspy_shared/src/ipc/channel/shm_io.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ mod tests {
680680
let cmd = command_for_fn!(
681681
(shm_name.clone(), child_index),
682682
|(shm_name, child_index): (String, usize)| {
683-
let shm = fspy_shm::open(&shm_name, SHM_SIZE).unwrap();
683+
let shm = fspy_shm::open(&shm_name).unwrap();
684684
// SAFETY: `shm` is a freshly opened shared memory region with a valid
685685
// pointer and size. Concurrent write access is safe because `ShmWriter`
686686
// uses atomic operations.

crates/fspy_shm/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# `fspy_shm`
2+
3+
`fspy_shm` is the private shared-memory layer used by fspy IPC channels. It gives the channel one API for creating a mapping, passing its identifier to another process, and opening additional views of the same bytes.
4+
5+
`fspy_shm` exposes only the operations used by fspy. Callers must treat an identifier as a string and must not depend on a platform's naming scheme.
6+
7+
## API
8+
9+
The public API is defined in [`src/lib.rs`](src/lib.rs).
10+
11+
| API | Contract |
12+
| ----------------- | ---------------------------------------------------------------------------------- |
13+
| `create(size)` | Creates a non-empty mapping and returns its unique owner. |
14+
| `open(id)` | Opens another view of the mapping identified by `id`. |
15+
| `Shm::id()` | Returns the identifier to send to another process. |
16+
| `Shm::len()` | Returns the mapped size. |
17+
| `Shm::as_ptr()` | Returns a mutable raw pointer to the first byte. |
18+
| `Shm::as_slice()` | Returns a shared slice. The caller must prevent mutation for the slice's lifetime. |
19+
20+
`Shm` does not synchronize memory access. The fspy channel combines it with atomic frame headers and a lock file. Senders hold a shared file lock while writing. The receiver takes the exclusive lock before reading, which waits for existing senders and rejects new ones.
21+
22+
## Ownership semantics
23+
24+
`create` returns the only owner. `open` returns non-owning views.
25+
26+
- While the owner is alive, a process that knows the identifier can open the mapping.
27+
- An opened view remains usable after the owner is dropped. Its operating system mapping keeps the underlying bytes alive.
28+
- After the owner is dropped, new opens behave differently by platform. POSIX removes the name. Windows can continue accepting opens by section name until the final handle or view is closed.
29+
30+
The channel hides that difference with its lock file. [`ChannelConf::sender`](../fspy_shared/src/ipc/channel/mod.rs) opens and locks the receiver's exact lock-file path before it calls `fspy_shm::open`. The receiver removes that path before dropping the owner, so a sender that starts later fails before opening shared memory.
31+
32+
## Backend boundary
33+
34+
At this point in the stack, `fspy_shm` delegates mapping creation and opening to the [`shared_memory`](https://crates.io/crates/shared_memory) crate. Because callers use only the API above, later changes can replace the backend on each platform without changing the channel protocol.

crates/fspy_shm/src/lib.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Behavior-neutral shared-memory facade for fspy channels.
1+
#![doc = include_str!("../README.md")]
22

33
use std::io;
44

@@ -9,7 +9,11 @@ pub struct Shm {
99
inner: Shmem,
1010
}
1111

12-
/// Creates a shared-memory mapping of `size` bytes.
12+
/// Creates a shared-memory mapping of `size` bytes and returns its owner.
13+
///
14+
/// Dropping the returned owner stops new [`open`] calls from being guaranteed
15+
/// to succeed, while views that are already open stay usable (see the
16+
/// [ownership semantics](crate)).
1317
///
1418
/// # Errors
1519
///
@@ -23,13 +27,17 @@ pub fn create(size: usize) -> io::Result<Shm> {
2327
Ok(Shm { inner })
2428
}
2529

26-
/// Opens the shared-memory mapping identified by `id`.
30+
/// Opens a view of the shared-memory mapping identified by `id`.
31+
///
32+
/// Guaranteed to succeed only while the mapping's owner is alive; the
33+
/// returned view stays usable independently of the owner afterwards (see the
34+
/// [ownership semantics](crate)).
2735
///
2836
/// # Errors
2937
///
3038
/// Returns an error if the mapping does not exist or cannot be mapped.
31-
pub fn open(id: &str, size: usize) -> io::Result<Shm> {
32-
let conf = ShmemConf::new().size(size).os_id(id);
39+
pub fn open(id: &str) -> io::Result<Shm> {
40+
let conf = ShmemConf::new().os_id(id);
3341
#[cfg(target_os = "windows")]
3442
let conf = conf.allow_raw(true);
3543

@@ -89,7 +97,7 @@ mod tests {
8997
// SAFETY: No writes occur while this slice is borrowed.
9098
assert!(unsafe { owner.as_slice() }.iter().all(|byte| *byte == 0));
9199

92-
let opened = open(owner.id(), SIZE).unwrap();
100+
let opened = open(owner.id()).unwrap();
93101
assert_eq!(opened.id(), owner.id());
94102
assert_eq!(opened.len(), SIZE);
95103

@@ -105,7 +113,7 @@ mod tests {
105113
write_byte(&owner, 0, 17);
106114

107115
let command = command_for_fn!(owner.id().to_owned(), |id: String| {
108-
let opened = open(&id, SIZE).unwrap();
116+
let opened = open(&id).unwrap();
109117
assert_eq!(read_byte(&opened, 0), 17);
110118
write_byte(&opened, SIZE - 1, 29);
111119
});
@@ -119,20 +127,20 @@ mod tests {
119127
let id = owner.id().to_owned();
120128
drop(owner);
121129

122-
assert!(open(&id, SIZE).is_err());
130+
assert!(open(&id).is_err());
123131
}
124132

125133
#[test]
126134
fn opened_mapping_survives_owner_drop() {
127135
let owner = create(SIZE).unwrap();
128136
let id = owner.id().to_owned();
129-
let opened = open(&id, SIZE).unwrap();
137+
let opened = open(&id).unwrap();
130138
write_byte(&owner, 0, 17);
131139
drop(owner);
132140

133141
// Windows keeps the named object alive while an opened view exists.
134142
#[cfg(not(target_os = "windows"))]
135-
assert!(open(&id, SIZE).is_err());
143+
assert!(open(&id).is_err());
136144
assert_eq!(read_byte(&opened, 0), 17);
137145
write_byte(&opened, SIZE - 1, 29);
138146
assert_eq!(read_byte(&opened, SIZE - 1), 29);

0 commit comments

Comments
 (0)