FastIPC FastIPC v1.0.0
include/fipc.h · fastipc.dll / libfastipc.so / libfastipc.dylib

C

The whole API is one header: 30 functions, four handles and eleven result codes, with the C calling convention. Every binding is a thin layer over these calls.

Build and link

the Zig build system · no CMake

The recommended way to build C (and C++) against FastIPC is the Zig build system: no CMake or other build tool, and Zig 0.17.0 is a single download (a program written in Zig imports the Zig module instead). It builds the library for your target and compiles your C with its bundled Clang. After zig fetch --save git+https://github.com/fastipc/fastipc, your build.zig links the dependency's artifact fastipc, the shared library, which also puts fipc.h and fipc.hpp on the include path. The repository's examples/c-cpp is such a project, with the servers and clients of this page and the C++ page.

Any other compiler works too. Take the headers and the library from the GitHub release: an archive per OS with include/, and lib/libfastipc.so (Linux x64 or arm64), lib/libfastipc.dylib (macOS 14.4+ on Apple Silicon) or bin/fastipc.dll with its import library lib/fastipc.lib (Windows x64). Or build them in a checkout with zig build -Doptimize=ReleaseFast: zig-out/include, zig-out/lib and, on Windows, zig-out/bin. On Linux, the program finds libfastipc.so through an rpath, LD_LIBRARY_PATH or a system library directory. On macOS, the dylib's install name is @rpath/libfastipc.dylib, so link the program with an rpath to its folder; the dylib is ad hoc signed, not notarized, so after a browser download clear the archive's quarantine flag with xattr -d com.apple.quarantine lib/libfastipc.dylib (an app with the hardened runtime and library validation signs the dylib it embeds with its own identity). On Windows, define FASTIPC_SHARED so the header declares the functions dllimport, and put fastipc.dll next to the program or on PATH. Every build targets x86-64-v3 (on macOS, Apple M1 and macOS 14.4).

build.zig
const fastipc = b.dependency("fipc", .{
    .target = target,
    .optimize = optimize,
}).artifact("fastipc");
exe.root_module.addCSourceFile(.{
    .file = b.path("src/server.c"),
    .flags = &.{"-std=c11"},
});
exe.root_module.addCMacro("FASTIPC_SHARED", "1"); // Windows
exe.root_module.linkLibrary(fastipc);
b.installArtifact(fastipc); // the library next to the programs
shell: other compilers
# Linux, from the release archive
cc server.c -Iinclude -Llib -lfastipc -o server
LD_LIBRARY_PATH=lib ./server   # then ./client in another terminal

# macOS, from the release archive
xattr -d com.apple.quarantine lib/libfastipc.dylib   # if a browser downloaded it
cc server.c -Iinclude -Llib -lfastipc -Wl,-rpath,@executable_path/lib -o server
./server

# Windows, MSVC (a developer prompt)
cl /DFASTIPC_SHARED /Iinclude server.c lib\fastipc.lib
copy bin\fastipc.dll .

Server and client

two processes, one name

Run the server in one process, then the client in another; fipc_stream_connect waits up to its timeout for a server to create the stream and accept. 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 (every pair of languages is tested).

server.c
#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;
}
client.c
#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;
}

The rules

the header's conventions
timeout_ms

Timeouts

Always the last parameter, in milliseconds. FIPC_NO_WAIT (0) doesn't wait; FIPC_FOREVER (−1) waits for ever. A call that runs out returns FIPC_TIMEOUT having changed nothing.

name · capacity

Names and rings

A name is 1 to 245 characters of [A-Za-z0-9_.-], not starting with . or -, local to the user (Linux, macOS) or the desktop session (Windows). The name's creator picks the ring size: for a stream, a power of two from 1 KiB to 2 GiB.

threads

One sender, one receiver

On a connection, one thread at a time sends and one receives, and the two may run at once. A broadcast's, a queue's or a service's owner needs no thread for its joiners: its process admits them on a thread of the library's, so it may join from any thread. Cancels work from any thread. To stop a waiting thread: cancel, join it, then close.

any size

Messages

A message holds at least one byte and arrives in order (a work queue promises no order; a service's replies arrive in the order of their requests). The copying calls take any size; a long message travels in pieces and is delivered whole or not at all.

FIPC_TOO_LARGE

Buffers too small

A receive into a buffer that is too small takes nothing and reports the length, so allocate that much and call again. Pass a zero-length buffer to ask for the length only.

FIPC_DISCONNECTED

The peer's end

Final for that connection: sends return it at once, receives after every completed message. Close the connection, then accept or connect a new one.

Zero-copy

messages of one piece: up to fipc_max_piece(conn) bytes

Reserve room in the ring, write the message there, publish it. On the other side, read it where it lies and release it. Pointers are 16-byte aligned. fipc_max_piece is the ring's capacity less 64 bytes; anything longer goes through fipc_send.

send in place
void* out;
if (fipc_send_acquire(conn, 64, &out,
                      FIPC_FOREVER) == FIPC_OK) {
    int n = snprintf(out, 64, "tick %d", tick);
    fipc_send_commit(conn, (size_t) n); /* publish n bytes */
}
receive in place
const void* data;
size_t len;
if (fipc_recv_acquire(conn, &data, &len,
                      FIPC_FOREVER) == FIPC_OK) {
    handle(data, len);       /* read-only, in the ring */
    fipc_recv_release(conn); /* frees its room */
}

Broadcast

one publisher, many subscribers

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. Messages are of one piece, up to a quarter of the capacity. fipc_broadcast_create returns the publisher's connection; the publisher's process admits the subscribers on a thread of the library's, so no thread of yours waits for them. The publisher waits for the first one (fipc_broadcast_wait_for_members) before it sends tick 1 to tick 3; its close ends the broadcast and frees the name. The subscriber joins gated (fipc_broadcast_join with FIPC_SUBSCRIBER_GATED), so it misses nothing (the publisher waits for it), and receives until FIPC_DISCONNECTED, which comes after the last message. A lossy subscriber (FIPC_SUBSCRIBER_LOSSY) never holds the publisher back: one that falls a lap behind gets FIPC_OVERRUN, with the number of messages lost in *out_len. A gated one that holds the publisher back for the broadcast's max_stall_ms is unsubscribed (FIPC_UNSUBSCRIBED).

publisher.c
#include <fipc.h>
#include <stdio.h>

int main(void)
{
    fipc_conn_t* publisher;
    /* 64 KiB of messages, up to 8 subscribers; one that holds the publisher back 5 s is dropped */
    if (fipc_broadcast_create("my_broadcast", 1 << 16, 8, 5000, &publisher) != FIPC_OK)
        return 1;
    uint32_t count;
    /* waits for the first subscriber */
    if (fipc_broadcast_wait_for_members(publisher, 1, &count, FIPC_FOREVER) == FIPC_OK)
    {
        for (int i = 1; i <= 3; i++)
        {
            char tick[16];
            int len = snprintf(tick, sizeof tick, "tick %d", i);
            fipc_send(publisher, tick, (size_t) len, FIPC_FOREVER);
        }
    }
    fipc_close(publisher); /* ends the broadcast: subscribers get FIPC_DISCONNECTED after the ticks */
    return 0;
}
subscriber.c
#include <fipc.h>
#include <stdio.h>

int main(void)
{
    fipc_conn_t* subscriber;
    /* gated: misses nothing (the publisher waits for it) */
    fipc_result_t result = fipc_broadcast_join("my_broadcast", FIPC_SUBSCRIBER_GATED, &subscriber, 5000);
    if (result != FIPC_OK)
    {
        fprintf(stderr, "join: %s\n", fipc_result_str(result));
        return 1;
    }
    char buf[256];
    size_t len;
    while ((result = fipc_recv(subscriber, buf, sizeof buf, &len, FIPC_FOREVER)) == FIPC_OK)
        printf("%.*s\n", (int) len, buf); /* tick 1, tick 2, tick 3 */
    fipc_close(subscriber);
    return result == FIPC_DISCONNECTED ? 0 : 1; /* the publisher closed */
}

Work queue

many producers, each message to one consumer

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, to another, only if that consumer's process ends before acknowledging it (delivery, below). Here the producer creates the queue (fipc_queue_create: a fipc_queue_t) and joins it as its producer with fipc_queue_join, as any process does: the owner's process admits every joiner on a thread of the library's, so the thread that created the queue may join it. It sends job 1 to job 3, waits for the consumer (fipc_queue_wait_for_members) and closes the queue (fipc_queue_close), which ends it: the consumer receives what is left, then FIPC_DISCONNECTED. Producers and consumers of other processes join with fipc_queue_join (FIPC_PRODUCER, FIPC_CONSUMER); a producer's or a consumer's close never ends the queue.

producer.c
#include <fipc.h>
#include <stdio.h>

int main(void)
{
    fipc_queue_t* queue;
    fipc_conn_t* producer = NULL;
    /* 4 KiB of messages per producer, 1 producer and 1 consumer: this process owns the queue and is its producer */
    if (fipc_queue_create("my_queue", 4096, 1, 1, &queue) != FIPC_OK)
        return 1;
    fipc_result_t result = fipc_queue_join("my_queue", FIPC_PRODUCER, &producer, 5000);
    for (int i = 1; i <= 3 && result == FIPC_OK; i++)
    {
        char job[16];
        int len = snprintf(job, sizeof job, "job %d", i);
        result = fipc_send(producer, job, (size_t) len, FIPC_FOREVER); /* waits in the queue for a consumer */
    }
    uint32_t count;
    /* waits for the consumer to join */
    if (result == FIPC_OK)
        result = fipc_queue_wait_for_members(queue, FIPC_CONSUMER, 1, &count, FIPC_FOREVER);
    fipc_close(producer);
    fipc_queue_close(queue); /* ends the queue: the consumer gets FIPC_DISCONNECTED after the jobs */
    return result == FIPC_OK ? 0 : 1;
}
consumer.c
#include <fipc.h>
#include <stdio.h>

int main(void)
{
    fipc_conn_t* consumer;
    fipc_result_t result = fipc_queue_join("my_queue", FIPC_CONSUMER, &consumer, 5000);
    if (result != FIPC_OK)
    {
        fprintf(stderr, "join: %s\n", fipc_result_str(result));
        return 1;
    }
    char buf[256];
    size_t len;
    while ((result = fipc_recv(consumer, buf, sizeof buf, &len, FIPC_FOREVER)) == FIPC_OK)
        printf("%.*s\n", (int) len, buf); /* job 1, job 2, job 3 */
    fipc_close(consumer);
    return result == FIPC_DISCONNECTED ? 0 : 1; /* the queue ended */
}

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 receivesWhen it acknowledgesIf its process endsWhat the application gets
fipc_recv (copying)inside the call, before it returnsinside the call: the message goes to another consumer, and this one never saw it; after the call returned: it doesn't go againat most once: no duplicates
fipc_recv_acquire, then fipc_recv_release at once, then the workat the release, before the workas aboveat most once: no duplicates
fipc_recv_acquire, the work, then fipc_recv_releaseat the release, after the workbefore the release: the message goes to another consumerat least once: a duplicate is possible, so the work must be idempotent

Service

many clients, a pool of servers, with replies

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 a client receives its replies in the order of its requests. Here the server creates the service (fipc_service_create: a fipc_service_t) and joins it as its server with fipc_service_join (FIPC_SERVER), answers three requests with their uppercase and closes the service (fipc_service_close), which ends it; the client joins (FIPC_CLIENT), sends ping 1 to ping 3 and prints each reply. A request whose server ended before it replied gets FIPC_ABANDONED.

service_server.c
#include <ctype.h>
#include <fipc.h>

int main(void)
{
    fipc_service_t* service;
    fipc_conn_t* server = NULL;
    /* 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 */
    if (fipc_service_create("my_service", 4096, 256, 1, 1, &service) != FIPC_OK)
        return 1;
    fipc_result_t result = fipc_service_join("my_service", FIPC_SERVER, &server, 5000);
    for (int i = 1; i <= 3 && result == FIPC_OK; i++) /* three requests, then the end */
    {
        char buf[256];
        size_t len;
        result = fipc_recv(server, buf, sizeof buf, &len, FIPC_FOREVER); /* the next request of any client */
        for (size_t j = 0; result == FIPC_OK && j < len; j++)
            buf[j] = (char) toupper((unsigned char) buf[j]);
        if (result == FIPC_OK)
            result = fipc_send(server, buf, len, FIPC_NO_WAIT); /* the reply goes to its client, and never waits */
    }
    fipc_close(server);
    fipc_service_close(service); /* ends the service: the client still receives the replies sent */
    return result == FIPC_OK ? 0 : 1;
}
service_client.c
#include <fipc.h>
#include <stdio.h>

int main(void)
{
    fipc_conn_t* client;
    fipc_result_t result = fipc_service_join("my_service", FIPC_CLIENT, &client, 5000);
    if (result != FIPC_OK)
    {
        fprintf(stderr, "join: %s\n", fipc_result_str(result));
        return 1;
    }
    for (int i = 1; i <= 3 && result == FIPC_OK; i++)
    {
        char buf[256];
        size_t len = (size_t) snprintf(buf, sizeof buf, "ping %d", i);
        result = fipc_send(client, buf, len, FIPC_FOREVER); /* a request: a server takes it */
        if (result == FIPC_OK)
            result = fipc_recv(client, buf, sizeof buf, &len, FIPC_FOREVER); /* its reply */
        if (result == FIPC_OK)
            printf("%.*s\n", (int) len, buf); /* PING 1, PING 2, PING 3 */
    }
    if (result != FIPC_OK) /* FIPC_ABANDONED: the server ended before it replied */
        fprintf(stderr, "request: %s\n", fipc_result_str(result));
    fipc_close(client);
    return result == FIPC_OK ? 0 : 1;
}

The calls. A service's connections use the calls every connection has; what each does depends on the role. Requests and replies are of one piece, 1 to max_message bytes, the owner's choice at fipc_service_create (1 to a quarter of the capacity), and fipc_max_piece returns it on both roles, so a buffer of that size takes every request and every reply.

CallOn a clientOn a server
fipc_send, fipc_send_acquire / _commitSends a request. Waits up to its timeout for room in the client's ring.Replies to the request it holds; never waits. FIPC_INVALID with no request held, FIPC_TOO_LARGE over fipc_max_piece.
fipc_recvThe reply to its oldest request whose reply it hasn't received, waiting for it if it hasn't come; FIPC_ABANDONED (with *out_len 0) if that request's server ended before it replied.The next request of any client, copied; the server then holds it until it replies.
fipc_recv_acquire / fipc_recv_releaseThe next reply in place, until the release, the next receive or the close.The next request in place, valid until the reply's fipc_send or fipc_send_commit; the release does nothing.
fipc_closeLeaves the service: requests no server took are dropped, and the replies to those already taken are lost.Leaves the service; a request it still holds is answered FIPC_ABANDONED.

All 30 functions

the exports, exactly
FunctionWhat it does
fipc_stream_createServer: claims a name for a stream and listens on it with rings of a given capacity, and returns its listener. Doesn't wait.
fipc_stream_acceptServer: waits for a client, sets its connection up on the calling thread and returns it. One client at a time. A call that times out keeps a client's setup for the next, so timeout 0 polls.
fipc_listener_cancelMakes every waiting and later accept return FIPC_CANCELLED, dropping a client whose setup began. Any thread.
fipc_listener_closeStops listening and frees the listener; accepted connections stay open.
fipc_stream_connectClient: connects to the stream on a name and sets the connection up on the calling thread, waiting for a server to create it and accept: client and server run on different threads or processes.
fipc_max_pieceThe longest message the zero-copy calls take: the capacity less 64 bytes (a broadcast's or a queue's connections: a quarter of the capacity, for every message; a service's: its max_message).
fipc_cancelMakes every waiting and later call on the connection return FIPC_CANCELLED. Any thread.
fipc_closeEnds the connection (the peer gets FIPC_DISCONNECTED), unmaps its rings, frees the handle.
fipc_sendSends one message of any size, copying it in, in pieces if needed.
fipc_recvReceives one message of any size into your buffer.
fipc_send_manySends several messages in one call, in order, and publishes them together, with at most one wake of the receivers; before any wait for room it publishes what it wrote. Every message is checked before any goes, and *out_sent says how many went. Batch both ends: a receiver that takes one message per call falls behind a batching sender.
fipc_recv_manyWaits for the next message, then takes every further message already there that fits your buffer, packed back to back, each one's length in an array of lengths (a broadcast's subscriber: those its publisher has moved past). Any result but FIPC_OK is the first message's, as fipc_recv returns it.
fipc_send_acquireZero-copy send, step 1: reserves contiguous room in the ring.
fipc_send_commitZero-copy send, step 2: publishes the reserved bytes as one message.
fipc_recv_acquireZero-copy receive, step 1: the next message, in place and read-only.
fipc_recv_releaseZero-copy receive, step 2: frees the message's room.
fipc_broadcast_createPublisher: claims a name for a broadcast of a given capacity and number of subscribers, and returns the publisher's connection, whose sends go to every subscriber; its process admits the subscribers on a thread of the library's. Its close ends the broadcast and frees the name. Doesn't wait.
fipc_broadcast_joinSubscriber: joins the broadcast on a name, lossy (FIPC_SUBSCRIBER_LOSSY) or gated (FIPC_SUBSCRIBER_GATED), and returns its connection, waiting for the publisher to create it, to have a free slot and to admit it. It starts at the publisher's next message as of its admission: a gated joiner holds the publisher back (max_stall_ms counts) from then on, and a lossy one on a busy broadcast may get FIPC_OVERRUN on its first receive.
fipc_broadcast_wait_for_membersPublisher: how many subscribers are joined; waits until at least a given number are, so that nothing is sent before they are there. Any thread.
fipc_queue_createOwner: claims a name for a work queue of a given ring capacity per producer and numbers of producers and consumers, and returns the queue's handle (fipc_queue_t); its process admits the members on a thread of the library's. Doesn't wait.
fipc_queue_joinProducer or consumer (FIPC_PRODUCER, FIPC_CONSUMER): joins the work queue on a name and returns its connection, waiting for the owner to create it, for a free place and to admit it. The owner joins its own queue this way too, from any thread. Each message goes to one consumer, and again to another only if that consumer's process ends before acknowledging it, in no promised order.
fipc_queue_wait_for_membersOwner: how many members of a role (or of both) are joined; waits until at least a given number are. FIPC_DISCONNECTED once the queue has ended. Any thread.
fipc_queue_cancelMakes every waiting and later fipc_queue_wait_for_members return FIPC_CANCELLED; the queue goes on. Any thread.
fipc_queue_closeEnds the queue (producers get FIPC_DISCONNECTED; consumers receive what is left, then FIPC_DISCONNECTED), frees the name and the handle; the members' connections stay open until their fipc_close.
fipc_service_createOwner: claims a name for a service of a given ring capacity per client, longest request and reply (max_message) and numbers of clients and servers, and returns the service's handle (fipc_service_t); its process admits the members on a thread of the library's. Doesn't wait.
fipc_service_joinClient or server (FIPC_CLIENT, FIPC_SERVER): joins the service on a name and returns its connection, waiting for the owner to create it, for a free place and to admit it. The owner joins its own service this way too, from any thread. A client's send is a request and its receive the next reply, in the order of its requests; a server's receive is the next request of any client and its send the reply.
fipc_service_wait_for_membersOwner: how many members of a role (or of both) are joined; waits until at least a given number are. FIPC_DISCONNECTED once the service has ended. Any thread.
fipc_service_cancelMakes every waiting and later fipc_service_wait_for_members return FIPC_CANCELLED; the service goes on. Any thread.
fipc_service_closeEnds the service (clients' sends and servers' receives get FIPC_DISCONNECTED; clients receive the replies already sent, then FIPC_DISCONNECTED), frees the name and the handle; the members' connections stay open until their fipc_close.
fipc_result_strNames a result code, such as "FIPC_OK".

Results

fipc_result_t
ValueCodeMeaning
0FIPC_OKDone.
1FIPC_TIMEOUTThe timeout ran out; with timeout 0, there was nothing to do without waiting.
2FIPC_DISCONNECTEDThe peer closed the connection or its process ended.
3FIPC_CANCELLEDA cancel (fipc_cancel, fipc_listener_cancel, fipc_queue_cancel, fipc_service_cancel) was called, and the call would have had to wait.
4FIPC_TOO_LARGEThe message doesn't fit the buffer, or one piece for zero-copy and on a broadcast, a queue or a service. It stays queued; its length is reported.
5FIPC_INVALIDA bad argument or handle, a call out of order, a peer of another version or user, corrupt ring content.
6FIPC_NO_MEMORYMemory or another OS resource ran out while setting a connection, a broadcast, a queue or a service up.
7FIPC_ADDR_IN_USEfipc_stream_create, fipc_broadcast_create, fipc_queue_create, fipc_service_create: another stream, broadcast, queue or service holds the name.
8FIPC_OVERRUNA lossy subscriber fell a lap behind and lost messages: fipc_recv stores how many in *out_len (at least 1), and the next receive goes on from a recent message.
9FIPC_UNSUBSCRIBEDA gated subscriber held the publisher back for the broadcast's max_stall_ms and was dropped: every later receive returns it.
10FIPC_ABANDONEDA service's client: the server that took the request ended before it replied. The request counts as answered (*out_len is 0), and the next receive returns the next reply; it may or may not have been carried out.