fastipc
Shared-memory IPC between processes, in any language.
A small Zig library for sending messages between processes on the same machine. Each connection is a pair of lock-free rings that both processes map, plus a local socket that tells each side when the other one has gone. You can use it from C, C++, Zig, Python, C#, Java, Rust, Lua, JavaScript or Go.
pip install fipc-pythondotnet add package Fipcio.github.fastipc:fipc:1.0.0cargo add fipcluarocks install fipcnpm install fipc-jsgo get github.com/fastipc/fastipc/bindings/go/fipc@latest| Intel Core i9 laptop | MacBook Air M1 | |||
|---|---|---|---|---|
| Case | Linux x86‑64 | Windows | macOS | Linux ARM64 |
| Round trip, 16 BLatency (ns, median) | 1.18×Aeron | 1.19×Aeron | 1.00×Aeron | 1.00×Aeron |
| Stream, 16 BThroughput (messages/s) | 0.89×*Aeron | 1.11×Aeron | 1.42×Aeron | 1.43×Aeron |
| Stream, 1 KiBThroughput (messages/s) | 1.54×Aeron | 1.32×Aeron | 1.14×Aeron | 1.11×Aeron |
| Broadcast to 4Throughput (messages/s) | 0.98×*Aeron | 1.18×Aeron | 1.21×Aeron | 1.71×Aeron |
| Work queue 4 × 4Throughput (messages/s) | 1.89×ZeroMQ | 2.04×ZeroMQ | 5.71×ZeroMQ | — |
| Service callsThroughput (calls/s) | 3.60×iceoryx2 | 3.30×iceoryx2 | 3.18×iceoryx2 | 3.10×iceoryx2 |
Intel Core i9-13900H laptop, 6 P-cores (Hyper‑Threading) and 8 E-cores, 32 GB, running Windows 11 and Linux in WSL2 (20 vCPUs).
Apple M1 MacBook Air, 4 P-cores and 4 E-cores, 8 GB, running macOS and Linux in a VM (4 vCPUs, 4 GB).
* In every benchmark, Aeron's receiver polls in a loop and never sleeps, which keeps a core 100% busy; on Windows its media driver spins too (Aeron in detail).
Small on purpose
from the architectureThe core is small, and it covers what two local processes usually need from each other: messages of any size, zero-copy, broadcasts, work queues, services with replies, and a prompt report when the other process exits or crashes.
The whole library, small enough to read
Everything behind fipc.h, for all three operating systems, is 8,324 lines of Zig. The count includes only code that ends up in the library. Blank lines, comments, tests, test hooks and compile-time checks are left out.
Either order, any restart
Start the server or the client first. fipc_stream_connect waits, within its timeout, for a server to listen and accept. Restart either side at any time. The other side receives every message the old process completed, then gets FIPC_DISCONNECTED, and its next fipc_stream_accept or fipc_stream_connect pairs it with the new one.
Results you can act on
Every call returns one of eleven result codes. Each code tells you the state of the connection and what to do next. If a send times out because the ring is full, or a receive times out because no message arrived, nothing was sent or taken, and you can call it again. A message too large for your receive buffer stays queued, so you can read it again with a bigger one. A disconnect means the peer is gone, and a server usually handles it by waiting to accept a new connection. All eleven codes.
Cheap on the hot path
Sending a message is one copy into shared memory, then a store and an atomic swap to publish it. No locks are taken. The operating system is involved only when the receiver has stopped spinning and gone to sleep, and then the wake-up is a single system call. A background thread watches for the other process to exit, so that check costs the hot path nothing. A 16-byte round trip between two processes takes 125 to 163 ns on the machines we measured, and no library we compared was faster.
Any language on the other end
one protocol · 100 pairs, testedA process in any of the ten languages can talk to a process in any other, because every binding wraps the same native library and speaks the same protocol. The two sides only have to agree on the connection name and the message format.
All the servers and clients in the quick start do the same thing. The client sends the message ping and the server answers PING. python devtool.py test interop runs every server against every client, each in its own process, on Linux, Windows and macOS.
# terminal 1, in bindings/rust
cargo run --example server
# terminal 2, next to client.py
python client.py
# b'PING'The two programs are the quick start's server.rs and client.py, unchanged.
| server ↓ client → | Zig | C | C++ | Python | C# | Java | Rust | Lua | JavaScript | Go |
|---|---|---|---|---|---|---|---|---|---|---|
| Zig | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| C | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| C++ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Python | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Java | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Lua | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| JavaScript | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Go | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
What it gives you
from include/fipc.hRings in shared memory
Messages go through lock-free ring buffers that both processes map. A connection has two rings, one per direction, each with one writer and one reader (SPSC). The write and read positions sit on separate cache lines, so the writer and the reader don't false-share.
Spin, then sleep
A waiting side spins briefly, then sleeps on a futex (Linux), an os_sync address wait (macOS) or a named event (Windows). A publish costs one store and one locked swap. The sleep protocol is built so that a wake-up is never lost.
Zero-copy, or any size
You can write a message directly into the ring and read it in place, 16-byte aligned. The copying calls take messages of any size. A message larger than the ring is sent in pieces and arrives whole. Nothing is allocated per message.
Knows when the peer is gone
A local control connection per connection (an abstract AF_UNIX socket, a named pipe, a socket file on macOS) reports the peer's end as soon as its process exits or closes the connection. There is no heartbeat or timeout to tune, and a crash leaves nothing to clean up.
Local only, on purpose
Zero-copy messages and a peer's end reported without a heartbeat both depend on shared memory and on one kernel that sees both processes. Neither holds across a network, so FastIPC has no network transport. A component that moves to another machine can switch to a network library such as ZeroMQ, whose PAIR, PUB/SUB, PUSH/PULL and REQ/REP sockets correspond to the stream, the broadcast, the queue and the service.
Broadcasts (pub/sub)
One publisher and up to 1,024 subscribers, in any processes. Each message is stored once, in memory that every subscriber maps. A lossy subscriber never holds the publisher back. A gated one misses nothing and can read messages in place.
Work queues
Up to 63 producers and 64 consumers, in any processes. Each message goes to one consumer. If that consumer's process ends before acknowledging it, the message goes to another.
Services (request and reply)
Up to 64 clients and one server or a pool of up to 64, in any processes. Each request goes to one server, and the replies come back to each client in the order it sent its requests. A request reserves room for its reply, so a server never waits for a client. If a server ends before replying, the request is answered with FIPC_ABANDONED.
A specified protocol
Everything on the wire is written down, so two builds interoperate bit for bit. A bug in one process can't crash the other: each side bounds-checks what it reads from shared memory, which costs a few integer comparisons per message, and a corrupt ring returns FIPC_INVALID.
One header
One header of 30 functions in front of a library you can read end to end (how small). The tests are about twice its size.
Tested under failure
Unit tests and a C-API conformance suite, run in one process and across processes, including peers killed at every step of the handshake. Both also run under ThreadSanitizer. Chaos, soak and fuzz harnesses kill and pause peers, churn connections and feed in corrupt data.
How it works
from docs/protocol.mdA server listens on a name and accepts one client at a time; a client connects to the name. Only processes of the same user can connect.
memfd with the SEGMENT frame and wakes sleepers with a futex on the ring's sleep flag. Windows uses a pagefile-backed section and four auto-reset named events per connection. macOS passes an unnamed POSIX shared memory object with SEGMENT and wakes sleepers with os_sync_wait_on_address. Linux and Windows leave nothing behind after a crash; on macOS a crashed owner's socket file and lock file stay until that name's next create removes them, and never get in the way.Set up
The server's
fipc_stream_acceptaccepts the client, creates a zeroed segment and offers it in a SEGMENT frame. The client'sfipc_stream_connectchecks the frame and the segment's header, maps it and answers READY. Both run on the calling threads, so a client and its server run on different threads or processes.fipc_stream_connectcan return before the server'sfipc_stream_acceptdoes; messages sent meanwhile wait in the ring.Talk
Messages go through the rings and never touch the control connection. A frame is a 16-byte header and a piece, and never wraps, so every piece is contiguous and the zero-copy calls can hand out pointers into the ring.
End
When a process closes the connection or exits for any reason, the kernel closes its end of the control connection. The other side reads EOF. Its sends then return
FIPC_DISCONNECTEDat once, and its receives do so after delivering every message the peer completed.
Performance
from bench/alternativesFastIPC, iceoryx2, Aeron, ZeroMQ, NNG, Boost.Interprocess, cpp-ipc and the operating system's sockets, run through the same cases on the same machines, each library in its fastest configuration. One sender and one receiver process for a stream and a round trip, four subscribers for a broadcast, one client and one server for a service.
Stream: 16-byte messages per second, one way
Round trip, 16 B, p50 (shorter is better; log scale)
Broadcast to 4 subscribers, lossless: 16-byte messages per second
Service, 1 client × 1 server: 16-byte calls per second
Intel Core i9-13900H laptop, 6 P-cores (Hyper‑Threading) and 8 E-cores, 32 GB, running Windows 11 and Linux in WSL2 (20 vCPUs).
Apple M1 MacBook Air, 4 P-cores and 4 E-cores, 8 GB, running macOS and Linux in a VM (4 vCPUs, 4 GB). Machines and method
Medians of 3 runs, no CPU pinning; hover a bar for the configuration. Every case, work queues, the libraries' features, waking a sleeping receiver, and what each language's binding costs.
Quick start
one exchange, every languageEach language has a server file and a client file. Run the server in one process and the client in another. The client sends ping and the server answers in upper case. Every pair makes the same exchange, so a client in any language works with a server in any other (every pair is tested).
zig fetch --save git+https://github.com/fastipc/fastipcconst std = @import("std");
const fastipc = @import("fastipc");
pub fn main(init: std.process.Init) !void {
// rings of 1 MiB each way
const listener = try fastipc.streamCreate(
init.gpa,
"my_channel",
1 << 20,
);
defer listener.close();
// waits for a client
const conn = try listener.accept(.none);
defer conn.close();
var buf: [256]u8 = undefined;
// .none: no timeout
const len = try conn.recv(&buf, .none);
const reply = std.ascii.upperString(&buf, buf[0..len]);
try conn.send(reply, .none);
}const std = @import("std");
const fastipc = @import("fastipc");
pub fn main(init: std.process.Init) !void {
const five_s: std.Io.Timeout = .{ .duration = .{
.raw = .fromSeconds(5),
.clock = .awake,
} };
const conn = try fastipc.streamConnect(
init.gpa,
"my_channel",
five_s,
);
defer conn.close();
try conn.send("ping", .none);
var buf: [256]u8 = undefined;
const len = try conn.recv(&buf, .none);
std.debug.print("{s}\n", .{buf[0..len]}); // PING
}Zig 0.17.0. Your build.zig imports the dependency's module: exe.root_module.addImport("fastipc", dep.module("fastipc")) (an example project). Errors are Zig error sets and timeouts std.Io.Timeout (.none waits as long as it takes). More on the Zig module.
The Zig build system builds the library and compiles your C, with no other build tool. Link the dependency's artifact fastipc (an example project). Any other compiler works with the release archives. Build and link.
#include <ctype.h>
#include <fipc.h>
int main(void)
{
fipc_listener_t* listener;
if (fipc_stream_create("my_channel", 1 << 20, &listener) != FIPC_OK) /* rings of 1 MiB each way */
return 1;
fipc_conn_t* conn;
if (fipc_stream_accept(listener, &conn, FIPC_FOREVER) == FIPC_OK) /* waits for a client */
{
char buf[256];
size_t len;
if (fipc_recv(conn, buf, sizeof buf, &len, FIPC_FOREVER) == FIPC_OK)
{
for (size_t i = 0; i < len; i++)
buf[i] = (char) toupper((unsigned char) buf[i]);
fipc_send(conn, buf, len, FIPC_FOREVER);
}
fipc_close(conn);
}
fipc_listener_close(listener);
return 0;
}#include <fipc.h>
#include <stdio.h>
int main(void)
{
fipc_conn_t* conn;
fipc_result_t result = fipc_stream_connect("my_channel", &conn, 5000);
if (result != FIPC_OK)
{
fprintf(stderr, "connect: %s\n", fipc_result_str(result));
return 1;
}
char buf[256];
size_t len;
if (fipc_send(conn, "ping", 4, FIPC_FOREVER) == FIPC_OK
&& fipc_recv(conn, buf, sizeof buf, &len, FIPC_FOREVER) == FIPC_OK)
printf("%.*s\n", (int) len, buf); /* PING */
fipc_close(conn);
return 0;
}C++20 through the header-only fipc.hpp, built like C. Calls return fipc::result<T>, so no exceptions are needed. More on the C++ wrapper.
#include <cctype>
#include <string>
#include <fipc.hpp>
int main()
{
auto listener = fipc::stream_create("my_channel", 1 << 20); // rings of 1 MiB each way
if (!listener)
return 1;
auto conn = listener->accept(); // waits for a client
if (!conn)
return 1;
std::string message;
if (!conn->receive(message))
return 1;
for (char& c : message)
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
return conn->send(message) ? 0 : 1;
}#include <chrono>
#include <cstdio>
#include <string>
#include <fipc.hpp>
int main()
{
auto conn = fipc::stream_connect("my_channel", std::chrono::seconds(5));
if (!conn)
{
std::fprintf(stderr, "connect: %s\n", conn.error().name());
return 1;
}
std::string reply;
if (!conn->send("ping") || !conn->receive(reply))
return 1;
std::printf("%s\n", reply.c_str()); // PING
return 0;
}pip install fipc-pythonimport 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())import fipc
with fipc.stream_connect("my_channel", timeout_ms=5000) as conn:
conn.send(b"ping")
print(conn.recv()) # b'PING'Calls block by default (timeout_ms=FOREVER) and raise FipcError for any result but OK. More on the Python binding.
dotnet add package Fipcusing System.Text;
using FastIpc;
// rings of 1 MiB each way; Accept waits for a client
using var listener = FipcStream.Create("my_channel", 1 << 20);
using var conn = listener.Accept();
if (conn.Receive(out byte[] message) == FipcResult.Ok)
{
string upper = Encoding.UTF8.GetString(message)
.ToUpperInvariant();
conn.Send(Encoding.UTF8.GetBytes(upper));
}using System.Text;
using FastIpc;
// waits up to 5 s for the server, else throws
using var conn = FipcStream.Connect("my_channel", 5000);
conn.Send("ping"u8);
conn.Receive(out byte[] reply);
Console.WriteLine(Encoding.UTF8.GetString(reply)); // PINGEach is a console project (dotnet new console) with the Fipc package. Setup throws a FipcException on failure; after it, every call returns a FipcResult, and a timeout or the peer's end is a result, not an exception. More on the C# binding.
implementation("io.github.fastipc:fipc:1.0.0")import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import io.github.fastipc.Listener;
public class Server {
public static void main(String[] args) {
// rings of 1 MiB each way; accept() waits for a client
try (Listener listener =
Fipc.createStream("my_channel", 1 << 20);
Connection conn = listener.accept()) {
String text = new String(conn.receive());
conn.send(text.toUpperCase().getBytes());
}
}
}import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import java.time.Duration;
public class Client {
public static void main(String[] args) {
try (Connection conn = Fipc.connectStream("my_channel",
Duration.ofSeconds(5))) {
conn.send("ping".getBytes());
byte[] reply = conn.receive();
System.out.println(new String(reply)); // PING
}
}
}Java 22+, through the FFM API. Run each with the jar on the class path: java --enable-native-access=ALL-UNNAMED -cp fipc-1.0.0.jar Server.java. Calls block by default and throw FipcException for any result but OK. More on the Java binding.
cargo add fipcfn main() -> fipc::Result<()> {
let mut listener = fipc::stream::create("my_channel", 1 << 20)?; // rings of 1 MiB each way
let mut conn = listener.accept(None)?; // waits for a client
let message = conn.receive(None)?;
conn.send(&message.to_ascii_uppercase(), None)?;
Ok(())
}use std::time::Duration;
fn main() -> fipc::Result<()> {
let mut conn = fipc::stream::connect("my_channel", Duration::from_secs(5))?;
conn.send(b"ping", None)?;
println!("{}", String::from_utf8_lossy(&conn.receive(None)?)); // PING
Ok(())
}Rust 1.85+. Calls return Result<T, Error>; a timeout (None waits as long as it takes) and the peer's end are errors to match on. More on the Rust binding.
luarocks install fipclocal fipc = require("fipc")
local listener = assert(fipc.stream_create("my_channel", 1024 * 1024)) -- rings of 1 MiB each way
local conn = assert(listener:accept()) -- waits for a client
local message = assert(conn:recv())
assert(conn:send(message:upper()))
conn:close()
listener:close()local fipc = require("fipc")
local conn = assert(fipc.stream_connect("my_channel", 5000)) -- waits up to 5 s for the server
assert(conn:send("ping"))
print(assert(conn:recv())) -- PING
conn:close()LuaJIT 2.1, through its FFI, with nothing to compile. A call that fails returns nil and the error's name ("timeout", "disconnected", ...), so assert() raises it; a timeout in milliseconds comes last, and none waits as long as it takes. More on the Lua binding.
npm install fipc-jsimport { createStream } from "fipc-js";
const listener = createStream("my_channel", 1 << 20); // rings of 1 MiB each way
const conn = await listener.accept(); // waits for a client; the event loop runs meanwhile
const message = await conn.receive();
await conn.send(message.toString().toUpperCase());
conn.close();
listener.close();import { connectStream } from "fipc-js";
const conn = await connectStream("my_channel", 5000); // waits up to 5 s for the server
await conn.send("ping");
console.log((await conn.receive()).toString()); // PING
conn.close();Node.js 22+, Bun and Deno 2, through one Node-API addon, with nothing to compile. Calls that may wait return a Promise and leave the event loop running (a Sync form waits on the calling thread); a call that fails rejects with a FipcError whose code names the result ("FIPC_TIMEOUT", ...). TypeScript types included. More on the JavaScript binding.
go get github.com/fastipc/fastipc/bindings/go/fipc@latestpackage main
import (
"bytes"
"log"
"github.com/fastipc/fastipc/bindings/go/fipc"
)
func main() {
listener, err := fipc.CreateStream("my_channel", 1<<20) // rings of 1 MiB each way
if err != nil {
log.Fatal(err)
}
defer listener.Close()
conn, err := listener.Accept(fipc.Forever) // waits for a client
if err != nil {
log.Fatal(err)
}
defer conn.Close()
message, err := conn.Receive(fipc.Forever)
if err != nil {
log.Fatal(err)
}
if err := conn.Send(bytes.ToUpper(message), fipc.Forever); err != nil {
log.Fatal(err)
}
}package main
import (
"fmt"
"log"
"time"
"github.com/fastipc/fastipc/bindings/go/fipc"
)
func main() {
conn, err := fipc.ConnectStream("my_channel", 5*time.Second) // waits up to 5 s for the server
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.Send([]byte("ping"), fipc.Forever); err != nil {
log.Fatal(err)
}
reply, err := conn.Receive(fipc.Forever)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(reply)) // PING
}Go 1.27+, without cgo. The package loads the library through purego, its one dependency, so CGO_ENABLED=0 builds and cross-compiling work. Every method is safe from any goroutine; errors are tested with errors.Is (fipc.ErrTimeout, fipc.ErrDisconnected, ...), and timeouts are time.Durations (fipc.Forever waits as long as it takes). More on the Go binding.
One library, ten languages
30fipc_* functions, one ABIZig
module fastipcThe library's own language. Listener, Queue, Service and Conn over your std.Io, with Zig error sets and std.Io.Timeout, compiled into your program. Streams, broadcasts, work queues and services, copied and zero-copy.
C
fipc.hThe whole API in one header: 30 functions, eleven result codes. Built with the Zig build system or any C compiler.
C API, zero-copy →C++
fipc.hppA header-only C++20 wrapper with RAII listeners and connections, fipc::result<T> without exceptions, std::chrono timeouts, zero-copy slots.
Python
fipcListener and Conn over CFFI's ABI mode, so no compiler is needed. Results other than OK raise FipcError.
C#
FipcFipcListener and FipcConnection are built on SafeHandles and can be disposed from any thread. Underneath is a raw P/Invoke layer, Fipc.
Java
fipcListener and Connection over the FFM API, Java 22+, with no JNI. Safe to close from any thread; failures throw FipcException.
Rust
fipcListener and Connection, safe and Send + Sync, over a raw FFI crate. The borrow checker keeps the threading and zero-copy rules; errors are a Result.
Lua
fipcOne module over LuaJIT's FFI, with no compiler and no C module. Listeners and connections with copied and zero-copy messages; a failed call returns nil and the error's name.
JavaScript
fipcOne Node-API addon for Node.js, Bun and Deno. Calls on Listener and Connection that may wait return Promises that never block the event loop, and each has a Sync form. Zero-copy views detach when they end. TypeScript types are included.
Go
fipcOne package that loads the library through purego, so no cgo is needed. Listener and Conn are safe from any goroutine, like a net.Conn. Errors work with errors.Is, timeouts are time.Durations, and a context can cancel a call.
Platform support
from docs/platform-support.mdFastIPC supports recent systems only. That lets the code use modern APIs and instructions directly, with no fallbacks or feature probes.
| Supported | What it means | |
|---|---|---|
| CPU | x86-64 with x86-64-v3 (AVX2, BMI1/2, FMA, MOVBE) and PREFETCHW on Linux and Windows; ARM64 (ARMv8.0-A) on Linux; Apple Silicon on macOS | Intel Haswell (2013) and later, AMD Zen (2017) and later; Raspberry Pi 3/4/5, AWS Graviton, Ampere, NVIDIA Jetson; Apple M1 and later |
| Linux | glibc 2.34 or newer, x86-64 and ARM64 | Ubuntu 22.04, Debian 12, RHEL 9 and later (kernels 5.14+) |
| Windows | Windows 11, Windows Server 2022 and later, x64 | Windows 10 left support in October 2025 |
| macOS | macOS 14.4 or newer, on Apple Silicon | the first with os_sync_wait_on_address; the dylib is ad hoc signed, not notarized |
| Build | Zig 0.17.0 | Only to build from source; the Python, NuGet, Maven, crates.io, LuaRocks and npm packages and the Go module carry the native library |
| Not supported | 32-bit systems, Windows on ARM, Intel Macs, musl-based Linux, x86-64 CPUs without AVX2 | |