IPC
Status: draft · Handles are typed, one type per kind (decided 2026-11-02) · Builds on Transports and Connections · Handles answer part of the open capabilities design · The first step of Comline for systems software
The question: what does Comline need to be a good interface between processes on one machine, beyond what a network connection needs?
Where things stand:
- The runtime calls itself an RPC/IPC runtime, but nothing designs IPC. The runtime contract lists Unix sockets and in-process among transports and stops there.
- What already suits IPC: the Rust runtime's lowest tier is
no_stdwith no allocator; dispatch is synchronous; decoders refuse hostile lengths before allocating; generated code forbidsunsafe; the shared runtime core has a C ABI. - Connections plan peer information from the transport in each connection's state, and interceptors that can reject calls (for authorization) before they reach a handler.
- Transports is a draft: optional declarations, transport profiles with stated facts, and a "transport fit" guarantee where declared.
What makes IPC different
| Topic | Over a network | Between processes |
|---|---|---|
| Transports | TCP, UDP, WebSocket | Unix sockets, pipes, shared memory, and each platform's own IPC |
| What can be sent | bytes | bytes, and handles: file descriptors, shared memory, endpoints to call |
| Large payloads | copied | can be shared in place, through shared memory |
| Who the peer is | what the peer proves (TLS) | what the OS reports: its process, user, sandbox |
| A peer that dies | noticed by a timeout | reported by the OS at once |
| Versions | independent deployments | often shipped together, but partial updates still mix versions |
| Latency | milliseconds | microseconds: per-call overhead dominates |
IPC transports
Profiles in the transports shape, with three facts IPC adds:
handles: which kinds of handle the transport can carry, and how many per message (Linux allows 253 file descriptors per message; a Zircon channel, 64);peer_identity: what the OS reports about the peer;peer_death: whether the OS reports a peer's exit.
| Profile | Boundaries | Handles | Peer identity | Peer death |
|---|---|---|---|---|
unix-stream@1 |
a stream | file descriptors | process, user, group | on close |
unix-seqpacket@1 |
messages | file descriptors | process, user, group | on close |
pipe@1 (anonymous pipes, stdio) |
a stream | none | the parent knows its child | on close |
windows-pipe@1 |
messages or a stream | handles, duplicated by the sender | the client's process and token | on close |
memory-ring@1 (a shared-memory ring, Comline's own, later) |
messages | none in the ring | from the handle that set it up | needs a side channel |
| Platform IPC, each its own profile: Mach and XPC (macOS), Binder (Android), D-Bus (Linux), ALPC (Windows), Zircon channels (Fuchsia) | messages | each platform's own | each platform's own | each platform's own |
postmessage@1 (browsers, workers, Electron) |
messages | transferable objects (MessagePort, ArrayBuffer) |
the origin | page or worker lifetime |
Handles
A handle is something the OS lets one process give another: an open file, a shared memory region, an endpoint of another channel. It travels beside the bytes, not in them.
The options
| In the schema | In one line | Assessment | |
|---|---|---|---|
| H1 | handle |
one opaque type for any handle | rejected: a receiver can't tell a file from a memory region, so it can't check what it was given |
| H2 | file, memory, channel, and endpoints of a protocol |
a type per kind of handle, each checked on arrival | decided 2026-11-02 |
| H3 | nothing: handles are a transport feature, passed beside calls | handles outside the schema | rejected: the schema couldn't say which call carries which handle, so nothing could check it |
H2: a type per kind
| Type | What it is | Rust | C |
|---|---|---|---|
file |
an open file, socket or pipe | OwnedFd / OwnedHandle |
int / HANDLE |
memory |
a shared memory region; memory[max N] bounds its size in bytes |
a mapped-region type | a descriptor and a size |
channel |
a raw IPC endpoint | the transport's endpoint | its handle |
remote Orders |
an endpoint to call protocol Orders |
a client for Orders |
a client handle |
receiver Orders |
an endpoint to serve Orders |
a server binding for Orders |
a server handle |
remote and receiver are capabilities in the Cap'n Proto and Mojo sense: a
call can hand the peer a new, typed connection. That's the capabilities part of
the open design.
protocol Files {
/// Opens a file and hands it to the caller.
function open(path: char[max 4096]) -> file;
/// Shares a frame buffer without copying it.
function frame() -> memory[max 8388608];
/// Gives the caller its own session, served on a new channel.
function session() -> remote Session;
}
How they work, whichever spelling is chosen:
- On the wire, a handle is encoded as its index in the message's
attachments: a
u32inpacked@1, a number injson@1. The handles themselves go through the transport. - Checked on arrival: the decoder checks each handle is the kind the schema
says (
fstat,zx_object_get_infoand their equivalents), and that the message carries exactly the handles its body refers to. An unexpected or wrong handle is refused, and closed. - Ownership moves: sending a handle gives it away; the sender keeps a copy only by duplicating it first. A received handle is owned by the receiver. Every handle in a refused message is closed, so nothing leaks.
- Bounded like everything else: an array of handles needs a maximum, and the transport's own limit per message is part of transport fit.
- Transport fit becomes required, not optional: a protocol that sends a handle can only run over a transport that carries that kind. That's checked at build, against the transports declared (Transports), and a protocol using handles must declare one.
- Version comparison: changing a handle's kind is breaking.
- Languages: Rust and C map to the OS types. TypeScript has handles only in
Node (file descriptors over Unix sockets, in limited cases) and in browsers
(transferables over
postMessage); its support claim says which.
Rights
A right restricts what the receiver can do with a handle: read but not write, map but not execute. It matters most for handing a sandboxed process a read-only view of a buffer or a file. Some systems attach rights to every handle (Zircon: read, write, map, duplicate; Mach: send and receive rights).
Open, to be settled after the rest of IPC:
| Option | Trade-off | |
|---|---|---|
| R1 | no rights in the schema: the kind only | simplest; the receiver can do whatever the OS lets the handle do |
| R2 | a small portable set in the schema (read, write), as in memory(read) |
the sender drops extra rights before sending, and the decoder verifies them. Every platform can express read and write: a file's open mode and sealed shared memory (memfd seals) on Linux, access masks on Windows, rights on Zircon |
| R3 | each platform's full set of rights | the most precise, but not portable |
Adding rights later tightens existing protocols, a breaking change, so it's best settled before 1.0. If rights come, R2 is the recommendation: read-only sharing is the real security need, and its small set works everywhere.
Who the peer is
The OS reports the peer: its process id, user and group (SO_PEERCRED on
Linux), its audit token (macOS), its uid (Android Binder), its token (Windows).
Unlike anything the peer sends, the peer can't forge it.
- Reported, not claimed: the transport puts the identity in the connection's state (Connections), and handlers and interceptors read it there.
- A guarantee, when declared: a protocol can require that a peer's identity is checked before any call is served. The check itself is an interceptor; what core guarantees is that the identity comes from the OS.
A peer that dies
A transport that reports a peer's exit closes the connection at once: every waiting call fails with "peer gone", instead of each waiting for its timeout.
Versions
Processes shipped together still skew: a partial update, an old helper process still running. The handshake stays, and IPC transports make it cheap: one message each way, on a connection that's often reused for the process's whole life.
Zero copy
A memory handle shares a region; reading a value in place in it needs a
memory layout both sides agree on byte for byte. That's the exact-layout format
in Comline for systems software.
Prior art
| System | What to learn |
|---|---|
| Chromium Mojo | an IDL with typed handles and endpoints (pending_remote, pending_receiver) |
| Android AIDL and HIDL, over Binder | IPC between apps, and HIDL for hardware layers: driver interfaces |
| Fuchsia FIDL, over Zircon channels | typed handles with rights; Fuchsia's drivers speak FIDL |
| D-Bus | a message bus, names and introspection |
| Windows COM and MIDL | interfaces across processes, with marshalling |
| Cap'n Proto | capabilities, and reading values in place |
Open
- ~~Handles~~: decided, H2. The spellings (
file,memory,channel,remote P,receiver P) are as proposed. - Rights on handles: R1, R2 or R3 (above), settled later.
- Which platform IPCs get official profiles, and in what order.
- A shared-memory ring of Comline's own, and its side channel for peer death.
- Requiring a peer identity check: how a protocol states it.