/* flan_dev — the part of the host ABI that only a dev build has. * * A redefinition module reaches the host's functions and globals through * symbols the host already exports: a cell for each function, the storage for * each global. That covers everything the program was *built* with. It does * not cover a name the module introduces — a defn or a defvar typed into the * REPL after the process started — because there is no symbol in the host to * bind to and ELF cannot grow one. * * So a name that is new at run time is keyed by string instead. This file is * the two lookups that make that work, and deliberately nothing else: * * flan_dev_cell(name) the cell a new function lives in * flan_dev_global(name, size, init) the storage a new global lives in * flan_dev_result(bytes, len) where an evaluated expression's rendering * goes, for the daemon to read back * * Both are idempotent: the second module to mention a name gets what the first * one got. That is the whole point. Two modules that each define their own * copy of a new function would each call their own, and redefining it would * update one of them. * * The table never moves. A module holds the address of a cell for as long as * it is loaded, so a growable table would leave those addresses pointing into * a freed allocation. Fixed capacity and a loud failure instead. * * Never dlclose a module. A cell holds an address inside that module's text, * and unloading it leaves every call site pointing at unmapped memory. There * is no unload path here on purpose. */ #include #include #include #include #define FLAN_DEV_MAX 4096 typedef struct { const char *name; /* strdup'd: the module that passed it may go away */ void *cell; /* a function's cell, or a global's storage */ size_t size; /* a global's size; 0 for a function */ } entry; static entry table[FLAN_DEV_MAX]; static size_t used; static void die(const char *what, const char *name) { fprintf(stderr, "flan_dev: %s: %s\n", what, name); fflush(stderr); abort(); } static entry *find(const char *name) { for (size_t i = 0; i < used; i++) if (strcmp(table[i].name, name) == 0) return &table[i]; return NULL; } static entry *intern(const char *name) { if (used == FLAN_DEV_MAX) die("out of dev name slots", name); entry *e = &table[used++]; e->name = strdup(name); if (e->name == NULL) die("out of memory", name); e->cell = NULL; e->size = 0; return e; } /* The cell a run-time-introduced function is called through. One indirection * more than a function the host was built with, whose cell is a symbol the * module can name directly — the compiler picks per name, so the common case * stays a single load. */ void **flan_dev_cell(const char *name) { entry *e = find(name); if (e == NULL) e = intern(name); return &e->cell; } /* Storage for a run-time-introduced global, allocated once. * * [init] is its declared initial value, or NULL for all-zero. It is copied on * the allocation and ignored on every call after it, which is where "a reload * must not reset the program's state" lives: the second module to mention this * name is a redefinition, and re-running an initialiser would throw away * exactly what the reload exists to preserve. Doing it here rather than by a * branch in the caller means the rule cannot be got wrong at one call site. * * A size mismatch is the layout-drift failure, caught at its first chance: the * running process has already laid this memory out, and handing back the old * allocation for a differently shaped type means the new body reads fields at * the wrong offsets and nothing ever says so. Retyping a var needs a restart. */ void *flan_dev_global(const char *name, uint64_t size, const void *init) { entry *e = find(name); if (e == NULL) { e = intern(name); e->cell = calloc(1, size ? (size_t)size : 1); if (e->cell == NULL) die("out of memory", name); e->size = (size_t)size; if (init != NULL && size > 0) memcpy(e->cell, init, (size_t)size); return e->cell; } if (e->size != (size_t)size) die("size changed; restart to retype", name); return e->cell; } /* ── The value of an evaluated expression ──────────────────────────── */ /* C-x C-e compiles a thunk that renders one expression and calls this with the * text. It is not written to stdout: stdout belongs to the program, it is in * the hot path for anything that prints, and a dev-only feature must not put a * branch in it. The daemon reads this back over the agent's socket instead. * * [generation] is what makes the read safe without a handshake. The thunk runs * on the game thread at a frame boundary, whenever that happens to be; the * daemon waits for the counter to move rather than guessing it has. */ #define RESULT_MAX 4096 static char result[RESULT_MAX]; static size_t result_len; static uint64_t generation; void flan_dev_result(const uint8_t *bytes, int64_t len) { size_t n = len < 0 ? 0 : (size_t)len; if (n > RESULT_MAX) n = RESULT_MAX; memcpy(result, bytes, n); result_len = n; /* Last, so a reader that sees the new generation sees the whole value. */ __atomic_store_n(&generation, generation + 1, __ATOMIC_RELEASE); } const char *flan_dev_result_get(uint64_t *gen, uint64_t *len) { *gen = __atomic_load_n(&generation, __ATOMIC_ACQUIRE); *len = (uint64_t)result_len; return result; }