mcap/modules/Shared_Memory_Channel/module.jai

389 lines
15 KiB
Plaintext

// Audit Header_Os_Specific.
// User-level protocol info.
// Handle wrapping.
// Assert that we don't read another message without being done with the previous.
// Assert that we are done reading or writing when destroying channels.
// Make the documentation below more accurate:
// This is a module for communicating quickly, with other processes on the same machine,
// using shared memory.
// The reader calls reader_create_channel to initialize,
// reader_release_channel to deinitialize that buffer.
// The writer calls writer_connect. If that succeeds,
// the writer can call writer_get_packet_pointer() to get the position into which
// we can write, and then writer_send_packet() to actually send.
// Document: Circular buffer is big relative to message size, so messages do not wrap.
// Only one thread at once to write or read.
// On Linux, we can't be O_EXCL so we hose ourselves if you run us twice? wtf? How do we get the fd to be tied to the process lifetime?
// Keep trying to reconnect if you start the game before the visualizer.
#if OS == .WINDOWS {
#load "windows.jai";
} else #if (OS == .LINUX) || (OS == .MACOS) {
#load "posix.jai";
}
Header :: struct {
magic : u32;
version : u32;
reader_status: enum s32 {
DISCONNECTED :: 0;
PAUSED :: 1;
ACTIVELY_READING :: 2;
};
writer_status: enum s32 {
DISCONNECTED :: 0;
PAUSED :: 1;
ACTIVELY_WRITING :: 2;
};
offset_to_os_specific_header: u32; // This is the offset from the start of the header.
offset_to_message_queue_header: u32; // This is the offset from the start of the header.
offset_to_message_data: u32; // This is the offset from the start of the header.
full_memory_region_size_in_bytes: s64; // The size of the full memory region, from the start of the header to the end of contiguous memory.
data_size_in_bytes: s64; // The size of the data area (a.k.a. sum of all message sizes that can be queued at once).
}
Read_Channel :: struct {
header: *Header;
os_specific: Read_Channel_Os_Specific;
}
Write_Channel :: struct {
header: *Header;
os_specific: Write_Channel_Os_Specific;
}
Message_Queue_Header :: struct {
read_cursor: s64; // Offset of the next data to read from the start of the Header (not this Message_Queue_Header).
pad: [56] u8 = ---; // Give us at least 1 cache line of padding.
write_cursor: s64; // Offset of the next data to write from the start of the Header (not this Message_Queue_Header).
}
Reserved_Message :: struct {
channel: *Write_Channel;
offset: s64;
total_bytes: s64; // This includes message overhead.
user_write_pointer: *u8; // Write your mesasge here.
}
Read_Message :: struct {
channel: *Read_Channel;
offset: s64;
total_bytes: s64;
user_read_pointer: *u8; // Can we not just make this a [] *u8?
user_read_bytes: s64;
}
OFFSET_TO_HEADER_OS_SPECIFIC :: 64; // Probably 1 or 2 cache lines?
OFFSET_TO_MESSAGE_QUEUE_HEADER :: 128;
OFFSET_TO_MESSAGE_DATA :: 256;
PER_MESSAGE_OVERHEAD_SIZE :: 16; // 8-byte per-message magic stamp, 8-byte length. The magic stamp helps us with debugging.
// All our length stamps are non-negative numbers, so our wrapping sentinel being negative
// means it will not be mistaken for a length. However, just being boring and writing -1 is not good,
// since that is a fill pattern others might use. So we make a negative number that is unlikely
// to be used elsewhere.
WRAPPING_SENTINEL :: 0xcafebabef00dface; // Reader, if you see this in the length slot, wrap to the start of the buffer.. Must be negative!
#assert size_of(Header) < OFFSET_TO_HEADER_OS_SPECIFIC;
#assert size_of(Header_Os_Specific) + OFFSET_TO_HEADER_OS_SPECIFIC < OFFSET_TO_MESSAGE_QUEUE_HEADER;
// already_exists means another reader owns the name; its live header is left untouched.
reader_create_channel :: (name: string, bytes: s64) -> (success: bool, channel: Read_Channel, already_exists: bool = false) {
success, channel, already_exists := reader_create_channel_os_specific(name, bytes);
if !success return false, .{}, already_exists;
header := channel.header;
Basic.Initialize(header);
data := cast(*u8) *channel.header;
header.magic = (.*) cast(*u32) CHANNEL_MAGIC.data; // @Endian
header.version = PROTOCOL_VERSION;
header.reader_status = .ACTIVELY_READING; // Maybe don't do this until the reader explicitly says so?
header.offset_to_os_specific_header = OFFSET_TO_HEADER_OS_SPECIFIC;
header.offset_to_message_queue_header = OFFSET_TO_MESSAGE_QUEUE_HEADER;
header.offset_to_message_data = OFFSET_TO_MESSAGE_DATA;
header.full_memory_region_size_in_bytes = bytes + OFFSET_TO_MESSAGE_DATA;
header.data_size_in_bytes = bytes;
mq := get_message_queue_header(header);
mq.read\_cursor = OFFSET_TO_MESSAGE_DATA;
mq.write_cursor = OFFSET_TO_MESSAGE_DATA;
return true, channel;
}
// Discards everything queued, for recovering once from a corrupted queue. A writer that is
// mid-commit can still land a partial message, which the next poll reports as fatal.
reader_reset_queue :: (channel: *Read_Channel) {
mq := get_message_queue_header(channel.header);
if !mq return;
cursor_store(*mq.read_cursor, channel.header.offset_to_message_data);
cursor_store(*mq.write_cursor, channel.header.offset_to_message_data);
}
get_message_queue_header :: inline (header: *Header) -> *Message_Queue_Header {
base := cast(*u8) header;
return cast(*Message_Queue_Header)(base + header.offset_to_message_queue_header);
}
// Each cursor publishes the message data behind it to the other process, which on ARM64 needs
// acquire and release ordering. compare_and_swap is the atomic documented as acquire-release on
// every target, so both go through it.
cursor_load :: inline (cursor: *s64) -> s64 {
// No cursor is ever -1, so this always fails and only returns the current value.
_, value := compare_and_swap(cursor, -1, -1);
return value;
}
cursor_store :: inline (cursor: *s64, value: s64) {
while true {
current := cursor.*;
if compare_and_swap(cursor, current, value) break;
}
}
/*
get_data_area :: inline (header: *Header) -> *u8 {
base := cast(*u8) header;
return base + header.offset_to_message_data;
}
*/
//
// OS-independent writer stuff:
//
writer_reserve_message :: (channel: *Write_Channel, data_bytes: s64) -> (success: bool, reserved: Reserved_Message) {
// @Incomplete assert if a message is currently reserved!
// We assume our buffer is much bigger than the messages.
// If we cannot get space for a message without wrapping the message in the middle,
// we skip back to the beginning of the buffer. (This means the user level code
// on the reading side can just look at the memory without worrying about wrapping around a ring buffer,
// and without having to copy it out.)
total_bytes := data_bytes + PER_MESSAGE_OVERHEAD_SIZE;
header := channel.header;
arena_start := header.offset_to_message_data;
arena_end := arena_start + header.data_size_in_bytes;
mqh := get_message_queue_header(header);
if !mqh return false, .{};
target_position := 0; // 0 means unset. All valid offsets are > 0, so if we see that, we know we found a place for this message.
write_wrapping_sentinel_at := 0; // If this is not 0, we wrapped, so tell the reader that we wrapped.
write_cursor := mqh.write_cursor; // only this side writes it
read\_cursor := cursor_load(*mqh.read_cursor);
if write_cursor >= read_cursor {
// Test against the end of the buffer.
remaining := arena_end - write_cursor;
if remaining >= total_bytes {
target_position = write_cursor;
} else {
end := header.offset_to_message_data + header.data_size_in_bytes;
if end - write_cursor >= PER_MESSAGE_OVERHEAD_SIZE {
// If there are fewer bytes left than this, the reader will just know to wrap.
write_wrapping_sentinel_at = write_cursor;
}
write_cursor = header.offset_to_message_data;
if total_bytes >= header.data_size_in_bytes - 1 return false, .{}; // No way will this message fit. -1 because we cannot meet the read cursor.
}
}
if !target_position {
// Test for space from the beginning of the buffer to the read cursor.
assert(write_cursor <= read_cursor); // The reader could catch up to us right when we are wrapping.
remaining := read_cursor - write_cursor - 1; // -1 so we cannot meet the read cursor from the bottom.
if remaining >= total_bytes {
target_position = header.offset_to_message_data;
} else {
return false, .{}; // We were unable to find a place for the message!
}
}
if write_wrapping_sentinel_at {
assert(write_wrapping_sentinel_at > header.offset_to_message_data); // Writing this to the first buffer position is an error, as it can cause the reader to loop without making progress, and anyway there would be no way to fit a message by wrapping if you were already at the beginning!.
dest := (cast(*u8) header) + write_wrapping_sentinel_at;
(.*)cast(*u64)dest = WRAPPING_SENTINEL;
}
assert(target_position > 0);
data_start := cast(*u8) header;
result: Reserved_Message;
result.offset = write_cursor;
result.total_bytes = total_bytes;
result.channel = channel;
result.user_write_pointer = data_start + write_cursor + PER_MESSAGE_OVERHEAD_SIZE;
return true, result;
}
writer_commit_message :: (channel: *Write_Channel, message: Reserved_Message) -> success: bool {
assert(message.channel == channel);
if message.channel != channel return false;
mqh := get_message_queue_header(channel.header);
if !mqh return false;
data := cast(*u8) channel.header;
magic_pointer := cast(*u64) (data + message.offset);
length_pointer := cast(*s64) (data + message.offset + 8);
assert(data + message.offset + 16 == message.user_write_pointer);
magic_pointer.* = (.*)cast(*u64) MESSAGE_MAGIC.data;
length_pointer.* = message.total_bytes;
cursor_store(*mqh.write_cursor, message.offset + message.total_bytes);
return true;
}
reader_poll_for_message :: (channel: *Read_Channel) -> (found: bool, fatal_error: bool, message: Read_Message) {
header := channel.header;
mqh := get_message_queue_header(header);
if !mqh return false, true, .{};
read_cursor := mqh.read_cursor; // only this side writes it
if read_cursor == cursor_load(*mqh.write_cursor) {
return false, false, .{};
}
data := cast(*u8) header;
magic_pointer := cast(*u64) (data + read_cursor);
length_pointer := cast(*s64) (data + read_cursor + 8);
desired_magic := (.*) cast(*u64) MESSAGE_MAGIC.data;
magic := magic_pointer.*;
if magic != desired_magic {
if magic == WRAPPING_SENTINEL {
// It's time to wrap!
// I hope we are not at the beginning of the buffer though!
if read_cursor == header.offset_to_message_data {
Basic.log_error("In reader_poll_for_message(), we got a WRAPPING_SENTINEL at the start of the buffer, which is invalid. We must stop.\n");
return false, true, .{};
}
cursor_store(*mqh.read_cursor, header.offset_to_message_data);
found, fatal_error, message := #this(channel);
return found, fatal_error, message;
}
Basic.log_error("Invalid magic number 0x% at offset % in reader_poll_for_message(): we got a parse error and must stop.\n", Basic.formatInt(magic, base=16), read_cursor);
return false, true, .{};
}
length := length_pointer.*;
if length < PER_MESSAGE_OVERHEAD_SIZE {
Basic.log_error("In reader_poll_for_message(), we got a length of %, which is invalid. Failing.\n", length);
return false, true, .{};
}
end := header.offset_to_message_data + header.data_size_in_bytes;
remaining := end - read_cursor;
if remaining < length {
Basic.log_error("In reader_poll_for_message(), we got a length of %, which is too long (only % bytes are left in the buffer). Failing.\n", length, remaining);
return false, true, .{};
}
message: Read_Message;
message.channel = channel;
message.offset = read_cursor;
message.total_bytes = length;
message.user_read_pointer = data + read_cursor + PER_MESSAGE_OVERHEAD_SIZE;
message.user_read_bytes = length - PER_MESSAGE_OVERHEAD_SIZE;
return true, false, message;
}
reader_done_with_message :: (message: *Read_Message) -> bool {
channel := message.channel;
assert(channel != null);
header := channel.header;
mqh := get_message_queue_header(header);
if !mqh return false;
read_cursor := mqh.read_cursor;
read_cursor += message.total_bytes;
end := header.offset_to_message_data + header.data_size_in_bytes;
if end - read_cursor < PER_MESSAGE_OVERHEAD_SIZE {
// Wrap.
read_cursor = OFFSET_TO_MESSAGE_DATA;
}
cursor_store(*mqh.read_cursor, read_cursor);
return true;
}
PROTOCOL_VERSION :: 1;
CHANNEL_MAGIC :: "ShmC";
MESSAGE_MAGIC :: "ShmM2022";
#scope_module
get_size_of_full_mapping_from_stub_header :: (pointer: *void) -> (success: bool, actual_bytes: s64) {
// Ininitially, we don't know how big the memory region is, so we just map
// the header. Then we look in the header to find the size of the full region,
// then re-map it.
header_copy := (.*) cast(*Header) pointer;
desired_magic := (.*) cast(*u32) CHANNEL_MAGIC.data; // @Endian
success := true;
if header_copy.magic != desired_magic {
Basic.log_error("In writer_connect(), the magic number of the mapped header did not match. (Wanted 0x%, got 0x%).\n", Basic.formatInt(desired_magic, base=16), Basic.formatInt(header_copy.magic, base=16));
success = false;
} else if header_copy.version != PROTOCOL_VERSION {
Basic.log_error("In writer_connect(), the protocol version did not match. (Wanted %, got %).\n", PROTOCOL_VERSION, header_copy.version);
success = false;
}
actual_bytes := header_copy.full_memory_region_size_in_bytes;
if actual_bytes < OFFSET_TO_MESSAGE_DATA {
Basic.log_error("In writer_connect(), the full_memory_region_size_in_bytes we got from the header was %, which is too small.\n", actual_bytes);
success = false;
}
return success, actual_bytes;
}
Basic :: #import "Basic";
assert :: Basic.assert;