Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku Kernel Ports: Bounded Message Queues and IPC Lifecycle

Use Haiku kernel ports as bounded message IPC with explicit payload sizes, timeout handling, single-reader protocols, and clean close/delete semantics.

Haiku kernel ports are named message queues for passing a small integer code and a byte payload between threads or teams. They are lower-level than BMessage, BMessenger, and BLooper: a port does not give you typed named fields, handler routing, or an application object model. In exchange, the kernel API exposes queue capacity, direct read/write operations, and timeout-aware variants for systems code that needs an explicit IPC boundary.

The capacity passed to create_port() is a queue depth in messages, not a byte budget. Each queued message has a code and a payload size. A full queue can make a writer wait, and an undersized read buffer can consume a message while returning only the bytes copied. These semantics make payload framing and backpressure essential parts of the protocol, not optional defensive polish.

Define a versioned message before writing code

Treat the int32 message code as a small operation identifier and the payload as a versioned record. Keep the record fixed-size where possible, include its version and declared length, and validate both before interpreting it. If a structure crosses 32-bit and 64-bit builds, do not put raw pointers, compiler-dependent size_t, or ABI-sensitive C++ objects in the payload. Encode fixed-width integers and explicit byte sequences instead.

Ports transport bytes; they do not serialize your application object automatically. A BMessage can be flattened when its richer typed fields and message semantics are appropriate, but direct ports do not make that choice for you. Never send an object pointer and expect another team to dereference it. Use shared memory for large data only with an explicit mapping and lifetime protocol, and send the area identity and bounds in the message.

struct RequestV1 {
    uint32 version;
    uint32 length;
    int32 operation;
    int64 requestID;
};

status_t SendRequest(port_id destination, const RequestV1& request)
{
    return write_port_etc(destination, 'rqst', &request, sizeof(request),
        B_RELATIVE_TIMEOUT, 500000);
}

status_t ReceiveRequest(port_id source, RequestV1* request)
{
    if (request == NULL)
        return B_BAD_VALUE;

    int32 code = 0;
    ssize_t size = read_port_etc(source, &code, request, sizeof(*request),
        B_RELATIVE_TIMEOUT, 500000);
    if (size < 0)
        return (status_t)size;
    if (code != 'rqst' || size != sizeof(*request)
        || request->version != 1 || request->length != sizeof(*request))
        return B_BAD_DATA;
    return B_OK;
}

The example assumes the sender and receiver share a compiler ABI and the structure has no padding differences. For a durable or cross-architecture protocol, serialize each field explicitly in a defined byte order. The timeout value is in microseconds because the kernel API uses bigtime_t; choose it from the operation’s budget rather than copying the example value.

Bound both queueing and message size

Queue capacity controls how many messages can wait; it does not cap the size of each payload. Haiku’s implementation rejects writes above its internal maximum message size, but application protocols should impose a smaller bound where possible. A small fixed payload makes stack buffers and validation easier. If a request needs bulk data, use the port for control messages and a separately managed shared area or file for the body.

When the queue fills, decide what backpressure means. A producer can block with write_port() or write_port_etc(), drop coalescible state updates, or return a failure to its caller. Never block an Interface Kit looper or a real-time media callback on a port whose receiver might be stopped. Use a timeout and propagate B_TIMED_OUT, B_WOULD_BLOCK, interruption, or bad-port errors according to the caller’s policy.

On the receiving side, choose a protocol with one of two safe buffer strategies. If all messages have a fixed maximum size, allocate a buffer large enough for the protocol’s maximum. If messages have variable lengths, use an API pattern that can determine the next message size and ensure only one reader consumes that message before the matching read. The port_buffer_size_etc() query reports the next queued payload size, but a separate query/read can race if multiple readers compete.

Most importantly, an undersized read_port_etc() buffer does not preserve the unread suffix for a second read: the current implementation removes the message and copies the smaller of the supplied buffer and the message length. The returned byte count is the number copied. Treat truncation as message loss, reject it, and design the receiver so its buffer cannot be smaller than a valid payload.

Use timeouts as part of the protocol

read_port() and write_port() may wait. Their _etc variants accept timeout flags and a bigtime_t timeout. Relative and absolute timeout modes are exposed by the kernel API. A relative timeout is usually easier for a request budget; an absolute deadline can make several sequential waits share one total deadline. Do not use an infinite wait in a shutdown path unless another operation is guaranteed to wake the thread.

Polling port_count() and then reading is usually inferior to waiting for the message directly: the queue can change between the check and the read, and polling adds needless CPU use. Similarly, checking capacity before writing is not a reservation. Another sender may fill the queue immediately afterward. Perform the operation and handle its result rather than treating observation APIs as synchronization primitives.

Timeout policy should match message semantics. A command that changes persistent state may need an explicit failure response if no receiver accepts it. A telemetry update might be safely coalesced or discarded when stale. A retry must use a request ID or idempotency key if the original write may have succeeded but the reply was lost. Ports provide message transport, not exactly-once request processing.

Make ownership and shutdown explicit

Create a port with a descriptive name and a capacity derived from measured burst behavior. Check the returned port_id before publishing it. Named lookup through find_port() is a discovery mechanism, not proof that the peer is ready or that the port implements the protocol version you expect. A successful lookup should be followed by a handshake containing protocol version, role, and capabilities.

close_port() and delete_port() are separate APIs. In the current kernel implementation, closing marks the port unavailable for new writes and wakes blocked readers and writers; queued messages can still be read until the queue drains. Deletion removes the port object. Coordinate shutdown so the receiver stops admitting new work, drains or explicitly discards queued work, signals the peer, and then closes/deletes the port according to the protocol. Do not free shared payload state while a message referencing it may still be in flight.

Port ownership can be changed with set_port_owner(), and port_info reports the owning team, name, capacity, queued count, and total count. These are diagnostic snapshots, not a transaction with queue operations. If ownership changes during shutdown or the owning team exits, handle invalid identifiers and wakeups as normal lifecycle outcomes. Avoid assuming that a stale port_id remains a valid identity forever.

For one-way events, encode a sequence number if consumers need to detect gaps. For request/reply, include a correlation ID and define how replies map to waiting callers. For broadcasts, a single port is a queue consumed by readers, not automatically a fan-out channel. Build explicit per-client ports or use a higher-level message service when every subscriber must receive each event.

Test failure and backpressure scenarios

Test a missing port, a port deleted while a reader waits, a queue at capacity, a writer timeout, a read timeout, an empty payload, the maximum application payload, one byte over the allowed limit, and an undersized receive buffer. Verify the operation code and byte count independently. Run two-reader and two-writer tests only if the protocol actually permits those topologies.

Also test peer restart, duplicate requests, lost responses, malformed versions, integer overflow in declared lengths, and shutdown with queued messages. Instrument queue depth and timeout counts, but do not log sensitive payloads. Keep logs bounded so an IPC failure does not cause a second resource failure.

Use BMessage and BMessenger for ordinary application-level messages when their typing, routing, and handler semantics fit. Use kernel ports when their byte-level queue and timeout behavior is an intentional part of a system boundary. Correct port code defines a bounded protocol, sizes reads safely, treats waits as fallible, and gives every endpoint a clear shutdown owner.

Related:

Sources:

Comments