the mem_write c memory primitive

2023-apr-20

It was when I was writing code for hot reloading that I was dealing with path concatenation. I didn't get it right the first time. There was lots of calls to `mem_copy` (just `memcpy` that uses `u64` instead of `size_t`) and it was kinda error prone one could say.

There had to be a better way.

static b32
try_reload_app_data(struct AppData* app, const WCHAR* current_dir_path, u32 current_dir_path_len) {
	const WCHAR app_lib_path[] = L"build/gamelib.dll";
	const WCHAR loaded_suffix[] = L".loaded";

	WCHAR path_buf[MAX_PATH] = {0};
	ASSERT(current_dir_path_len + STR_LEN(app_lib_path) + STR_LEN(loaded_suffix) < LEN(path_buf));

	u32 current_dir_path_size = current_dir_path_len * sizeof(current_dir_path[0]);

	// we first copy current_dir_path into path_buf
	mem_copy(
		path_buf,
		current_dir_path,
		current_dir_path_size
	);
	// then concatenate app_lib_path to it
	mem_copy(
		(char*)path_buf + current_dir_path_size,
		app_lib_path,
		sizeof(app_lib_path)
	);
	// given that current_dir_path contains "my_project_path",
	// path_buf should contain "my_project_path/build/gamelib.dll"

	// then copy it all to loaded_path_buf
	WCHAR loaded_path_buf[LEN(path_buf)] = {0};
	mem_copy(
		loaded_path_buf,
		path_buf,
		current_dir_path_size + sizeof(app_lib_path) - sizeof(L'\0')
	);
	// then concat loaded_suffix to it
	mem_copy(
		(char*)loaded_path_buf + current_dir_path_size + sizeof(app_lib_path) - sizeof(L'\0'),
		loaded_suffix,
		sizeof(loaded_suffix),
	);
	// loaded_path_buf should contain "my_project_path/build/gamelib.dll.loaded"

	// rest of code goes here
	// what it does is copy file at path_buf to loaded_path_buf and then dynamically load that dll
	// done this way so we're free to overwrite the dll at path_buf with a new version for hot reloading
}

Notice the calls to `mem_copy`. They contain lots size and pointer math. Easy to make a mistake (I did).

First, it would be better to pass in the dst buffer and its end instead of its size as the end does not move as we write into it.

void
mem_write(void* restrict dst_start, const void* dst_end, const void* restrict src, u64 src_size) {
	void* result = (char*)dst_start + src_size;
	ASSERT(result <= dst_end);
	mem_copy(dst_start, src, src_size);
}

I've come up with the name `mem_write` since it looks like one of those stream write functions but for memory.

At first this does not change much as the only change was to add a parameter (`dst_end`). At least we can assert that we have space in dst buffer. One further improvement which helps us writing to the correct part of dst buffer, is to keep a separate "cursor" which points the next "free" space in dst buffer. Like so:

static b32
try_reload_app_data(struct AppData* app, const WCHAR* current_dir_path, u32 current_dir_path_len) {
	const WCHAR app_lib_path[] = L"build/gamelib.dll";
	const WCHAR loaded_suffix[] = L".loaded";

	WCHAR path_buf[MAX_PATH] = {0};
	char* path_buf_at = (char*)path_buf;

	u32 current_dir_path_size = current_dir_path_len * sizeof(current_dir_path[0]);

	mem_write(
		path_buf_at,
		END(path_buf),
		current_dir_path,
		current_dir_path_size,
	);
	path_buf_at += current_dir_path_size;
	mem_write(
		path_buf_at,
		END(path_buf),
		app_lib_path,
		sizeof(app_lib_path)
	);
	path_buf_at += sizeof(app_lib_path);

	WCHAR loaded_path_buf[LEN(path_buf)] = {0};
	char* loaded_path_buf_at = (char*)loaded_path_buf;
	mem_write(
		loaded_path_buf_at,
		END(loaded_path_buf),
		path_buf,
		current_dir_path_size + sizeof(app_lib_path) - sizeof(L'\0')
	);
	loaded_path_buf_at += current_dir_path_size + sizeof(app_lib_path) - sizeof(L'\0');
	mem_write(
		loaded_path_buf_at,
		END(loaded_path_buf),
		loaded_suffix,
		sizeof(loaded_suffix)
	);
	loaded_path_buf_at += loaded_path_buf_at;

	// rest of code here...
}

These macros help us getting a pointer to the end of the array:

define LEN(array) (sizeof(array) / sizeof((array)[0]))
define END(array) ((array) + LEN(array))

However, still lots of duplicate code and lots of pointer math.

A small but helpful change would be to return where the "cursor" ends up from `mem_write`:

void*
mem_write(void* restrict dst_start, const void* dst_end, const void* restrict src, u64 src_size) {
	void* result = (char*)dst_start + src_size;
	ASSERT(result <= dst_end);
	mem_copy(dst_start, src, src_size);
	return result;
}
static b32
try_reload_app_data(struct AppData* app, const WCHAR* current_dir_path, u32 current_dir_path_len) {
	const WCHAR app_lib_path[] = L"build/gamelib.dll";
	const WCHAR loaded_suffix[] = L".loaded";

	WCHAR path_buf[MAX_PATH] = {0};
	WCHAR* path_buf_at = path_buf; // cursor does not need to be of type char* anymore

	u32 current_dir_path_size = current_dir_path_len * sizeof(current_dir_path[0]);

	path_buf_at = mem_write(
		path_buf_at,
		END(path_buf),
		current_dir_path,
		current_dir_path_size
	);
	path_buf_at = mem_write(
		path_buf_at,
		END(path_buf),
		app_lib_path,
		sizeof(app_lib_path)
	);

	WCHAR loaded_path_buf[LEN(path_buf)] = {0};
	WCHAR* loaded_path_buf_at = loaded_path_buf; // cursor does not need to be of type char* anymore
	loaded_path_buf_at = mem_write(
		loaded_path_buf_at,
		END(loaded_path_buf),
		path_buf,
		current_dir_path_size + sizeof(app_lib_path) - sizeof(L'\0')
	);
	loaded_path_buf_at = mem_write(
		loaded_path_buf_at,
		END(loaded_path_buf),
		loaded_suffix,
		sizeof(loaded_suffix)
	);

	// rest of code here...
}

Now, to ease passing `src_size` and `src`, we can make these two macros:

// takes a length a start ptr and returns a size (in bytes) and same start ptr
define SLICE_MEM(ptr, len) (ptr), ((len) * sizeof((ptr)[0]))
// takes a start and end ptr and returns a size (in bytes) and start ptr
define RANGE_MEM(start, end) (start), ((u64)((end) - (start)) * sizeof((start)[0]))

These macros let us compose memories of different shape with our `mem_write` like this:

static b32
try_reload_app_data(struct AppData* app, const WCHAR* current_dir_path, u32 current_dir_path_len) {
	const WCHAR app_lib_path[] = L"build/gamelib.dll";
	const WCHAR loaded_suffix[] = L".loaded";

	WCHAR path_buf[MAX_PATH] = {0};
	WCHAR* path_buf_at = path_buf;

	// NOTE: no more `current_dir_path_size`

	path_buf_at = mem_write(
		path_buf_at,
		END(path_buf),
		SLICE_MEM(current_dir_path, current_dir_path_len)
	);
	path_buf_at = mem_write(
		path_buf_at,
		END(path_buf),
		app_lib_path,
		sizeof(app_lib_path)
	);

	WCHAR loaded_path_buf[LEN(path_buf)] = {0};
	WCHAR* loaded_path_buf_at = loaded_path_buf;
	loaded_path_buf_at = mem_write(
		loaded_path_buf_at,
		END(loaded_path_buf),
		RANGE_MEM(path_buf, path_buf_at - 1) // `-1` easier than `- sizeof(L'\0')` to express no trailing L'\0'
	);
	loaded_path_buf_at = mem_write(
		loaded_path_buf_at,
		END(loaded_path_buf),
		loaded_suffix,
		sizeof(loaded_suffix)
	);

	// rest of code here...
}

We have two `_MEM` macros because sometimes it's easier to express a memory range as length + ptr, and sometimes a start + end ptr is easier.

Note how now we can use `path_buf_at - 1` inside `RANGE_MEM` to copy the contents of path_buf without the trailing `L'\0'`.

Now, we could have made the prototype of `mem_write` be

void* mem_write(void* restrict dst_start, const void* dst_end, const void* restrict src_start, const void* src_end)

This was my first design of `mem_write` but i've ended up not doing that because I could not figure a way to pass a literal directly without first creating a local buf (like `loaded_suffix`).

NOTE: It's not possible to create a macro: `#define STR_RANGE(str) (str), ((str) + sizeof(str) / sizeof((str)[0]))`. It would expand a `str` literal twice which compilers do not guarantee that would point to the same location (string pooling). And in fact msvc (at least in `/Od` builds) creates multiple `str` literals in this case.

With this design we can create one more macro and enjoy maximum compression:

// takes an array and returns the ptr to its start and its size (in bytes)
define MEM(array) &(array), sizeof(array)
static b32
try_reload_app_data(struct AppData* app, const WCHAR* current_dir_path, u32 current_dir_path_len) {
	const WCHAR app_lib_path[] = L"build/gamelib.dll";

	WCHAR path_buf[MAX_PATH] = {0};
	WCHAR* path_buf_at = path_buf;
	path_buf_at = mem_write(path_buf_at, END(path_buf), SLICE_MEM(current_dir_path, current_dir_path_len));
	path_buf_at = mem_write(path_buf_at, END(path_buf), MEM(app_lib_path)); // it works on local bufs

	WCHAR loaded_path_buf[LEN(path_buf)] = {0};
	WCHAR* loaded_path_buf_at = loaded_path_buf;
	loaded_path_buf_at = mem_write(loaded_path_buf_at, END(loaded_path_buf), RANGE_MEM(path_buf, path_buf_at - 1));
	loaded_path_buf_at = mem_write(loaded_path_buf_at, END(loaded_path_buf), MEM(L".loaded")); // it works on literals too!

	// rest of code...
}

As a nice bonus, it's also possible to use the `MEM` macro with other `mem_` functions like `void* mem_zero(void* ptr, u64 size)` which is equivalent to `memset(ptr, 0, size)`.

What was previously:

mem_zero(platform->renderer_command_buffers, sizeof(platform->renderer_command_buffers));
mem_zero(MEM(platform->renderer_command_buffers));

It works even with variables which are not array:

// overlapped is a windows OVERLAPPED struct
mem_zero(MEM(dir_watcher->overlapped));
This is why I wrote `MEM` as `#define MEM(array) &(array), sizeof(array)` and not `#define MEM(array) (array), sizeof(array)` (there's no `&` in the second version).

As you can see, even with C's limitations, it's possible to achieve nice expressiveness when we have the right (composable) primitives.

A little caveat to keep in mind though: `MEM` will include the trailing `\0` when used with string literals . In this example I did want to copy it into the buffer. But for other cases, i've also defined these macros which exclude the `\0`:

define STR_LEN(str) (LEN(str) - 1)
define STR_END(str) ((str) + STR_LEN(str))
define STR_MEM(str) &(str), (sizeof(str) - sizeof((str)[0]))

You may argue that this is a lot of macros for your taste and I would even agree. However i'm still experimenting with this way of writing buf filling code and so far it seems promising.

This post was inspired by this semantic compression blog post:

semantic compression blog post