shm

Crystal License Platform

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 viewsRegion(T) for a single typed value, Buffer for raw bytes with a streaming IO API.
  • Fiber- and process-safe — a local Sync::RWLock pairs with a futex-backed CrossLock in 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>/stat and the lock uses the futex syscall 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 of region, yields it to the one-parameter block, and writes the block's mutation back, all under a single write lock. Pass recover: true to run against a torn payload after an owner death instead of raising.

class SHM::Region(T)

ReferenceSHM::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-opened Segment.

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

ReferenceSHM::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::RWLock and @cross : CrossLock in 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

StructSHM::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

ReferenceSHM::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, IOBase) — 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 (BaseReader) — 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 (BaseWriter) — 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 (ExceptionSHM::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

  1. Fork it (https://github.com/shpeckman/shm/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

MIT

Contributors

Repository

shm

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 6 hours ago
  • July 22, 2026
License

MIT License

Links
Synced at

Thu, 23 Jul 2026 20:14:04 GMT

Languages