Modules

Shared_Memory_Channel

moduleSource
macOSLinuxWindows

#import "Shared_Memory_Channel";

A one-way message queue between two programs (or two parts of one) over shared memory.

A channel has one reader and one writer. The reader creates it by name with reader_create_channel, the writer attaches with writer_connect. The writer sends a message by reserving room (writer_reserve_message), filling it in place and committing it (writer_commit_message); the reader finds messages in order with reader_poll_for_message, reads them in place and finishes each with reader_done_with_message. Nothing copies the payload and nothing blocks: a full ring makes a reserve fail, an empty one makes a poll find nothing, and the program decides how to wait. The ring keeps messages whole, 8-byte aligned, so a message must fit in the ring in one piece.

The pieces are lock-free and safe for exactly one reader and one writer, possibly in different processes. The other end is not trusted to be well-behaved: a damaged header or cursor is reported as a fatal error instead of being followed. Needs Linux, macOS or Windows. Names and the lifetime of the shared object are described on reader_create_channel.

Example
name := tprint("jai-doc-module-%", to_nanoseconds(current_time_monotonic()));

ok, reader := reader_create_channel(name, 512);
assert(ok);
connected, writer := writer_connect(name);
assert(connected);

reserved_ok, reserved := writer_reserve_message(*writer, 3);
assert(reserved_ok);
memcpy(reserved.user_write_pointer, "abc".data, 3);
assert(writer_commit_message(*writer, reserved));

found, fatal, message := reader_poll_for_message(*reader);
assert(found && !fatal && message.user_read_bytes == 3);
assert(reader_done_with_message(*message));

assert(writer_disconnect(*writer));
assert(reader_release_channel(*reader));

Doesn't run in the browser (skipped on wasm).

Procedures#

Channel#

(name: string, bytes: s64) -> (success: bool, channel: Read_Channel)

Creates the channel called name with a region of bytes bytes and returns it as (success, channel). The caller becomes its reader and owns the name: nobody can connect until it exists, and the name goes away with reader_release_channel.

name := tprint("jai-doc-%", to_nanoseconds(current_time_monotonic()));

ok, reader := reader_create_channel(name, 1024);
assert(ok);
assert(reader.header.data_size_in_bytes == 768);

taken, again := reader_create_channel(name, 1024);
assert(!taken); // the name belongs to the first reader

assert(reader_release_channel(*reader));

Doesn't run in the browser (skipped on wasm).

The Message_Queue_Header of a channel, given its Header: a pointer into the shared region, valid until the channel is released. Handy for asking how much is waiting (write_cursor - read_cursor).

(channel: *Write_Channel, data_bytes: s64) -> (success: bool, reserved: Reserved_Message)

Claims room for a message of data_bytes bytes and returns (success, reserved); write the payload into reserved.user_write_pointer, then call writer_commit_message. Nothing is visible to the reader until the commit.

(channel: *Write_Channel, message: Reserved_Message) -> success: bool

Publishes a reserved message: from this call on the reader can find it. Returns false if the reservation is not valid any more (it was not made by this writer on this channel, the record was changed, or the reader has disconnected), and the message is then not published.

(channel: *Read_Channel) -> (found: bool, fatal_error: bool, message: Read_Message)

Looks for the next message without waiting and returns (found, fatal_error, message).

name := tprint("jai-doc-%", to_nanoseconds(current_time_monotonic()));

ok, reader := reader_create_channel(name, 1024);
assert(ok);
connected, writer := writer_connect(name);
assert(connected);

// The writer reserves space, fills it in and commits it.
reserved_ok, reserved := writer_reserve_message(*writer, 5);
assert(reserved_ok);
memcpy(reserved.user_write_pointer, "hello".data, 5);
assert(writer_commit_message(*writer, reserved));

// The reader finds it, reads it in place and gives the space back.
found, fatal, message := reader_poll_for_message(*reader);
assert(found && !fatal);
assert(to_string(message.user_read_pointer, message.user_read_bytes) == "hello");
assert(reader_done_with_message(*message));

found, fatal, message = reader_poll_for_message(*reader);
assert(!found && !fatal); // empty again

assert(writer_disconnect(*writer));
assert(reader_release_channel(*reader));

Doesn't run in the browser (skipped on wasm).

(message: *Read_Message) -> bool

Marks the message as read and frees its space for the writer; the payload must not be used afterwards. Messages have to be finished in order, so this works on the message reader_poll_for_message returned last. Returns false, and frees nothing, for a message that was already finished (the record is cleared on success) or whose bookkeeping does not match the channel.

Channel POSIX#

(name: string, bytes: s64) -> (success: bool, channel: Read_Channel)

The platform half of reader_create_channel, which calls it: creates the shared-memory object (shm_open with O_EXCL on Unix, a file mapping on Windows), maps it and writes the header. Call reader_create_channel instead.

(name: string, name_not_found_is_quiet := false) -> (success: bool, channel: Write_Channel)

Connects to the channel called name as its writer and returns (success, channel). Give the channel back with writer_disconnect.

name := tprint("jai-doc-%", to_nanoseconds(current_time_monotonic()));

missing, nothing := writer_connect(name, name_not_found_is_quiet = true);
assert(!missing); // no reader yet

ok, reader := reader_create_channel(name, 512);
assert(ok);
connected, writer := writer_connect(name);
assert(connected);

second, other := writer_connect(name, name_not_found_is_quiet = true);
assert(!second); // one writer at a time

assert(writer_disconnect(*writer));
connected, writer = writer_connect(name);
assert(connected);
assert(writer_disconnect(*writer));
assert(reader_release_channel(*reader));

Doesn't run in the browser (skipped on wasm).

(channel: *Write_Channel) -> bool

Disconnects the writer: the channel marks the writer as gone, the mapping is closed and channel.header is cleared. Messages already committed stay readable by the reader. A new writer may then connect. Returns false for a writer that is not connected (including a second call) or if the system reports an error while unmapping or closing.

(channel: *Read_Channel) -> bool

Closes the reading end and removes the channel's name, so no writer can connect afterwards; a connected writer sees the reader as gone and its reserves fail. The mapping is closed and channel.header is cleared. Unread messages are lost. Returns false for a channel that is not open or if the system reports an error. Always call it, or the name stays taken on Unix targets until the system is restarted.

Types#

:: struct

The second record of the region, at a fixed offset after the Header; it holds the channel marker the other end checks.

:: struct

What the reader keeps for the operating system: the file descriptor of the shared-memory object on Unix targets, the mapping handle on Windows. Only reader_release_channel should close it.

:: struct

What the writer keeps for the operating system: the file descriptor of the shared-memory object on Unix targets, the mapping handle on Windows. Only writer_disconnect should close it.

:: struct

The reading end of a channel, from reader_create_channel. It points at the shared region through header. Give it back with reader_release_channel; the pointer is not valid after that. Keep the record where it is while in use, and do not copy it for a second reader: a channel has exactly one reader.

:: struct

The writing end of a channel, from writer_connect. It points at the shared region through header. Give it back with writer_disconnect. A channel has exactly one writer at a time.

:: struct

The two cursors of the message ring, in bytes counted from the start of the channel's life: read_cursor is how much the reader has finished with and write_cursor how much the writer has committed. They only grow; the position in the ring is the cursor modulo data_size_in_bytes, and write_cursor - read_cursor is what is waiting. They sit in different cache lines. Get the record with get_message_queue_header.

:: struct

Space in the ring that the writer has claimed with writer_reserve_message and not committed yet.

:: struct

A message the reader has found with reader_poll_for_message: user_read_pointer and user_read_bytes give its payload, which is read in place in the shared region. Pass it to reader_done_with_message when finished, which frees the space for the writer; the payload must not be used afterwards. Messages arrive in the order they were committed.

Constants#