shm
Fiber-safe, cross-process shared-memory primitives for Crystal.
shm maps a POSIX shared-memory segment (shm_open/mmap) and layers a readers-writer lock over it that coordinates both fibers within a process (a local Sync::RWLock) and separate processes (a futex-backed cross-process lock). The cross-process lock detects and recovers from a peer that died while holding a lock, using PID plus start-time liveness so a recycled PID never masks a dead owner.
Two views sit on top of a segment: Region for a single typed value, and Buffer for raw bytes with an IO streaming interface.
Features
- Two views —
Region(T)for a single typed value,Bufferfor raw bytes with a streamingIOAPI. - Fiber- and process-safe — a local
Sync::RWLockpairs with a futex-backedCrossLockin one place. - Crash recovery — a peer that dies mid-write is detected and reaped; readers/writers can repair the torn payload instead of corrupting silently.
- Type-stamped segments — a segment remembers the size and identity of its type; reopening it as a mismatched type raises
LayoutError. - Zero-copy access — reads and writes go straight to the mapped payload.
Requirements
Linux only. The recovery path reads
/proc/<pid>/statand the lock uses thefutexsyscall directly.
- Crystal
>= 1.0.0
Installation
Add the dependency to your shard.yml:
dependencies:
shm:
github: shpeckman/shm
version: ~> 1.0.0
Then run:
shards install
Quick start
require "shm"
@[Extern]
struct Counter
property n : Int64
def initialize
@n = 0_i64
end
end
SHM::Region(Counter).create("/demo", destroy: true) do |region|
region.set(Counter.new)
SHM.update(region) { |c| c.n += 1 }
region.get.n # => 1
end
Usage
Segments
A segment is a named region of shared memory. The name is a POSIX shared-memory object name; a leading / is added if you omit one, so "counter" and "/counter" refer to the same segment.
.create makes a new segment and fails if it already exists, .open attaches to an existing segment and fails if it is missing, and .open_or_create attaches if present and otherwise creates.
Every value type stored in a Region must have a well-defined C memory layout, so it must be an @[Extern] struct or a primitive. A segment created for one type is stamped with that type's size and identity; opening it as a differently-sized type raises LayoutError instead of silently corrupting.
Only the process that created a segment should destroy it. Segment#close detaches the current handle, and Segment#unlink removes the segment name so it is freed once every handle closes.
Regions
# Producer process
region = SHM::Region(Counter).create("/counter")
region.set(Counter.new)
# Any process
region = SHM::Region(Counter).open("/counter")
region.update { |c| c.n += 1 }
region.get.n
# Conditional update — return nil to abandon the write
region.try_update { |c| c.n < 100 ? Counter.new.tap { |x| x.n = c.n + 1 } : nil }
# Direct pointer access under a write lock
region.with_ptr { |ptr| ptr.value.n += 1 }
Buffers
SHM::Buffer.create("/log", 4096) do |buf|
buf.set("hello shared world")
buf.string # => "hello shared world"
# Streaming IO
buf.open_write { |io| io.puts("line one") }
buf.each_line { |line| puts line }
end
Crash recovery
If a process dies while holding the write lock, the payload may be half-written. The next process to take the lock detects the dead owner, reclaims the lock, and marks the segment inconsistent, so the next read or write raises InconsistentError unless recovery was requested.
begin
region.write { |ptr| ptr.value.n += 1 }
rescue SHM::InconsistentError
region.repair { |ptr| ptr.value = Counter.new }
end
Pass recover: true to run a block on the torn payload directly instead of raising, then call mark_consistent once the payload is sound again.
API Reference
module SHM
Top-level namespace and entry point.
Constants
VERSION = "1.0.0"
Macros
update(region, recover = false, &block)— reads the value out ofregion, yields it to the one-parameter block, and writes the block's mutation back, all under a single write lock. Passrecover: trueto run against a torn payload after an owner death instead of raising.
class SHM::Region(T)
Reference → SHM::Region(T) · includes SHM::Locked
A view over a segment holding a single typed value T. T must have a well-defined C memory layout — an @[Extern] struct or a primitive.
Class methods
| Method | Description |
|---|---|
.create(name : String) : Region(T) |
Creates a new segment sized for T; fails if the name exists. |
.create(name, *, destroy = false, &) |
Block form; tears the segment down on exit, unlinking it when destroy: true. |
.open(name : String) : Region(T) |
Attaches to an existing segment; raises NotFoundError if missing. |
.open(name, *, destroy = false, &) |
Block form of .open. |
.open_or_create(name : String) : Region(T) |
Attaches if present, otherwise creates sized for T. |
.open_or_create(name, *, destroy = false, &) |
Block form of .open_or_create. |
Constructors
.new(segment : Segment)— wraps an already-openedSegment.
Instance methods
| Method | Description |
|---|---|
#get : T |
Returns the current value. |
#set(value : T) : Nil |
Writes value under a write lock. |
#read(recover = false, &) |
Yields the current value and a recovered flag under a read lock. |
#write(recover = false, &) |
Yields the value pointer and a recovered flag under a write lock. |
#update(&) |
Yields the current value and writes back whatever the block returns. |
#try_update(&) : Bool |
Yields the value; returns false and abandons the write if the block returns nil, else writes and returns true. |
#with_ptr(recover = false, &) |
Yields the value pointer under a write lock. |
#value_ptr : Pointer(T) |
Returns a raw pointer to the value in shared memory. |
#repair(&) |
Runs the block against the pointer with recovery enabled, then clears the inconsistency flag. |
#clear : Nil |
Zero-fills the value in place under a write lock. |
#segment : Segment |
Returns the underlying Segment. |
class SHM::Buffer
Reference → SHM::Buffer · includes SHM::Locked
A view over a segment holding raw bytes, with block-scoped accessors and an IO streaming interface.
Class / constructor methods
| Method | Description |
|---|---|
.create(name, size) : Buffer |
Creates a new segment of size bytes; fails if the name exists. |
.create(name, size, *, destroy = false, &) |
Block form. |
.open(name) : Buffer |
Attaches to an existing segment. |
.open(name, *, destroy = false, &) |
Block form. |
.open_or_create(name, size) : Buffer |
Attaches if present, otherwise creates. |
.open_or_create(name, size, *, destroy = false, &) |
Block form. |
.new(segment : Segment) |
Wraps an already-opened Segment. |
Instance methods
| Method | Description |
|---|---|
#get : Bytes |
Returns a copy of the stored bytes, capped at content_length. |
#set(bytes : Bytes) : Nil |
Writes bytes, raising IO::Error on overflow. |
#set(string : String) : Nil |
Writes the UTF-8 bytes of string. |
#string : String |
Returns the stored bytes as a String. |
#read(recover = false, &) |
Yields a read-only slice and a recovered flag. |
#write(recover = false, &) |
Yields a writable slice and a recovered flag. |
#open_read(recover = false, &) |
Yields a Stream::Reader under a read lock, closing it on exit. |
#open_write(recover = false, truncate = true, &) |
Yields a Stream::Writer under a write lock, closing it on exit. |
#reader(recover = false) : Stream::Reader |
Returns a Stream::Reader with locks held; caller owns closing it. |
#writer(recover = false, truncate = true) : Stream::Writer |
Returns a Stream::Writer with locks held; caller owns closing it. |
#each_line(recover = false, &) : Nil |
Opens a reader and yields each line of the payload. |
#content_length : UInt64 |
Returns the number of meaningful bytes currently stored. |
#size : UInt64 |
Returns the payload capacity. |
#clear : Nil |
Zero-fills the payload and resets content_length to 0. |
#to_s(io : IO) : Nil |
Appends the stored string to io. |
#segment : Segment |
Returns the underlying Segment. |
module SHM::Locked
The locking protocol shared by the segment views, Region and Buffer.
Each view pairs a process-local Sync::RWLock (fiber safety) over a CrossLock (process safety). This module owns that pairing and the owner-death guard, so the recovery contract lives in exactly one place. Block-scoped critical sections go through #guarded_read/#guarded_write; the manual-lifetime streaming paths go through #acquire_read/#acquire_write, which leave both locks held and return a release proc.
Including types must set
@local : Sync::RWLockand@cross : CrossLockin their own initializer.
| Method | Description |
|---|---|
#consistent? : Bool |
Returns whether no unrecovered owner death is outstanding. |
#mark_consistent : Nil |
Clears the inconsistency flag after the caller has repaired the payload. |
#close : Nil |
Detaches this handle from the segment. |
#unlink : Nil |
Removes the segment name, destroying it once every handle closes. |
struct SHM::CrossLock
Struct → SHM::CrossLock
The futex-backed cross-process readers-writer lock. Detects a dead owner by polling and reaps it via PID/start-time liveness.
Constants
RECOVERY_POLL_NS = 50000000_i64
RECOVERY_POLLS = 4
| Method | Description |
|---|---|
.new(state : Pointer(LockState)) |
Wraps the shared lock state. |
#lock_read : Bool |
Acquires a read lock, reaping a dead writer if detected. Returns whether recovery occurred. |
#lock_write : Bool |
Acquires the write lock, reaping dead writers and readers as needed. Returns whether recovery occurred. |
#unlock_read : Nil |
Releases a read lock, waking waiters when the last reader leaves. |
#unlock_write : Nil |
Releases the write lock and wakes waiters. |
#consistent? : Bool |
Returns whether the segment has no unrecovered owner death. |
#mark_consistent : Nil |
Marks the segment consistent. |
module SHM::Liveness
PID and start-time liveness checks. Reads /proc/<pid>/stat without yielding to the event loop, so it is safe on the recovery path.
| Method | Description |
|---|---|
#alive?(pid) : Bool |
Whether pid names a live, non-zombie process. |
#same_process?(pid, start) : Bool |
Whether pid is alive and, when known, has the given start time — the guard against a recycled PID. |
#self_identity : Tuple(LibC::Int, UInt64) |
The current process's {pid, start_time}. |
#start_time(pid) : UInt64 |
Process start time (field 22 of /proc/<pid>/stat), or 0. |
#raw_state(pid) : Char? |
Process state character, or nil if the process is gone. |
#zombie?(pid) : Bool |
Whether pid is in the zombie state. |
class SHM::Segment
Reference → SHM::Segment
The mapped POSIX shared-memory segment: the mmap'd region, its header, and the payload.
| Method | Description |
|---|---|
.create(name, payload_size) : Segment |
Creates a new segment exclusively; fails if the name exists. |
.open(name) : Segment |
Attaches to an existing segment. |
.open_or_create(name, payload_size) : Segment |
Attaches if present, otherwise creates. |
#payload : Pointer(UInt8) |
Pointer to the payload (past the header). |
#payload_size : UInt64 |
Payload capacity. |
#total_size : UInt64 |
Total mapped size (header plus payload). |
#content_length : UInt64 / #content_length=(v) : Nil |
Read/write the meaningful-byte count in the header. |
#to_slice : Bytes |
Writable Bytes view of the payload. |
#read_only_slice : Bytes |
Read-only Bytes view of the payload. |
#stamp_type(size, hash) : Nil |
Stamps the type identity on first use, or raises LayoutError on mismatch. |
#cross_lock : CrossLock |
A CrossLock over this segment's lock state. |
#name : String |
The segment name. |
#close : Nil |
Unmaps and closes this handle (idempotent). |
#unlink : Nil |
Removes the segment name. |
module SHM::Stream
The streaming IO implementation layered over a segment.
SHM::Stream::Base (abstract, IO → Base) — owns close semantics: on #close it runs on_close, releases the cross lock, and calls the local-release proc if present. Provides #closed?.
SHM::Stream::Reader (Base → Reader) — a read-only IO over the payload, bounded by content length.
| Method | Description |
|---|---|
#read(slice) : Int32 |
Reads up to slice.size bytes; returns 0 at end of data. |
#length : UInt64 |
Readable byte count. |
#pos : UInt64 |
Current read position. |
#remaining : UInt64 |
Unread byte count. |
#rewind : Nil |
Resets the read position to 0. |
#write(slice) : Nil |
Raises IO::Error — read-only. |
SHM::Stream::Writer (Base → Writer) — a write-only IO over the payload, bounded by capacity. truncate: true (default) resets content on open.
| Method | Description |
|---|---|
#write(slice) : Nil |
Writes slice, raising IO::Error on overflow, and advances content_length. |
#capacity : UInt64 |
Total writable capacity. |
#pos : UInt64 |
Current write position. |
#space : UInt64 |
Remaining writable space. |
#rewind : Nil |
Resets the write position and content length to 0. |
#read(slice) : Int32 |
Raises IO::Error — write-only. |
Errors
All errors inherit from SHM::Error (Exception → SHM::Error), which also provides .from_errno(message : String) : Error for wrapping failed syscalls.
| Error | Raised when |
|---|---|
SHM::ExistsError |
An exclusive create hits a name that already exists. |
SHM::NotFoundError |
Opening a segment name that does not exist. |
SHM::LayoutError |
The segment can't hold the requested type, or its stamped identity doesn't match. |
SHM::InconsistentError |
A lock is acquired but the previous owner died mid-write, so the payload may be torn. Catch it to repair, or pass recover: true to work on the torn payload. |
Development
git clone https://github.com/shpeckman/shm
cd shm
shards install
crystal spec
Contributing
- Fork it (https://github.com/shpeckman/shm/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
Contributors
- shpeckman — creator and maintainer
shm
- 0
- 0
- 0
- 0
- 0
- about 6 hours ago
- July 22, 2026
MIT License
Thu, 23 Jul 2026 20:14:04 GMT