fipc
Messages between processes on one machine, through shared memory: the Python binding of FastIPC, a small library written in Zig. Four kinds, each created on a name:
- Streams: a server creates a stream (
fipc.stream_create) and itsListeneraccepts one client at a time; a client connects to the name (fipc.stream_connect). Each connection has a shared-memory segment with one ring per direction, and reports the peer's end as soon as its process exits or closes the connection. - Broadcasts: one publisher (
fipc.broadcast_create) sends every message to each of its subscribers (fipc.broadcast_join). - Work queues: producers and consumers join a queue (
fipc.queue_create,fipc.queue_join), and each message goes to one consumer. - Services: clients and servers join a service (
fipc.service_create,fipc.service_join): each request goes to one server, and its reply back to its client.
Installation
pip install fipc-python
The wheels bundle the native library: Windows x64 (Windows 11 / Server 2022 or newer) and Linux x64 (glibc 2.34 or
newer: Ubuntu 22.04, Debian 12, RHEL 9 and later), which need an x86-64-v3 CPU (AVX2: Intel Haswell, AMD Zen or
later), Linux on ARM64 (manylinux_2_34_aarch64, glibc 2.34 or newer) and macOS 14.4 or newer on Apple Silicon. The
macOS wheel is tagged macosx_14_0_arm64 (pip matches macOS 11
and later by the major version only), so pip also installs it on macOS 14.0 to 14.3, where the library doesn't load.
The binding loads it through CFFI's ABI mode, so one wheel per platform fits every Python 3.9+. A library of your own
comes first: set the environment variable FASTIPC_LIB_DIR to its folder (when it is set, the binding looks nowhere
else).
Use
# server.py
import fipc
# rings of 1 MiB each way
with fipc.stream_create("my_channel", 1 << 20) as listener:
with listener.accept() as conn: # waits for a client
message = conn.recv()
conn.send(message.upper())
# client.py
import fipc
with fipc.stream_connect("my_channel", timeout_ms=5000) as conn:
conn.send(b"ping")
print(conn.recv()) # b'PING'
Run each in its own terminal: python server.py, then python client.py. The client sends the message ping, and the
server answers with its uppercase, PING. Every example of the
repository's README makes this exchange, so its Zig, C, C++, C#, Java,
Rust, Lua, JavaScript and Go clients work against this server too, and this client against their servers.
A name holds a stream, a broadcast, a work queue or a service, and each kind has its functions: stream_create,
stream_connect; broadcast_create, broadcast_join; queue_create, queue_join; service_create,
service_join. They are the only way to make a Listener, a Conn, a Queue or a Service.
- Streams:
fipc.stream_create(name, capacity)claims the name and returns itsListener, whoseaccept()sets one client at a time up on the calling thread (NO_WAITpolls);fipc.stream_connect(name)connects to it.stream_connectwaits for the server'saccept, so a client and its server run in different processes or on different threads. - Copied messages:
sendandrecv(a message of any size: it travels in pieces and arrives whole), orrecv_intoa buffer of your own. - Zero-copy (messages of one piece, up to
max_piece()bytes):send_acquire(length), write into the memoryview,send_commit(length);recv_acquire(), read,recv_release(). - Batches:
send_many(messages)sends a list of messages, published together with at most one wake of the receivers, and returns how many went (an error'ssentsays how many went before it);recv_many(max_count=16)waits for the next message, then takes every further one already there, and returns a list ofbytes. On every kind: a broadcast's subscriber takes those its publisher has moved past, and a service's client sends a batch of requests (owed as its sends are:callwaits until their replies are received) and receives their replies in order; a server's batches raiseINVALID. Batch both ends: a receiver that takes one message per call falls behind a batching sender.
Timeouts are int milliseconds, the last parameter: NO_WAIT (0) doesn't wait, FOREVER (-1, the default) waits for
ever. A call that fails raises FipcError, whose result is a Result (Result.TIMEOUT, Result.DISCONNECTED,
...). On a connection, one thread at a time sends and one receives; cancel may be called from any thread, close
only when no other thread is inside a call on the connection. A disconnected connection is final: close it, and
accept or connect a new one. A Conn left unclosed is closed when it is garbage-collected (a thread inside a call on
it, or a memoryview of send_acquire or recv_acquire, keeps it referenced), and so is a Listener, which frees its
name; a Queue and a Service never are.
Broadcasts
A broadcast sends every message of one publisher to each of its subscribers, in any processes of the user: each message is stored once, in memory every subscriber maps, and is of one piece (up to a quarter of the capacity).
# publisher.py
import fipc
# 64 KiB of messages, up to 8 subscribers; one that holds the publisher back 5 s is dropped
with fipc.broadcast_create("my_broadcast", 1 << 16, 8, 5000) as publisher:
publisher.wait_for_members(1) # waits for the first subscriber
for i in (1, 2, 3):
publisher.send(f"tick {i}".encode())
# closing the publisher ends the broadcast: subscribers get DISCONNECTED after the ticks
# subscriber.py
import fipc
# gated: misses nothing (the publisher waits for it)
with fipc.broadcast_join("my_broadcast", fipc.SUBSCRIBER_GATED, timeout_ms=5000) as subscriber:
try:
while True:
print(subscriber.recv().decode()) # tick 1, tick 2, tick 3
except fipc.FipcError as e:
if e.result != fipc.Result.DISCONNECTED: # the publisher closed
raise
Run python publisher.py, then python subscriber.py (or in the other order: broadcast_join waits up to its
timeout for the publisher), which prints tick 1 to tick 3. fipc.broadcast_create(name, capacity, max_subscribers, max_stall_ms) returns the publisher, a Conn and the broadcast's one handle, which the with block
closes.
- Joining:
fipc.broadcast_join(name, role); the publisher's process admits each subscriber by itself, on a thread of the library's.publisher.wait_for_members(at_least)waits until that many have joined (at a timeout or a cancel, the error'scountholds the count), so that nothing is sent before they are there. - Gated (
SUBSCRIBER_GATED): misses nothing, as the publisher waits for it, and may receive in place (recv_acquire,recv_release). One that holds the publisher back formax_stall_msis unsubscribed: its receives raiseUNSUBSCRIBED. - Lossy (
SUBSCRIBER_LOSSY): never holds the publisher back. One that falls a lap behind getsOVERRUNinstead of a message, and the error'slostsays how many messages it lost; the next receive goes on from a recent message. - The end: closing the publisher ends the broadcast and frees its name; each subscriber receives what was sent,
then
DISCONNECTED. The publisher's garbage collection closes it too: keep it referenced while the broadcast is in use.
Work queues
A work queue takes messages from up to 63 producers and hands each to one of up to 64 consumers, in any processes of the user. Each producer has a ring of its own; a consumer takes a message of any producer, and the queue promises no order. Messages are of one piece (up to a quarter of the capacity) and wait in the queue until a consumer takes them. Each message goes to one consumer, and again only if that consumer's process ends before acknowledging it (Delivery, below).
# producer.py
import fipc
# 4 KiB of messages per producer, 1 producer and 1 consumer: this process owns the queue and is its producer
with fipc.queue_create("my_queue", 4096, 1, 1) as queue:
with fipc.queue_join("my_queue", fipc.PRODUCER) as producer:
for i in (1, 2, 3):
producer.send(f"job {i}".encode()) # waits in the queue until a consumer takes it
queue.wait_for_members(fipc.CONSUMER, 1) # waits for the consumer to join
# closing the queue ends it: the consumer gets DISCONNECTED after the jobs
# consumer.py
import fipc
with fipc.queue_join("my_queue", fipc.CONSUMER, timeout_ms=5000) as consumer:
try:
while True:
print(consumer.recv().decode()) # job 1, job 2, job 3
except fipc.FipcError as e:
if e.result != fipc.Result.DISCONNECTED: # the queue ended
raise
Run python producer.py, then python consumer.py (or in the other order: queue_join waits up to its timeout for
the queue), which prints job 1 to job 3. fipc.queue_create(name, capacity, max_producers, max_consumers)
returns the Queue, which the with block closes.
- Joining:
fipc.queue_join(name, PRODUCER)orfipc.queue_join(name, CONSUMER), the owner's members too, from any thread: the owner's process admits each joiner by itself, on a thread of the library's. The example's producer sends first (the jobs wait in the queue), then waits for its consumer, so it works in either start order. - The owner's queue:
queue.wait_for_members(role, at_least)waits until that many members of the role (PRODUCER,CONSUMER, orNonefor both) have joined (at a timeout or a cancel, the error'scountholds the count);queue.cancel()makes its waits raiseCANCELLED, from any thread. AQueuenever closes itself (it has no finalizer): a queue lives untilclose()or the end of its process, and dropping the object doesn't end it. - Receiving:
recv(),recv_into(buf)(TOO_LARGEtakes nothing: another consumer may take that message) or in place,recv_acquire()untilrecv_release(). - The end: after the queue's close, a producer's sends raise
DISCONNECTED, and each consumer receives what is left in the queue, thenDISCONNECTED. A producer's or a consumer's close only leaves the queue.
Delivery. Each message goes to one consumer. It goes again, to another consumer, only if the process of the consumer that took it ends before acknowledging it, and when a consumer acknowledges is its own choice, not the queue's. A message received in place is acknowledged at its release (or the consumer's next receive, or its close).
| How the consumer receives | When it acknowledges | If its process ends | What the application gets |
|---|---|---|---|
recv() or recv_into() (copying) |
inside the call, before it returns | inside the call: the message goes to another consumer, and this one never saw it; after the call returned: it doesn't go again | at most once: no duplicates |
recv_acquire(), then recv_release() at once, then the work |
at the release, before the work | as above | at most once: no duplicates |
recv_acquire(), the work, then recv_release() |
at the release, after the work | before the release: the message goes to another consumer | at least once: a duplicate is possible, so the work must be idempotent |
- Choose by the work, per consumer. Work that may safely run twice (rendering, a computation, re-indexing, converting a file) acknowledges after it; work whose effects must never repeat (a payment, an email, a motor command) acknowledges before it.
- Not covered: a consumer that hangs while its process runs (there are no timeouts, by design); the owner's process, with which the queue ends; a reboot. A consumer holds at most one unacknowledged message at a time, the one received in place.
- A message that goes again comes in no set order, after messages its producer sent later: the queue promises no order anyway.
- A message held in place holds back its producer's room until its release: a consumer that acknowledges after a long job keeps that message's room, and so the room of the producer's messages after it, until then.
- A message that crashes every consumer that takes it, acknowledged after the work, goes to each consumer in turn, with no limit: acknowledge before the work where that can happen.
Services
A service takes requests from up to 64 clients and hands each to one of up to 64 servers, in any processes of the
user; the server that took a request replies to it, and the reply goes back to the client that sent it. A server waits
for the requests of every client at once (one recv), and the servers of a pool share the load: each takes the next
request of any client. Each client has a ring of its own, which holds its requests and the room for their replies, so a
server never waits for a client. Requests and replies are of one piece, 1 to max_message bytes, the owner's choice.
# service_server.py
import fipc
# 4 KiB per client, requests and replies of up to 256 bytes, 1 client and 1 server: this process owns the service
# and is its server
with fipc.service_create("my_service", 4096, 256, 1, 1):
with fipc.service_join("my_service", fipc.SERVER, timeout_ms=5000) as server:
for _ in range(3): # three requests, then the end
request = server.recv() # the next request of any client
server.send(request.upper()) # the reply goes to its client, and never waits
# closing the service ends it: the client still receives the replies sent
# service_client.py
import fipc
with fipc.service_join("my_service", fipc.CLIENT, timeout_ms=5000) as client:
for i in (1, 2, 3):
# sends the request and returns its reply; FipcError (ABANDONED) if the server ended before it replied
reply = client.call(f"ping {i}".encode())
print(reply.decode()) # PING 1, PING 2, PING 3
Run python service_server.py, then python service_client.py (or in the other order: service_join waits up to its
timeout for the service), which prints PING 1 to PING 3. fipc.service_create(name, capacity, max_message, max_clients, max_servers) returns the Service, which the with block closes.
- Joining:
fipc.service_join(name, CLIENT)orfipc.service_join(name, SERVER), the owner's members too, from any thread: the owner's process admits each joiner by itself, on a thread of the library's. Clients wait while no server is joined, and servers while no client is. - The owner's service:
service.wait_for_members(role, at_least)waits until that many members of the role (CLIENT,SERVER, orNonefor both) have joined (at a timeout or a cancel, the error'scountholds the count);service.cancel()makes its waits raiseCANCELLED, from any thread. AServicenever closes itself (it has no finalizer): a service lives untilclose()or the end of its process, and dropping the object doesn't end it. - A client:
send(request)sends a request, andrecv()(recv_into(buf), or in placerecv_acquire()untilrecv_release()) receives the reply to its oldest request whose reply it hasn't received yet: the n-th reply answers the n-th request, whichever server replies first. A client may send several requests before it receives (as many as its ring has room for: each holds its length rounded up to 64 bytes, plusmax_messagerounded up to 64, until its reply has been received), and the servers work on them at once. A receive that times out or is cancelled leaves its reply to come: the next receive returns it. call(request)sends a request and returns its reply. A call checks its request first:INVALIDfor an empty one,TOO_LARGEovermax_piece(), at once, even while a reply is owed. Then a call on a cancelled connection raisesCANCELLEDat once, whatever its timeout, and sends nothing: it could never receive the reply. The connection keeps count of the replies still owed: a call whose receive timed out or was cancelled leaves its reply owed, and the next call first receives and drops that reply (at most one is owed, since every call drops it first; anABANDONEDin its place counts too), within its own timeout and before it sends, so a call never returns the reply to another request. A call that had to drop an owed reply first sends its request only if its timeout has time left: once a positive timeout has run out, it raisesTIMEOUTwithout sending, so no server sees that request; withNO_WAIT(0) no step waits, and it sends if there is room. After a call that gave up, a plainrecvreturns that call's late reply (orABANDONED) first: replies come in request order. A call raisesINVALIDwhile a request sent withsendstill has its reply to be received: that reply is yours, so receive it first. A call sends and receives, so no other thread may send or receive on the connection meanwhile.- A server:
recv()takes the next request of any client (each client's in the order it sent them), andsend(reply)replies to it and never waits. A server holds one request at a time: its receives raiseINVALIDuntil it replies, and a reply with no request held raisesINVALID. A request received in place (recv_acquire()) stays valid until the reply'ssendorsend_commit;recv_release()does nothing. A reply to a client that has left is dropped. A server may receive on one thread and reply on another. Itsclose()answers the request it holdsABANDONED, and so does the garbage collection of a server'sConnleft unclosed: a server holds its request, and its client waits for the reply, until it is closed (withwithorclose()) or garbage-collected (or its process ends). ABANDONED: a client's receive (or call) raisesFipcErrorwithResult.ABANDONEDwhen the server that took the request ended before it replied: it closed its connection, or its process ended. The request counts as answered, and the next receive returns the next reply. An abandoned request may or may not have been carried out; the service never sends it again, and the client decides whether to.- The end: after the service's close, clients' sends and servers' receives raise
DISCONNECTED; a client receives the replies that came before the end, thenDISCONNECTED, and a server's reply raisesDISCONNECTED. A client's or a server's close only leaves the service: a client's requests that no server took are dropped. - Not covered: a server that hangs while its process runs (there are no timeouts, by design: its client waits, within its own timeouts); the owner's process, with which the service ends.
Documentation
- The website: this binding's guide, with examples of every call.
include/fipc.h: the C API under this binding, and every call's contract (results, timeouts, threads, messages in pieces, the peer's end).- Platform support, the protocol and the benchmarks.
- Changelog, issues.
License
MIT. Copyright (c) 2025-2026 Hayden Donnelly. See LICENSE.
Metadata
Release files for fipc-python 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| fipc_python-1.0.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| fipc_python-1.0.0-py3-none-manylinux_2_34_x86_64.whl | Python 3 | none | Linux glibc 2.34+ x86-64 | Details |
| fipc_python-1.0.0-py3-none-manylinux_2_34_aarch64.whl | Python 3 | none | Linux glibc 2.34+ ARM64 | Details |
| fipc_python-1.0.0-py3-none-macosx_14_0_arm64.whl | Python 3 | none | macOS 14.0+ ARM64 | Details |
Total release size: 825.3 kB
Release files / fipc_python-1.0.0-py3-none-win_amd64.whl
| Download URL | fipc_python-1.0.0-py3-none-win_amd64.whl |
|---|---|
| Size | 257.2 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
4aecec8f38d224b5fd86f7916a89b2bec4a9aeb69ad9fc4c211d7192b33cfe32
|
|
BLAKE2b-256 checksum How to use checksums |
9dacc7014d9f3b0694b6e3eb896fe1ecfd52786560a7be6c3338b1a4a93d5b5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / fipc_python-1.0.0-py3-none-manylinux_2_34_x86_64.whl
| Download URL | fipc_python-1.0.0-py3-none-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 196.2 kB |
| Tags | Linux glibc 2.34+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
494352d94c0889257e072544e12e4152ef6a4ca1914f000a1bf5bb7335955840
|
|
BLAKE2b-256 checksum How to use checksums |
5ffe14bd7edb82601759045b247e096c520d6d0695265796ac840e9cac1c0482
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / fipc_python-1.0.0-py3-none-manylinux_2_34_aarch64.whl
| Download URL | fipc_python-1.0.0-py3-none-manylinux_2_34_aarch64.whl |
|---|---|
| Size | 186.3 kB |
| Tags | Linux glibc 2.34+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
7ee2126e751db607f370c9d129b857e5fda2defde78ad45d3a71e2baba75dbd8
|
|
BLAKE2b-256 checksum How to use checksums |
6fd58a9f8a168b766dd8a1687884f21d0ff05cffab9bd44f718ad9b23c94fec2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / fipc_python-1.0.0-py3-none-macosx_14_0_arm64.whl
| Download URL | fipc_python-1.0.0-py3-none-macosx_14_0_arm64.whl |
|---|---|
| Size | 185.6 kB |
| Tags | Python 3 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
09fbd54c6329e9cb1bcadec781445f40dd6714aef31eff45e2780ff19e094a55
|
|
BLAKE2b-256 checksum How to use checksums |
0d61ae2b2eab028307df59f9139ae8010a432bd6d6173c0fb448f2391f9045bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logSecurity for fipc-python 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 4 files in this release have been uploaded with provenance metadata.
Attestation for fipc_python-1.0.0-py3-none-win_amd64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.