A simple Python library for RPC inter-process communication using shared memory and POSIX semaphores.
# Clone and enter repo
git clone https://github.com/nunoatgithub/shm-rpc-bridge.git
cd shm-rpc-bridge
# Option A: pip editable install (simple)
pip install -e .
# Option B: create a conda env from `environment.yml` (calls pip install)
conda env create -f environment.yml
conda activate shm-rpc-bridgeOn Linux, instead of POSIX semaphores, futexes can be used. However, they offer no measurable benefit to this library in terms of performance or stability and may actually be less stable. Use with caution; the code base toggles to this mode automatically when constructed with
USE_FUTEX=1 pip install -e .- Python 3.8 or higher
- Linux/MacOS/BSD with POSIX shared memory and semaphore support
posix-ipclibrary (installed automatically)orjsonlibrary (installed automatically)
from shm_rpc_bridge import RPCServer
# Create server
server = RPCServer("my_service")
# Register methods
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}!"
server.register("add", add)
server.register("greet", greet)
# Start serving (blocks until stopped)
server.start()from shm_rpc_bridge import RPCClient
# Connect to server
with RPCClient("my_service") as client:
# Make RPC calls
result = client.call("add", a=5, b=3)
print(f"5 + 3 = {result}") # Output: 5 + 3 = 8
greeting = client.call("greet", name="Alice")
print(greeting) # Output: Hello, Alice!βββββββββββββββ βββββββββββββββ
β Client β β Server β
β Process β β Process β
ββββββββ¬βββββββ ββββββββ¬βββββββ
β β
β 1. Serialize request (JSON) β
β 2. Write to shared memory β
β 3. Signal with semaphore β
ββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Shared Memory Region β
β βββββββββββββββββββββββββββββββββ-ββ β
β β Request Buffer (ClientβServer)β β
β β Response Buffer (ServerβClient)β β
β ββββββββββββββββββββββββββββββββββ-β β
ββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 4. Read from shared memory β
β 5. Deserialize & execute β
β 6. Serialize result β
β 7. Write response β
β 8. Signal completion β
ββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 9. Read response β
β 10. Deserialize result β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
- POSIX Shared Memory Buffers: Two buffers (request/response) for bidirectional communication
- POSIX Semaphores: Producer-consumer pattern for synchronization
- JSON Serialization: Given the generic nature of the RPC contract proposed by this API, json (with orjson) is the absolute best possible. I tested most of the alternatives (e.g.protobuf, capnproto, cysimdjson), but the presence of generic blobs in the request and response always forces a generic form of serialization before serializing the root object, so unless you use json for the entire structure, it's always json + other proto on top => slower. If you consider other more specialized RPC contracts, a fork from this repo with a quicker data layer would certainly make sense.
Some benchmarks are included to help understand performance characteristics.
Comparison of direct in-memory calls vs this library :
./benchmark/run_benchmark.shπ Full benchmark details β
Comparison of this library with gRPC (Unix domain sockets and TCP/IP):
./benchmark/vs_grpc/run_benchmark.shπ Full benchmark details β
class RPCServer:
def __init__(
self,
name: str,
buffer_size: int = SharedMemoryTransport.DEFAULT_BUFFER_SIZE,
timeout: float | None = None
)
def register(self, name: str, func: Callable) -> None:
"""Register a method for RPC calls."""
def register_function(self, func: Callable) -> Callable:
"""Decorator to register a method."""
def start(self) -> None:
"""Start the server (blocking)."""
def stop(self) -> None:
"""Stop the server."""
def close(self) -> None:
"""Clean up resources."""class RPCClient:
def __init__(
self,
name: str,
buffer_size: int = SharedMemoryTransport.DEFAULT_BUFFER_SIZE,
timeout: float | None = 5.0
)
def call(self, method: str, **params) -> Any:
"""Make an RPC call to the server."""
def close(self) -> None:
"""Clean up resources."""class RPCError(Exception):
"""Base exception for RPC errors."""
class RPCTimeoutError(RPCError):
"""Raised when an operation times out."""
class RPCMethodError(RPCError):
"""Raised when a remote method call fails."""
class RPCTransportError(RPCError):
"""Raised when transport layer fails."""
class RPCSerializationError(RPCError):
"""Raised when serialization/deserialization fails."""Complete working examples are provided in the examples/ directory:
- Calculator Service: A simple calculator with add, subtract, multiply, divide operations
- Accumulator Service: A stateful accumulator that maintains a running total per client
Each RPC channel creates two shared memory regions:
Request Buffer (Client β Server):
ββββββββββββββββββββββββββββββββββββββββββ
β Size (4 bytes) β JSON Message (N bytes)β
ββββββββββββββββββββββββββββββββββββββββββ
Response Buffer (Server β Client):
ββββββββββββββββββββββββββββββββββββββββββ
β Size (4 bytes) β JSON Message (N bytes)β
ββββββββββββββββββββββββββββββββββββββββββ
Four POSIX semaphores per channel:
request_empty: Counts empty slots in request bufferrequest_full: Counts full slots in request bufferresponse_empty: Counts empty slots in response bufferresponse_full: Counts full slots in response buffer
- Same-host only: Shared memory requires processes on the same machine
- POSIX systems: Requires POSIX semaphore support (Linux, macOS, BSD)
- Buffer size: Messages must fit in configured buffer
- No encryption: Data in shared memory is not encrypted (same-host trust model)
- Single channel: Each client-server pair uses one channel (no connection pooling)
- No threading: The server registers signal handlers that automate the deletion of resources on SIGTERM and SIGINT. Due to Python's known limitation about registering signal handlers in threads, the server cannot be spawned in threads, only processes.
- Synchronous only: Can't leverage async I/O
Server must be started before clients connect. Ensure server is running:
ps aux | grep your_server_scriptIncrease buffer size when creating client/server:
Run the cleanup utility:
python util/cleanup_ipc.pypip install -e ".[dev]"In addition to Python dependencies, workflow validation requires act, a tool to run GitHub Actions locally.
This is is NOT a Python package and cannot be installed via pip or listed in pyproject.toml. Each developer must install it separately on their system.
See https://nektosact.com/installation/
The project supports Python versions 3.8 through 3.13 on Linux and macOS. The Linux implementation has two transport variants: POSIX-based and futex-based.
Workflow: .github/workflows/ci.yml
The CI runs automatically on every push to master and tests all Python versions (3.8-3.13) on both ubuntu-latest and macos-latest.
Jobs:
test: Runs pytest across all OS/Python combinationslint: Runs ruff linting once (Python 3.8, Linux only)type-check: Runs mypy type checking once (Python 3.8, Linux only)
For feature branch development, you can manually trigger CI with filters:
- Push your branch:
git push origin my-feature - Go to GitHub β Actions β "CI" β "Run workflow"
- Select your branch from dropdown
- Choose filters:
- OS:
all,ubuntu-latest, ormacos-latest - Python version:
allor specific version (3.8-3.13) - Debug: Enable SSH access via tmate for interactive debugging
- OS:
- Click "Run workflow"
This allows you to:
- Test support for a different operating system than yours
- Test specific OS/Python combinations without running the full matrix
- Debug issues interactively by SSH-ing into the runner
Tip: Use git commit --amend + git push --force to iterate on your branch without polluting commit history.
macOS cannot legally or technically be containerized on non-Apple hardware due to licensing restrictions. The only way to validate macOS support is:
- CI with macOS runners (GitHub Actions runs on actual Apple hardware)
- Local macOS machine (your own Mac or cloud macOS VM)
Since you can't run macOS in Docker on Linux:
- Develop locally on Linux, run Linux tests (both POSIX and futex variants if desired)
- Push to a branch and manually trigger CI with macOS filter
- Check GitHub Actions for macOS job results
- Iterate based on macOS logs if issues arise
The CI tests both Linux transport variants (POSIX and futex) as well as the macOS POSIX implementation.
| Task | Command |
|---|---|
| Run all tests locally | pytest |
| Test single Python version | tox -e py38 (or py39, py310, etc.) |
| Lint code | tox -e lint |
| Type check | tox -e type |
| Format code | tox -e format |
| Validate CI workflows | tox -e workflow |
| Run full test matrix locally | tox |
| Test on macOS (from Linux) | Push branch β manually trigger CI with macOS filter |
| Test on Linux (from macOS) | Push branch β manually trigger CI with Linux filter |
For detailed CI usage, debugging tips, and workflow examples, see .github/workflows/README.md