global-buffer 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- global_buffer-1.0.0/LICENSE +21 -0
- global_buffer-1.0.0/MANIFEST.in +4 -0
- global_buffer-1.0.0/PKG-INFO +211 -0
- global_buffer-1.0.0/README.md +177 -0
- global_buffer-1.0.0/pyproject.toml +57 -0
- global_buffer-1.0.0/setup.cfg +4 -0
- global_buffer-1.0.0/setup.py +19 -0
- global_buffer-1.0.0/src/global_buffer/__init__.py +46 -0
- global_buffer-1.0.0/src/global_buffer/_core.c +12665 -0
- global_buffer-1.0.0/src/global_buffer/_core.pyx +286 -0
- global_buffer-1.0.0/src/global_buffer/_version.py +1 -0
- global_buffer-1.0.0/src/global_buffer/buffer.py +181 -0
- global_buffer-1.0.0/src/global_buffer/codec.py +32 -0
- global_buffer-1.0.0/src/global_buffer/consumer.py +44 -0
- global_buffer-1.0.0/src/global_buffer/exceptions.py +23 -0
- global_buffer-1.0.0/src/global_buffer/gb_atomics.h +22 -0
- global_buffer-1.0.0/src/global_buffer/layout.py +142 -0
- global_buffer-1.0.0/src/global_buffer/notifier.py +55 -0
- global_buffer-1.0.0/src/global_buffer/reader.py +184 -0
- global_buffer-1.0.0/src/global_buffer/spec.py +67 -0
- global_buffer-1.0.0/src/global_buffer.egg-info/PKG-INFO +211 -0
- global_buffer-1.0.0/src/global_buffer.egg-info/SOURCES.txt +35 -0
- global_buffer-1.0.0/src/global_buffer.egg-info/dependency_links.txt +1 -0
- global_buffer-1.0.0/src/global_buffer.egg-info/requires.txt +7 -0
- global_buffer-1.0.0/src/global_buffer.egg-info/top_level.txt +1 -0
- global_buffer-1.0.0/tests/test_buffer_reader.py +171 -0
- global_buffer-1.0.0/tests/test_codec.py +28 -0
- global_buffer-1.0.0/tests/test_consumer.py +41 -0
- global_buffer-1.0.0/tests/test_core_ring.py +90 -0
- global_buffer-1.0.0/tests/test_crossproc.py +103 -0
- global_buffer-1.0.0/tests/test_extra.py +209 -0
- global_buffer-1.0.0/tests/test_layout.py +54 -0
- global_buffer-1.0.0/tests/test_notifier.py +49 -0
- global_buffer-1.0.0/tests/test_public_api.py +26 -0
- global_buffer-1.0.0/tests/test_spec.py +40 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Izzet Sezer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: global_buffer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cross-platform cross-process shared-memory ring buffer: zero-copy numpy arrays + pydantic messages, 0-CPU blocking callbacks.
|
|
5
|
+
Author: Izzet Sezer
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sezer-muhammed/GlobalBuffer
|
|
8
|
+
Project-URL: Repository, https://github.com/sezer-muhammed/GlobalBuffer
|
|
9
|
+
Project-URL: Documentation, https://github.com/sezer-muhammed/GlobalBuffer/tree/main/docs
|
|
10
|
+
Project-URL: Changelog, https://github.com/sezer-muhammed/GlobalBuffer/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/sezer-muhammed/GlobalBuffer/issues
|
|
12
|
+
Keywords: shared-memory,ipc,ring-buffer,numpy,pydantic,zero-copy
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: System :: Hardware
|
|
21
|
+
Classifier: Topic :: System :: Networking
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: Intended Audience :: Science/Research
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.23
|
|
28
|
+
Requires-Dist: msgspec>=0.18
|
|
29
|
+
Requires-Dist: pydantic>=2
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
32
|
+
Requires-Dist: Cython>=3.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# GlobalBuffer
|
|
36
|
+
|
|
37
|
+
Cross-platform, cross-process **named shared-memory buffer** for Python.
|
|
38
|
+
|
|
39
|
+
`import global_buffer as gb`
|
|
40
|
+
|
|
41
|
+
A writer publishes samples by name; any process on the same host attaches by name
|
|
42
|
+
and reads — either the newest sample or every sample in order, by blocking call or
|
|
43
|
+
background callback. Built for streams that mix **high frequency** (e.g. 200 Hz
|
|
44
|
+
numeric arrays) and **low frequency** (e.g. 1 Hz structured status messages)
|
|
45
|
+
without burning CPU on either end.
|
|
46
|
+
|
|
47
|
+
It fills the gap between `multiprocessing.shared_memory` (too low-level),
|
|
48
|
+
`UltraDict` (dated, pickle-based), and iceoryx2/eCAL (heavy native installs):
|
|
49
|
+
a pip-installable, pydantic-native, callback-driven buffer with a clean API and a
|
|
50
|
+
lock-free Cython hot path.
|
|
51
|
+
|
|
52
|
+
## Status
|
|
53
|
+
|
|
54
|
+
**v1.0.0.** Core is stable and covered by 70 tests (including a cross-process
|
|
55
|
+
no-torn-reads stress test), verified on macOS / CPython 3.14. Linux, Windows and
|
|
56
|
+
aarch64 (Jetson) are supported and wheels are configured, with broad CI
|
|
57
|
+
verification on those platforms in progress. See [`CHANGELOG.md`](CHANGELOG.md)
|
|
58
|
+
for known limitations.
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Two stream kinds, one API**
|
|
63
|
+
- **Array streams** — fixed dtype + shape numpy data, written and read **zero-copy**.
|
|
64
|
+
- **Message streams** — `pydantic` models on the public API, `msgspec` (msgpack) on the wire (~10× faster than pickle).
|
|
65
|
+
- **Last-value or in-order reads** — `latest()` jumps to the newest sample;
|
|
66
|
+
`next()` consumes every sample in order and reports `overruns` if a reader falls behind.
|
|
67
|
+
- **Lock-free, tear-free** — single-writer / multi-reader ring with a spare slot
|
|
68
|
+
(`capacity + 1`) plus a per-slot seqlock implemented with C11 atomics. No torn reads even at high rate.
|
|
69
|
+
- **Near-0-CPU wakeups** — readers block on an adaptive poll of the shared commit
|
|
70
|
+
counter; idle readers cost roughly one atomic load every couple of milliseconds.
|
|
71
|
+
- **Cross-platform** — Linux, macOS, Windows; ships as compiled wheels.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install global_buffer
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Wheels are published for CPython 3.9–3.13 on manylinux x86_64 / aarch64,
|
|
80
|
+
macOS (x86_64 + arm64) and Windows amd64. A source build needs a C11 compiler.
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
### Array stream (200 Hz, zero-copy)
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import global_buffer as gb
|
|
88
|
+
import numpy as np
|
|
89
|
+
|
|
90
|
+
# writer / owner
|
|
91
|
+
csi = gb.create(name="csi", schema=gb.ArraySpec(dtype="complex64", shape=(64, 4)),
|
|
92
|
+
capacity=8)
|
|
93
|
+
|
|
94
|
+
with csi.reserve() as slot: # slot is an ndarray view directly into shm
|
|
95
|
+
slot[:] = frame # fill in place — no copy
|
|
96
|
+
# or: csi.write(frame) # single-memcpy convenience form
|
|
97
|
+
|
|
98
|
+
# reader (any other process)
|
|
99
|
+
r = gb.attach("csi") # schema discovered from the segment
|
|
100
|
+
frame = r.latest() # newest committed sample
|
|
101
|
+
r.on_data(lambda sample, seq: process(sample), mode="latest") # bg thread
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Message stream (1 Hz, pydantic)
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import pydantic, global_buffer as gb
|
|
108
|
+
|
|
109
|
+
class Status(pydantic.BaseModel):
|
|
110
|
+
gain: float
|
|
111
|
+
cam_on: bool
|
|
112
|
+
|
|
113
|
+
status = gb.create(name="status", schema=Status, capacity=4, max_bytes=512)
|
|
114
|
+
status.write(Status(gain=1.2, cam_on=True))
|
|
115
|
+
|
|
116
|
+
rs = gb.attach("status", model=Status) # schema mismatch -> raises on attach
|
|
117
|
+
msg = rs.next(timeout=1.0) # -> validated Status instance
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### OO consumer
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
class CsiConsumer(gb.Consumer):
|
|
124
|
+
def callback(self): # framework sets self.data / self.seq
|
|
125
|
+
self.processed = heavy_process(self.data)
|
|
126
|
+
|
|
127
|
+
ob = CsiConsumer.attach("csi", mode="latest")
|
|
128
|
+
ob.start()
|
|
129
|
+
...
|
|
130
|
+
ob.stop()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Semantics
|
|
134
|
+
|
|
135
|
+
- `capacity` is the number of logical slots; the core allocates `capacity + 1` so
|
|
136
|
+
the writer never overwrites the slot a reader could currently be reading.
|
|
137
|
+
- A reader created with `attach()` starts at the newest sample present at attach time.
|
|
138
|
+
- `latest()` returns `None` on an empty buffer. `next(timeout=...)` raises
|
|
139
|
+
`gb.Empty` on timeout; without a timeout it blocks.
|
|
140
|
+
- `next()` accumulates `reader.overruns` when the writer laps the reader by more
|
|
141
|
+
than `capacity` samples (the reader then jumps to the oldest still-available sample).
|
|
142
|
+
- `reader.writer_alive` reflects a heartbeat the writer stamps on every write
|
|
143
|
+
(a writer silent for >2 s reads as not alive).
|
|
144
|
+
|
|
145
|
+
## Lifecycle
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
buf.close() # detach this handle (segment stays alive)
|
|
149
|
+
buf.unlink() # owner removes the segment
|
|
150
|
+
gb.unlink(name) # remove a segment by name (e.g. clean up after a crash)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
GlobalBuffer manages segment lifetime explicitly (it opts out of the
|
|
154
|
+
multiprocessing `resource_tracker` where supported, Python 3.13+), so a reader
|
|
155
|
+
exiting never unlinks the owner's segment.
|
|
156
|
+
|
|
157
|
+
## Platform support
|
|
158
|
+
|
|
159
|
+
| OS | Segment | Notification |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| Linux | `multiprocessing.shared_memory` (POSIX shm) | adaptive poll on commit counter |
|
|
162
|
+
| macOS | `multiprocessing.shared_memory` (POSIX shm) | adaptive poll on commit counter |
|
|
163
|
+
| Windows | `multiprocessing.shared_memory` (mem-mapped) | adaptive poll on commit counter |
|
|
164
|
+
|
|
165
|
+
> **Verification status.** Behaviour is verified on macOS today; the Linux/Windows
|
|
166
|
+
> CI matrix and aarch64 wheels are configured and will be exercised before those
|
|
167
|
+
> platforms are declared production-verified.
|
|
168
|
+
>
|
|
169
|
+
> **Note on notifications.** The current release uses adaptive polling of the
|
|
170
|
+
> shared commit counter for wakeups — fully portable, reliable on all three OSes,
|
|
171
|
+
> and near-0 CPU when idle (the poll interval backs off to ~2 ms). A true 0-CPU
|
|
172
|
+
> kernel-blocking backend (Linux `eventfd` / process-shared pthread condvar,
|
|
173
|
+
> Windows named semaphore) fits behind the same interface and is planned once it
|
|
174
|
+
> can be verified per-OS in CI. POSIX named semaphores were evaluated and dropped:
|
|
175
|
+
> they behave unreliably on macOS.
|
|
176
|
+
|
|
177
|
+
## Build from source
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
python -m pip install -U pip setuptools wheel Cython numpy msgspec pydantic
|
|
181
|
+
python setup.py build_ext --inplace
|
|
182
|
+
PYTHONPATH=src python -c "import global_buffer as gb; print(gb.__version__)"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Run the tests
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
PYTHONPATH=src python -m pytest tests -v # full suite
|
|
189
|
+
PYTHONPATH=src python -m pytest tests -m "not crossproc_slow" # skip the long stress test
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Or in Docker:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
docker build -t globalbuffer . && docker run --rm globalbuffer
|
|
196
|
+
docker compose up # two-process writer/reader demo
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Jetson / aarch64
|
|
200
|
+
|
|
201
|
+
Wheels are built for `manylinux aarch64`. Atomics and process spawn behaviour can
|
|
202
|
+
differ from x86; run the suite on the target device once as a smoke test.
|
|
203
|
+
|
|
204
|
+
## Design
|
|
205
|
+
|
|
206
|
+
Full documentation is in [`docs/`](docs/index.md); design rationale and the
|
|
207
|
+
on-disk segment layout are in [`docs/design.md`](docs/design.md).
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT © 2026 Izzet Sezer
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# GlobalBuffer
|
|
2
|
+
|
|
3
|
+
Cross-platform, cross-process **named shared-memory buffer** for Python.
|
|
4
|
+
|
|
5
|
+
`import global_buffer as gb`
|
|
6
|
+
|
|
7
|
+
A writer publishes samples by name; any process on the same host attaches by name
|
|
8
|
+
and reads — either the newest sample or every sample in order, by blocking call or
|
|
9
|
+
background callback. Built for streams that mix **high frequency** (e.g. 200 Hz
|
|
10
|
+
numeric arrays) and **low frequency** (e.g. 1 Hz structured status messages)
|
|
11
|
+
without burning CPU on either end.
|
|
12
|
+
|
|
13
|
+
It fills the gap between `multiprocessing.shared_memory` (too low-level),
|
|
14
|
+
`UltraDict` (dated, pickle-based), and iceoryx2/eCAL (heavy native installs):
|
|
15
|
+
a pip-installable, pydantic-native, callback-driven buffer with a clean API and a
|
|
16
|
+
lock-free Cython hot path.
|
|
17
|
+
|
|
18
|
+
## Status
|
|
19
|
+
|
|
20
|
+
**v1.0.0.** Core is stable and covered by 70 tests (including a cross-process
|
|
21
|
+
no-torn-reads stress test), verified on macOS / CPython 3.14. Linux, Windows and
|
|
22
|
+
aarch64 (Jetson) are supported and wheels are configured, with broad CI
|
|
23
|
+
verification on those platforms in progress. See [`CHANGELOG.md`](CHANGELOG.md)
|
|
24
|
+
for known limitations.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Two stream kinds, one API**
|
|
29
|
+
- **Array streams** — fixed dtype + shape numpy data, written and read **zero-copy**.
|
|
30
|
+
- **Message streams** — `pydantic` models on the public API, `msgspec` (msgpack) on the wire (~10× faster than pickle).
|
|
31
|
+
- **Last-value or in-order reads** — `latest()` jumps to the newest sample;
|
|
32
|
+
`next()` consumes every sample in order and reports `overruns` if a reader falls behind.
|
|
33
|
+
- **Lock-free, tear-free** — single-writer / multi-reader ring with a spare slot
|
|
34
|
+
(`capacity + 1`) plus a per-slot seqlock implemented with C11 atomics. No torn reads even at high rate.
|
|
35
|
+
- **Near-0-CPU wakeups** — readers block on an adaptive poll of the shared commit
|
|
36
|
+
counter; idle readers cost roughly one atomic load every couple of milliseconds.
|
|
37
|
+
- **Cross-platform** — Linux, macOS, Windows; ships as compiled wheels.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install global_buffer
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Wheels are published for CPython 3.9–3.13 on manylinux x86_64 / aarch64,
|
|
46
|
+
macOS (x86_64 + arm64) and Windows amd64. A source build needs a C11 compiler.
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
### Array stream (200 Hz, zero-copy)
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import global_buffer as gb
|
|
54
|
+
import numpy as np
|
|
55
|
+
|
|
56
|
+
# writer / owner
|
|
57
|
+
csi = gb.create(name="csi", schema=gb.ArraySpec(dtype="complex64", shape=(64, 4)),
|
|
58
|
+
capacity=8)
|
|
59
|
+
|
|
60
|
+
with csi.reserve() as slot: # slot is an ndarray view directly into shm
|
|
61
|
+
slot[:] = frame # fill in place — no copy
|
|
62
|
+
# or: csi.write(frame) # single-memcpy convenience form
|
|
63
|
+
|
|
64
|
+
# reader (any other process)
|
|
65
|
+
r = gb.attach("csi") # schema discovered from the segment
|
|
66
|
+
frame = r.latest() # newest committed sample
|
|
67
|
+
r.on_data(lambda sample, seq: process(sample), mode="latest") # bg thread
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Message stream (1 Hz, pydantic)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import pydantic, global_buffer as gb
|
|
74
|
+
|
|
75
|
+
class Status(pydantic.BaseModel):
|
|
76
|
+
gain: float
|
|
77
|
+
cam_on: bool
|
|
78
|
+
|
|
79
|
+
status = gb.create(name="status", schema=Status, capacity=4, max_bytes=512)
|
|
80
|
+
status.write(Status(gain=1.2, cam_on=True))
|
|
81
|
+
|
|
82
|
+
rs = gb.attach("status", model=Status) # schema mismatch -> raises on attach
|
|
83
|
+
msg = rs.next(timeout=1.0) # -> validated Status instance
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### OO consumer
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
class CsiConsumer(gb.Consumer):
|
|
90
|
+
def callback(self): # framework sets self.data / self.seq
|
|
91
|
+
self.processed = heavy_process(self.data)
|
|
92
|
+
|
|
93
|
+
ob = CsiConsumer.attach("csi", mode="latest")
|
|
94
|
+
ob.start()
|
|
95
|
+
...
|
|
96
|
+
ob.stop()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Semantics
|
|
100
|
+
|
|
101
|
+
- `capacity` is the number of logical slots; the core allocates `capacity + 1` so
|
|
102
|
+
the writer never overwrites the slot a reader could currently be reading.
|
|
103
|
+
- A reader created with `attach()` starts at the newest sample present at attach time.
|
|
104
|
+
- `latest()` returns `None` on an empty buffer. `next(timeout=...)` raises
|
|
105
|
+
`gb.Empty` on timeout; without a timeout it blocks.
|
|
106
|
+
- `next()` accumulates `reader.overruns` when the writer laps the reader by more
|
|
107
|
+
than `capacity` samples (the reader then jumps to the oldest still-available sample).
|
|
108
|
+
- `reader.writer_alive` reflects a heartbeat the writer stamps on every write
|
|
109
|
+
(a writer silent for >2 s reads as not alive).
|
|
110
|
+
|
|
111
|
+
## Lifecycle
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
buf.close() # detach this handle (segment stays alive)
|
|
115
|
+
buf.unlink() # owner removes the segment
|
|
116
|
+
gb.unlink(name) # remove a segment by name (e.g. clean up after a crash)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
GlobalBuffer manages segment lifetime explicitly (it opts out of the
|
|
120
|
+
multiprocessing `resource_tracker` where supported, Python 3.13+), so a reader
|
|
121
|
+
exiting never unlinks the owner's segment.
|
|
122
|
+
|
|
123
|
+
## Platform support
|
|
124
|
+
|
|
125
|
+
| OS | Segment | Notification |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Linux | `multiprocessing.shared_memory` (POSIX shm) | adaptive poll on commit counter |
|
|
128
|
+
| macOS | `multiprocessing.shared_memory` (POSIX shm) | adaptive poll on commit counter |
|
|
129
|
+
| Windows | `multiprocessing.shared_memory` (mem-mapped) | adaptive poll on commit counter |
|
|
130
|
+
|
|
131
|
+
> **Verification status.** Behaviour is verified on macOS today; the Linux/Windows
|
|
132
|
+
> CI matrix and aarch64 wheels are configured and will be exercised before those
|
|
133
|
+
> platforms are declared production-verified.
|
|
134
|
+
>
|
|
135
|
+
> **Note on notifications.** The current release uses adaptive polling of the
|
|
136
|
+
> shared commit counter for wakeups — fully portable, reliable on all three OSes,
|
|
137
|
+
> and near-0 CPU when idle (the poll interval backs off to ~2 ms). A true 0-CPU
|
|
138
|
+
> kernel-blocking backend (Linux `eventfd` / process-shared pthread condvar,
|
|
139
|
+
> Windows named semaphore) fits behind the same interface and is planned once it
|
|
140
|
+
> can be verified per-OS in CI. POSIX named semaphores were evaluated and dropped:
|
|
141
|
+
> they behave unreliably on macOS.
|
|
142
|
+
|
|
143
|
+
## Build from source
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python -m pip install -U pip setuptools wheel Cython numpy msgspec pydantic
|
|
147
|
+
python setup.py build_ext --inplace
|
|
148
|
+
PYTHONPATH=src python -c "import global_buffer as gb; print(gb.__version__)"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Run the tests
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
PYTHONPATH=src python -m pytest tests -v # full suite
|
|
155
|
+
PYTHONPATH=src python -m pytest tests -m "not crossproc_slow" # skip the long stress test
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Or in Docker:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
docker build -t globalbuffer . && docker run --rm globalbuffer
|
|
162
|
+
docker compose up # two-process writer/reader demo
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Jetson / aarch64
|
|
166
|
+
|
|
167
|
+
Wheels are built for `manylinux aarch64`. Atomics and process spawn behaviour can
|
|
168
|
+
differ from x86; run the suite on the target device once as a smoke test.
|
|
169
|
+
|
|
170
|
+
## Design
|
|
171
|
+
|
|
172
|
+
Full documentation is in [`docs/`](docs/index.md); design rationale and the
|
|
173
|
+
on-disk segment layout are in [`docs/design.md`](docs/design.md).
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT © 2026 Izzet Sezer
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel", "Cython>=3.0", "numpy>=1.23"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "global_buffer"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Cross-platform cross-process shared-memory ring buffer: zero-copy numpy arrays + pydantic messages, 0-CPU blocking callbacks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Izzet Sezer" }]
|
|
13
|
+
keywords = ["shared-memory", "ipc", "ring-buffer", "numpy", "pydantic", "zero-copy"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: System :: Hardware",
|
|
23
|
+
"Topic :: System :: Networking",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["numpy>=1.23", "msgspec>=0.18", "pydantic>=2"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/sezer-muhammed/GlobalBuffer"
|
|
31
|
+
Repository = "https://github.com/sezer-muhammed/GlobalBuffer"
|
|
32
|
+
Documentation = "https://github.com/sezer-muhammed/GlobalBuffer/tree/main/docs"
|
|
33
|
+
Changelog = "https://github.com/sezer-muhammed/GlobalBuffer/blob/main/CHANGELOG.md"
|
|
34
|
+
"Bug Tracker" = "https://github.com/sezer-muhammed/GlobalBuffer/issues"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["pytest>=7", "Cython>=3.0"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
package-dir = { "" = "src" }
|
|
41
|
+
packages = ["global_buffer"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.dynamic]
|
|
44
|
+
version = { attr = "global_buffer._version.__version__" }
|
|
45
|
+
|
|
46
|
+
[tool.cibuildwheel]
|
|
47
|
+
build = "cp39-* cp310-* cp311-* cp312-* cp313-*"
|
|
48
|
+
skip = "*-musllinux_i686 *_i686 pp*"
|
|
49
|
+
test-requires = ["pytest", "numpy", "msgspec", "pydantic"]
|
|
50
|
+
test-command = "pytest {project}/tests -m 'not crossproc_slow'"
|
|
51
|
+
archs = ["auto64"]
|
|
52
|
+
|
|
53
|
+
[tool.cibuildwheel.linux]
|
|
54
|
+
archs = ["x86_64", "aarch64"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
markers = ["crossproc_slow: long cross-process stress tests"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
from setuptools import Extension, setup
|
|
5
|
+
from Cython.Build import cythonize
|
|
6
|
+
|
|
7
|
+
if sys.platform == "win32":
|
|
8
|
+
extra = ["/std:c11", "/O2"]
|
|
9
|
+
else:
|
|
10
|
+
extra = ["-std=c11", "-O3"]
|
|
11
|
+
|
|
12
|
+
ext = Extension(
|
|
13
|
+
"global_buffer._core",
|
|
14
|
+
sources=["src/global_buffer/_core.pyx"],
|
|
15
|
+
include_dirs=[numpy.get_include(), "src/global_buffer"],
|
|
16
|
+
extra_compile_args=extra,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
setup(ext_modules=cythonize([ext], language_level=3))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""GlobalBuffer: cross-platform, cross-process shared-memory ring buffer.
|
|
2
|
+
|
|
3
|
+
Zero-copy numpy array streams and pydantic message streams, last-value or
|
|
4
|
+
in-order reads, and background callbacks. See the README for the full API.
|
|
5
|
+
"""
|
|
6
|
+
from ._version import __version__
|
|
7
|
+
from .buffer import GlobalBuffer, open_shm, shm_name
|
|
8
|
+
from .consumer import Consumer
|
|
9
|
+
from .exceptions import (BufferClosed, BufferExists, BufferNotFound, Empty,
|
|
10
|
+
GlobalBufferError, SchemaMismatch)
|
|
11
|
+
from .reader import Reader
|
|
12
|
+
from .spec import ArraySpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create(name, schema, capacity, max_bytes=None):
|
|
16
|
+
"""Create and own a named buffer (writer). ``schema`` is an
|
|
17
|
+
:class:`ArraySpec` or a ``pydantic.BaseModel`` subclass."""
|
|
18
|
+
return GlobalBuffer(name, schema, capacity, max_bytes=max_bytes)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def attach(name, model=None, poll_min=None, poll_max=None):
|
|
22
|
+
"""Attach to an existing named buffer (reader). For message buffers, pass
|
|
23
|
+
``model=`` to get validated instances and a schema-compatibility check.
|
|
24
|
+
|
|
25
|
+
``poll_max`` raises the wakeup poll-backoff cap (seconds); a larger value
|
|
26
|
+
lowers idle CPU when running many readers, at the cost of up to that much
|
|
27
|
+
extra wake latency (default ~2 ms). ``poll_min`` sets the busy floor."""
|
|
28
|
+
return Reader(name, model=model, poll_min=poll_min, poll_max=poll_max)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def unlink(name):
|
|
32
|
+
"""Remove a named segment by name (e.g. to clean up after a crash)."""
|
|
33
|
+
try:
|
|
34
|
+
shm = open_shm(shm_name(name))
|
|
35
|
+
except FileNotFoundError:
|
|
36
|
+
return
|
|
37
|
+
shm.close()
|
|
38
|
+
shm.unlink()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"__version__", "ArraySpec", "GlobalBuffer", "Reader", "Consumer",
|
|
43
|
+
"create", "attach", "unlink",
|
|
44
|
+
"GlobalBufferError", "Empty", "SchemaMismatch", "BufferClosed",
|
|
45
|
+
"BufferExists", "BufferNotFound",
|
|
46
|
+
]
|