Download Game! Currently 98 players. Last logged in:AirEckyLexelalexWhlxch

Blitzer's Blog >> 72862

Back to blogs index
Posted: 01 Oct 2026 18:02 [ permalink ]
A POSIX shared memory object (/dev/shm) provides the cleanest boundary between
the V8 engine and the C daemon. By backing a Node.js SharedArrayBuffer with a
file descriptor generated by shm_open(), both processes operate on the exact
same physical memory pages without kernel transitions.
Here is the memory layout mapping the control and data planes:
| Region | Offset | Size | Purpose |
|---|---|---|---|
| Control Header | 0x0000 | 64 Bytes | Atomic head and tail pointers
(cache-line aligned). |
| Ring Buffer | 0x0040 | ~16 KB | Fixed-size array of IPC message structs. |
| Padding | ... | Variable | Aligns the data plane to the next 4KB page
boundary. |
| Data Plane | 0x1000 | N MB | The Cellular Automata simulation grid. |
The C Memory Layout
Use <stdatomic.h> to define the structs. To prevent torn reads during
lock-free concurrency, each slot in the ring buffer requires its own atomic
state machine (EMPTY, WRITING, READY, READING).
#include <stdatomic.h>
#include <stdint.h>

#define SLOT_EMPTY   0
#define SLOT_WRITING 1
#define SLOT_READY   2
#define SLOT_READING 3

typedef struct {
    atomic_uint state;
    uint32_t opcode;        // 1 = EXEC, 2 = SWAP_SO
    char target_so[32];     // e.g., "thermo_v2"
    uint32_t grid_offset;
    uint32_t grid_size;
} ipc_slot_t;

typedef struct {
    atomic_uint head; // Node.js increments on write
    atomic_uint tail; // C daemon increments on read
    ipc_slot_t slots[256];
} ring_buffer_t;

The Node.js Boundary
Because standard V8 SharedArrayBuffer allocations are strictly process-bound,
you must bridge the boundary. The most robust method for an edge-capable
architecture is a tiny N-API C++ addon that opens the POSIX memory segment and
wraps it for V8:
/ N-API Wrapper concept
int fd = shm_open("/genesis_mem", O_RDWR, 0666);
void* mapped = mmap(NULL, TOTAL_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
0);

/ Expose the mapped POSIX memory to Node.js as a native SharedArrayBuffer
napi_value v8_sab;
napi_create_external_arraybuffer(env, mapped, TOTAL_SIZE, NULL, NULL,
&v8_sab);

Once exposed to JavaScript, you cast Int32Array views over the control headers
and use the native Atomics API (Atomics.load(), Atomics.store(), and
Atomics.compareExchange()) to manipulate the exact same head, tail, and state
bytes as the C daemon.
Lock-Free Execution Flow
 * Node.js (Producer): Reads head, verifies slots[head].state == SLOT_EMPTY
using Atomics.compareExchange to swap to SLOT_WRITING.
 * Node.js: Writes the .so target and offsets, then swaps state to SLOT_READY.
 * C Daemon (Consumer): Polls slots[tail]. If SLOT_READY, it swaps to
SLOT_READING.
 * C Daemon: Calls dlsym() to locate the function in the target .so, executes
the matrix transformation directly on the Data Plane (0x1000), and resets the
slot to SLOT_EMPTY.
Should we map out the dlopen state machine the C daemon uses to hot-swap .so
files while keeping the ring buffer consumer loop active?