Because the C daemon processes ring buffer slots sequentially, the dlopen
state machine is naturally thread-safe. By the time the daemon reads a SWAP_SO
opcode, any previous matrix operations have already finished. This eliminates
the need for complex Read-Copy-Update (RCU) locks.
To keep the execution loop as fast as possible, you must avoid calling dlsym()
(which performs string-based symbol lookups) on every tick. Instead, the
daemon maintains a lightweight registry that caches the resolved function
pointers.
The Module Registry
The C daemon tracks loaded shared objects in a static array. Each module
defines a standardized entry pointfor example, compute_tick.
#include <dlfcn.h> #include <string.h> #include <stdio.h> #include <stdint.h>
#include <stdatomic.h> #define MAX_MODULES 16 typedef struct { char name[32];
void* handle; // Cached function pointer matching your required signature void
(*compute_tick)(void* grid_memory, uint32_t offset, uint32_t size); }
compute_module_t; compute_module_t registry[MAX_MODULES];
The Hot-Swap Routine
When the LPC VM requests a hot-swap, the Node.js orchestrator writes the
OP_SWAP_SO opcode into the ring buffer. The C daemon reads this and triggers
the reload.
void hot_swap_module(const char* target_name, const char* new_so_path) {
compute_module_t* mod = find_module_by_name(target_name); // Helper to search
registry if (!mod) { mod = allocate_module_slot(target_name); // Helper to
claim an empty slot } else if (mod->handle) { dlclose(mod->handle); // Safely
drop the old binary from memory } // Load the newly compiled shared object //
RTLD_NOW guarantees immediate symbol resolution (fails fast if there's an
error) // RTLD_LOCAL prevents symbol namespace collisions between different
.so files mod->handle = dlopen(new_so_path, RTLD_NOW | RTLD_LOCAL); if
(mod->handle) { // Cache the function pointer so the hot-loop doesn't pay
lookup costs mod->compute_tick = dlsym(mod->handle, "compute_tick");
printf("[Compute] Successfully swapped %s
", target_name); } else { fprintf(stderr, "[Compute] FATAL: dlopen failed: %s
", dlerror()); mod->compute_tick = NULL; } }
The Lock-Free Consumer Loop
The daemon operates in a tight, CPU-bound polling loop. It checks the tail
pointer, executes the opcode directly against the mmap data plane, and resets
the slot.
void process_ring_buffer(ring_buffer_t* rb, void* data_plane) { uint32_t tail
= atomic_load(&rb->tail); ipc_slot_t* slot = &rb->slots[tail % 256]; if
(atomic_load(&slot->state) == SLOT_READY) { // Lock the slot to prevent Node
from overwriting it during execution atomic_store(&slot->state, SLOT_READING);
if (slot->opcode == OP_EXEC) { compute_module_t* mod = find_module_by_name(slot
->target_so); if (mod && mod->compute_tick) { // Execute directly on the
shared memory grid mod->compute_tick(data_plane, slot->grid_offset,
slot->grid_size); } } else if (slot->opcode == OP_SWAP_SO) { hot_swap_module(sl
ot->target_so, slot->new_so_path); } // Execution complete. Unlock the slot
and advance the tail pointer. atomic_store(&slot->state, SLOT_EMPTY);
atomic_fetch_add(&rb->tail, 1); } }
Architectural Safeguards
Strict Statelessness: The .so files must declare zero static or global
variables. Every byte of state they mutate must live in the data_plane pointer
passed to them. If a .so holds internal state, dlclose() will instantly
destroy it.
Immediate Error Surfacing: Using RTLD_NOW instead of RTLD_LAZY is critical. If
your C compiler outputs a .so with a typo (e.g., calling an undefined math
function), RTLD_NOW catches it at load time, gracefully rejecting the swap.
RTLD_LAZY would accept the load and abruptly segmentation-fault the daemon the
moment the function was called.
Ghost Modules: Because dlopen holds a reference count, the Linux kernel keeps
the physical memory mapped even if the .so file on disk is deleted or
overwritten by your compiler. You can safely compile directly over the .so
file while the daemon is actively running the old version in memory.