Java
Four classes, Listener, Connection, WorkQueue and Service, made by the static methods of Fipc (createStream, connectStream, createBroadcast, joinBroadcast, createQueue, joinQueue, createService, joinService), over the C API through the FFM API (java.lang.foreign), for Java 22 and later: no JNI, no dependencies. A call that doesn't succeed throws FipcException. All four are AutoCloseable and safe to close from any thread. The jar carries the native library for Windows x64, Linux x64 and ARM64 (glibc 2.34+) and macOS 14.4+ on Apple Silicon.
implementation("io.github.fastipc:fipc:1.0.0")Server and client
two processes, one nameStart the two in either order, in two terminals: Fipc.connectStream waits up to its timeout for a server to create the stream and accept. With the jar on the class path, the source launcher runs each file as it is: java --enable-native-access=ALL-UNNAMED -cp fipc-1.0.0.jar Server.java. The pair makes the exchange every example on this site makes, the message ping, answered in upper case: this client works with any language's server, and this server with any language's client.
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
}
}
}Messages
bytes of any size, no headerA connection carries messages: send takes a message of any size, and receive returns each one whole in a new array (or fills your buffer). This server echoes every message back until its client leaves, which ends its next receive with DISCONNECTED.
import io.github.fastipc.*;
public class EchoServer {
public static void main(String[] args) {
// rings of 1 MiB each way: a power of two, 1 KiB to 2 GiB
try (Listener listener = Fipc.createStream("demo", 1 << 20);
Connection conn = listener.accept()) {
while (true) {
conn.send(conn.receive()); // echo, any size
}
} catch (FipcException e) {
if (e.result() != Result.DISCONNECTED) throw e;
System.out.println("client gone");
}
}
}import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import java.time.Duration;
public class EchoClient {
public static void main(String[] args) {
try (Connection conn =
Fipc.connectStream("demo", Duration.ofSeconds(5))) {
for (String word : new String[] {"hello", "shared", "memory"}) {
conn.send(word.getBytes());
System.out.println(new String(conn.receive()));
}
}
}
}Errors and timeouts
FipcException.result() is a ResultEvery call that can wait has an overload without a timeout, which waits for ever, and one that takes a java.time.Duration: Fipc.NO_WAIT (Duration.ZERO) doesn't wait, Fipc.FOREVER waits for ever. A call that doesn't succeed throws FipcException, unchecked, whose result() says why: TIMEOUT, DISCONNECTED, CANCELLED, TOO_LARGE, INVALID, NO_MEMORY or ADDR_IN_USE, on a broadcast OVERRUN (a lossy subscriber lost messages: lostMessages() says how many) or UNSUBSCRIBED, and on a service's client ABANDONED (the server that took the request ended before it replied).
DISCONNECTED is final for that connection: close it, then accept or connect a new one. A call on a closed listener or connection throws IllegalStateException.
byte[] msg;
try {
msg = conn.receive(Duration.ofMillis(100));
} catch (FipcException e) {
if (e.result() != Result.TIMEOUT) throw e;
msg = null; // nothing yet
}Zero-copy
up to conn.maxPiece() bytesWrite into a MemorySegment of the ring and commit; read a read-only MemorySegment of the ring and release it. A segment is valid until its commit or release and confined to the thread that acquired it: touching it afterwards, or from another thread, throws instead of reaching memory that is no longer yours.
MemorySegment room = conn.sendAcquire(5); // in the ring
room.copyFrom(MemorySegment.ofArray("hello".getBytes()));
conn.sendCommit(5);MemorySegment message = conn.receiveAcquire(); // read-only, in the ring
handle(message);
conn.receiveRelease(); // frees its roomBatches
many messages per call, both endssendMany(messages, [timeout]) sends a List<byte[]> in order and publishes it together, with at most one wake of the receivers, and returns how many went; the timeout bounds the whole call (Fipc.NO_WAIT: only what fits now). Every message is checked before any goes. A FipcException is the first message that didn't go, and its sentMessages() says how many did.
receiveMany(maxCount, [timeout]) waits for the next message, then takes every further one already there and returns them as a List<byte[]>; receiveMany(segment, lengths, timeout) packs them into your MemorySegment, their lengths in a long[]. A broadcast's subscriber takes those its publisher has moved past; a service's client sends a batch of requests (owed as its sends are) and receives their replies in order; a server's batches throw INVALID. Batch both ends: a receiver that takes one message per call falls behind a batching sender.
int sent = conn.sendMany(List.of(
"one".getBytes(), "two".getBytes()));
for (byte[] msg : peer.receiveMany(64)) {
handle(msg);
}Broadcast
one publisher, many subscribersA 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. Messages are of one piece, up to a quarter of the capacity (maxPiece()). Fipc.createBroadcast returns the publisher's Connection, the broadcast's one handle. The publisher's process admits subscribers by itself, so no thread of yours waits for them: the publisher waits for the first one (publisher.waitForMembers(1); at a TIMEOUT the exception's memberCount() holds the count) before it sends tick 1 to tick 3; closing the publisher ends the broadcast and frees the name. The subscriber joins gated (Fipc.joinBroadcast with SubscriberRole.GATED), so it misses nothing (the publisher waits for it), may receive in place (receiveAcquire()), and receives until DISCONNECTED, which comes after the last message. A lossy subscriber (SubscriberRole.LOSSY) never holds the publisher back: one that falls a lap behind gets OVERRUN, and the exception's lostMessages(), a long, says how many messages it lost. A gated one that holds the publisher back for the broadcast's maxStall is unsubscribed (UNSUBSCRIBED).
import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import java.time.Duration;
public class Publisher {
public static void main(String[] args) {
// 64 KiB of messages, up to 8 subscribers; one that
// holds the publisher back 5 s is dropped
try (Connection publisher = Fipc.createBroadcast(
"my_broadcast", 1 << 16, 8, Duration.ofSeconds(5))) {
publisher.waitForMembers(1); // waits for the first one
for (int i = 1; i <= 3; i++) {
publisher.send(("tick " + i).getBytes());
}
}
// closing the publisher ends the broadcast: subscribers
// get DISCONNECTED after the ticks
}
}import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import io.github.fastipc.FipcException;
import io.github.fastipc.Result;
import io.github.fastipc.SubscriberRole;
import java.time.Duration;
public class Subscriber {
public static void main(String[] args) {
// gated: misses nothing (the publisher waits for it)
try (Connection sub = Fipc.joinBroadcast("my_broadcast",
SubscriberRole.GATED, Duration.ofSeconds(5))) {
while (true) {
byte[] tick = sub.receive();
System.out.println(new String(tick)); // tick 1..3
}
} catch (FipcException e) {
// DISCONNECTED: the publisher closed
if (e.result() != Result.DISCONNECTED) throw e;
}
}
}Work queue
many producers, each message to one consumerA 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 (maxPiece()), and wait in the queue until a consumer takes them. Each message goes to one consumer, and again, to another, only if that consumer's process ends before acknowledging it (delivery, below). Here the producer creates the queue (Fipc.createQueue returns a WorkQueue), joins it as a producer (Fipc.joinQueue with QueueRole.PRODUCER), sends job 1 to job 3, which wait in the queue, and waits for the consumer to join (queue.waitForMembers(QueueRole.CONSUMER, 1)). The owner's process admits every producer and consumer by itself, the owner's own too. queue.close() ends the queue: the consumer receives what is left, then DISCONNECTED, and a producer's sends throw DISCONNECTED from then on; a producer's or a consumer's close never ends it. A queue lives until close() or the end of its process; dropping the object doesn't end it: unlike the other handles, a WorkQueue has no Cleaner, as its use lives in its members.
import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import io.github.fastipc.QueueRole;
import io.github.fastipc.WorkQueue;
public class Producer {
public static void main(String[] args) {
// 4 KiB of messages per producer, 1 producer and 1
// consumer: this process owns the queue and is its
// producer
try (WorkQueue queue = Fipc.createQueue("my_queue", 4096,
1, 1)) {
try (Connection producer = Fipc.joinQueue("my_queue",
QueueRole.PRODUCER)) {
for (int i = 1; i <= 3; i++) {
// waits in the queue until a consumer takes it
producer.send(("job " + i).getBytes());
}
// waits for the consumer to join
queue.waitForMembers(QueueRole.CONSUMER, 1);
}
}
// closing the queue ends it: the consumer gets
// DISCONNECTED after the jobs
}
}import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import io.github.fastipc.FipcException;
import io.github.fastipc.QueueRole;
import io.github.fastipc.Result;
import java.time.Duration;
public class Consumer {
public static void main(String[] args) {
try (Connection consumer = Fipc.joinQueue("my_queue",
QueueRole.CONSUMER, Duration.ofSeconds(5))) {
while (true) {
byte[] job = consumer.receive();
System.out.println(new String(job)); // job 1..3
}
} catch (FipcException e) {
// DISCONNECTED: the queue ended
if (e.result() != Result.DISCONNECTED) throw e;
}
}
}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 |
|---|---|---|---|
receive() (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 |
receiveAcquire(), then receiveRelease() at once, then the work | at the release, before the work | as above | at most once: no duplicates |
receiveAcquire(), the work, then receiveRelease() | 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.
Service
many clients, a pool of servers, with repliesA 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 receive), 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 maxMessage bytes, the owner's choice (maxPiece()). Here the server creates the service (Fipc.createService returns a Service), joins it as its server (Fipc.joinService with ServiceRole.SERVER: the owner's process admits every joiner by itself, its own included) and answers three requests with their uppercase. The client joins (ServiceRole.CLIENT) and calls ping 1 to ping 3: call sends a request and returns its reply. The service's close() ends it: clients' sends and servers' receives throw DISCONNECTED, and a client receives the replies that came before the end, then DISCONNECTED; a client's or a server's close never ends the service (a server's answers the request it holds ABANDONED). A Service, as a WorkQueue, has no Cleaner: a service lives until close() or the end of its process, and dropping the object doesn't end it.
import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import io.github.fastipc.Service;
import io.github.fastipc.ServiceRole;
import java.time.Duration;
public class ServiceServer {
public static void main(String[] args) {
// 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
try (Service service = Fipc.createService("my_service",
4096, 256, 1, 1)) {
try (Connection server = Fipc.joinService("my_service",
ServiceRole.SERVER, Duration.ofSeconds(5))) {
// three requests, then the end
for (int i = 1; i <= 3; i++) {
// the next request of any client
String request = new String(server.receive());
// the reply goes to its client, and never waits
server.send(request.toUpperCase().getBytes());
}
}
}
// closing the service ends it: the client still
// receives the replies sent
}
}import io.github.fastipc.Connection;
import io.github.fastipc.Fipc;
import io.github.fastipc.ServiceRole;
import java.time.Duration;
public class ServiceClient {
public static void main(String[] args) {
try (Connection client = Fipc.joinService("my_service",
ServiceRole.CLIENT, Duration.ofSeconds(5))) {
for (int i = 1; i <= 3; i++) {
// sends the request and returns its reply; throws
// FipcException (ABANDONED) if the server ended
// before it replied
byte[] reply = client.call(("ping " + i).getBytes());
System.out.println(new String(reply)); // PING 1..3
}
}
}
}- A client's replies come in the order of its requests.
send(request)sends a request, andreceive(into a new array, a buffer or a segment, or in placereceiveAcquireuntilreceiveRelease) receives the reply to its oldest request whose reply it hasn't received yet, 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, plusmaxMessagerounded 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, timeout)never returns another request's 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. A call checks its request first:INVALIDfor an empty one,TOO_LARGEovermaxPiece(), at once, even while a reply is owed. Then a call on a cancelled connection throwsCANCELLEDat once, whatever its timeout, and sends nothing: it could never receive the reply. 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 throwsTIMEOUTwithout sending, so no server sees that request; with a zero timeout no step waits, and it sends if there is room. After a call that gave up, a plainreceivereturns that call's late reply (orABANDONED) first: replies come in request order. A call throwsINVALIDwhile a request sent withsend(orsendCommit) still has its reply to be received: that reply is yours, so receive it first; on any connection other than a service's client it throwsINVALIDtoo. A call sends and receives, so no other thread may send or receive on the connection meanwhile.- A server holds one request at a time.
receivetakes the next request of any client (each client's in the order it sent them), andsend(reply)replies to it and never waits; until then its receives throwINVALID, and a reply with no request held throwsINVALID. A request received in place stays the server's until the reply'ssendorsendCommit, and the reply may be written from it in place. The reply invalidates the segment, as doreceiveRelease(which leaves the request held until its reply), the next receive andclose()on the receiving thread; another thread's reply throwsWrongThreadExceptionwhile the segment is valid, so release it before replying there. A server's close answers a request it hasn't replied toABANDONED: one held in place, at its segment's end. A reply to a client that has left is dropped. A server may receive on one thread and reply on another. ABANDONED: a client's receive (or call) throws it when 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 owner's service:
service.waitForMembers(role, atLeast, timeout)waits until that many members of the role (ServiceRole.CLIENTorServiceRole.SERVER; without one, both) have joined, and returns the count;service.cancel()makes its waits throwCANCELLED, from any thread. Clients wait while no server is joined, and servers while no client is. - 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.
Any language on the other end
one protocolThe Server and Client above work with a peer in any of the ten languages: every pair of languages is tested.
Threads and close
close, then joinOn a connection, one thread at a time sends and one thread at a time receives, and the two may run at once. cancel() and close() may be called from any thread, at any time. close() cancels the connection, so a waiting call throws CANCELLED, and the native close runs once the calls in progress have returned: no call ever runs on a closed connection.
Waits are not interruptible: Thread.interrupt() doesn't wake a call; cancel() and close() do. A waiting call blocks its thread in native code, which pins a virtual thread, so use platform threads for calls that wait long. A listener or connection that is never closed is closed by a Cleaner once it is unreachable; a work queue or a service never is.
Thread reader = Thread.ofPlatform().start(() -> {
try {
while (true) {
handle(new String(conn.receive()));
}
} catch (FipcException e) {
// CANCELLED (closed) or DISCONNECTED
}
});
...
conn.close(); // wakes the reader: CANCELLED
reader.join();Install and native access
Java 22+ · FFM APIThe binding calls restricted methods of the FFM API, so grant it native access, or the JVM prints a warning on the first call: --enable-native-access=ALL-UNNAMED with the jar on the class path, --enable-native-access=io.github.fastipc on the module path, or Enable-Native-Access: ALL-UNNAMED in an executable jar's manifest.
The jar's native library is extracted once per content into the user's cache (%LOCALAPPDATA%\fipc, ~/.cache/fipc, on macOS ~/Library/Caches/fipc) and checked before each load. The system property fastipc.library.path names another one; Fipc.libraryPath() says which was loaded.
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.fastipc:fipc:1.0.0")
}
application {
mainClass = "Server"
applicationDefaultJvmArgs =
listOf("--enable-native-access=ALL-UNNAMED")
}Reference
io.github.fastipc| Call | What it does |
|---|---|
Fipc.createStream(name, capacity) | Claims the name for a stream and returns its Listener; ADDR_IN_USE if another stream, broadcast, queue or service holds it. |
listener.accept([timeout]) | Sets the next client's connection up on the calling thread and returns its Connection. One client at a time: close it before the next accept. Fipc.NO_WAIT polls. |
listener.cancel() · close() | Cancel waiting accepts · free the name (any thread); accepted connections stay open. |
Fipc.connectStream(name, [timeout]) | Connects to the stream's server, waiting for it to create the stream and accept: client and server run on different threads or processes. |
send(message, [timeout]) | Sends one message of any size: a byte[] (or a range of one), a ByteBuffer or a MemorySegment. |
receive([timeout]) | Receives one message of any size as a new byte[]. |
receive(buffer, [timeout]) | Receives into your ByteBuffer or MemorySegment and returns the length; TOO_LARGE if it doesn't fit. |
sendMany(messages, [timeout]) · receiveMany(maxCount, [timeout]) | Batches: send a List<byte[]>, published together, and return how many went (on an error, sentMessages()) · receive every message already there, up to maxCount, as a list (or into a segment, their lengths in a long[]). Batch both ends. |
sendAcquire(n) · sendCommit(n) | Zero-copy send. |
receiveAcquire() · receiveRelease() | Zero-copy receive. |
maxPiece() · cancel() · close() | The zero-copy limit · cancel waiting calls · end the connection (any thread). |
Fipc.createBroadcast(name, capacity, maxSubscribers, maxStall) | Claims the name for a broadcast and returns the publisher's Connection; closing it ends the broadcast. |
publisher.waitForMembers([atLeast, [timeout]]) | The number of joined subscribers; with atLeast, waits for that many. memberCount() holds the count at a TIMEOUT. |
Fipc.joinBroadcast(name, role, [timeout]) | Joins the broadcast as a SubscriberRole.GATED or LOSSY subscriber; it receives, then DISCONNECTED once the publisher is closed. |
Fipc.createQueue(name, capacity, maxProducers, maxConsumers) | Claims the name for a work queue and returns its WorkQueue, which lives until close() or the end of the process. |
queue.waitForMembers([role,] atLeast, [timeout]) | Waits until that many members of the QueueRole (without one, of both roles) are joined, and returns the count. |
queue.cancel() · close() | Cancel waiting waits · end the queue (any thread). |
Fipc.joinQueue(name, role, [timeout]) | Joins the queue as a QueueRole.PRODUCER, which sends, or a CONSUMER, which receives each message no other consumer took, then DISCONNECTED once the queue has ended. The owner joins its own queue so too. |
Fipc.createService(name, capacity, maxMessage, maxClients, maxServers) | Claims the name for a service and returns its Service, which lives until close() or the end of the process. |
service.waitForMembers([role,] atLeast, [timeout]) | Waits until that many members of the ServiceRole (without one, of both roles) are joined, and returns the count. |
service.cancel() · close() | Cancel waiting waits · end the service (any thread). |
Fipc.joinService(name, role, [timeout]) | Joins the service as a ServiceRole.CLIENT, which sends requests and receives their replies in order, or a SERVER, which receives the next request of any client and replies with its next send. The owner joins its own service so too. |
call(request, [timeout]) | A service's client: sends the request and returns its reply; ABANDONED if its server ended before it replied. |
Measured through this binding, copied 16-byte messages run at 14 to 15 million per second between two processes on the x86-64 baseline machine and 27 to 35 million on Apple's M1, after the JIT's warm-up (details).