/* reload_host.c — redefinition, exercised in one process. * * This is the smallest thing that can prove the dev loop's first two steps: * a function recompiled into its own object, loaded into a program that is * already running, *installed* there, and then reached by a call site that * was compiled before it existed. No socket, no daemon, no frame boundary — * those are step 3, and the agent that does them lives next to flan_rt.c for * the same reason this host is C: there is no OCaml in a game process. * * It stands in for the entry point of a Flan program, so the .flan fixture it * links against has no [main] of its own. Three things are being checked, and * only a single process can check any of them: * * - installing a new body makes the host's own [outer] — linked once, never * rebuilt — call it, which is the whole of C-c C-c; * - the loaded copy writes the *host's* [counter] and calls the host's * [helper], because a redefinition module declares both rather than * defining them; * - the state carries across every reload untouched; * - a name the host was never built with — v3's [extra] and [added] — can be * introduced, and then itself redefined by v4 while v3's already-installed * [bump] keeps calling it. That last one is what separates a cell found by * name from a function address cached by name; everything else passes * either way. * * Each version is its own file rather than one path rewritten in place: * dlopen keys its cache on the path, so re-opening the same name can hand back * the handle it already has and the test would then "pass" on the code it * loaded the first time. Nothing is ever dlclosed — a cell holds an address * inside a module's text, and unloading it would leave call sites pointing at * unmapped memory. */ #include #include #include #include /* The Flan symbols the executable itself defines. Flan names contain * characters C identifiers cannot, so each one is reached through its asm * label — the same name Emit spells. */ /* The trailing ptr is the transfer channel spec-conditions.md §6 puts in every * Flan signature. This host never transfers, so it passes a slot of its own * that stays null — but the parameter is not optional: getting it wrong reads * garbage as the channel and fails nowhere near here. */ extern int64_t flan_outer(void *xfer) __asm__("flan.outer"); extern int64_t flan_counter __asm__("flan.counter"); void flan_rt_init(int32_t argc, char **argv); /* What a redefinition module exposes. It is a named function and not an ELF * constructor on purpose: the agent has to choose when the store happens — * on the game thread, between frames — and a constructor would do it during * dlopen, wherever that call happened to be. */ typedef void (*install_fn)(void); /* The load is timed here rather than from the test process, because this is * the part that has to fit inside a frame. */ static double now_ms(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return (double)t.tv_sec * 1e3 + (double)t.tv_nsec / 1e6; } static int install(const char *path) { double t0 = now_ms(); void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL); if (h == NULL) { fprintf(stderr, "dlopen %s: %s\n", path, dlerror()); return 0; } install_fn f = (install_fn)(uintptr_t)dlsym(h, "flan_reload_install"); if (f == NULL) { fprintf(stderr, "dlsym flan_reload_install in %s: %s\n", path, dlerror()); return 0; } double t1 = now_ms(); f(); fprintf(stderr, "dlopen+dlsym %.2fms install %.4fms\n", t1 - t0, now_ms() - t1); return 1; } int main(int argc, char **argv) { flan_rt_init(argc, argv); if (argc < 2) { fprintf(stderr, "usage: %s ...\n", argv[0]); return 2; } void *xfer = NULL; printf("host %lld\n", (long long)flan_outer(&xfer)); for (int i = 1; i < argc; i++) { if (!install(argv[i])) return 1; printf("after%d %lld\n", i, (long long)flan_outer(&xfer)); } printf("counter %lld\n", (long long)flan_counter); return 0; }