flan/spike/embed/merged_main.c
Joseph Ferano 2e9b29e549 One binary that is both a Flan program and the compiler that built it
The first six probes each proved a piece. merged.sh puts them together: the
program's @main is renamed out of the way, a C main takes the main thread and
runs it there, caml_startup happens on a thread beside it, and clang links the
lot -- the emitted program object, flan_rt.c, flan_dev.c, flan_agent.c and the
whole compiler as one -output-complete-obj. It runs, and the compiler inside it
compiles the very source the program was built from.

Nothing is wired up. The two halves share an address space and do not speak.
That is the point: the question was whether they can, not what they would say.

sig.sh and symbols.sh answer the two questions the first pass got wrong or
skipped. The SIGSEGV reading in harness5.c was taken at the wrong moment --
OCaml 5 starts domains after caml_startup returns, so the disposition had to be
read from inside the runtime, and against a plain ocamlopt executable as a
control. symbols.sh is the hazard nobody looks for until the link fails: four
.c files that are compiled into two different processes today, and the OCaml
runtime, all landing in one link.
2026-09-12 20:32:51 +07:00

78 lines
2.6 KiB
C

/* One process: the Flan program on the main thread, the OCaml compiler beside
* it on a domain of its own.
*
* This is the shape item 11 settles on, and the reason it is written this way
* round rather than the other: on macOS the window has to be on the main
* thread, so the game keeps main() and the compiler moves to the side --
* beside the listener vendor/agent/flan_agent.c already starts there.
*
* The program and the compiler do not talk to each other here. Wiring them up
* is the real work; this only shows they can share an address space, a link,
* and a process, with clang doing the final link. */
#include <caml/callback.h>
#include <caml/alloc.h>
#include <caml/mlvalues.h>
#include <caml/threads.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
/* The Flan program's entry point, renamed out of main's way by merged.sh. */
extern int flan_program_main(int argc, char **argv);
static char **g_argv;
static const char *g_src;
/* The Flan program's main calls exit(), so the compiler has to be up before it
* starts -- which is the honest ordering anyway: the image comes up and serves,
* then the program runs, the way starting an SBCL image does. */
static atomic_int compiler_ready = 0;
static double ms_since(struct timespec a) {
struct timespec b;
clock_gettime(CLOCK_MONOTONIC, &b);
return (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6;
}
static void *compiler_side(void *unused) {
struct timespec t0;
const value *f;
(void)unused;
clock_gettime(CLOCK_MONOTONIC, &t0);
caml_startup(g_argv);
printf("[compiler] up on a side thread in %.3f ms\n", ms_since(t0));
f = caml_named_value("spike_thread_compile");
if (f) {
clock_gettime(CLOCK_MONOTONIC, &t0);
printf("[compiler] %s\n", String_val(caml_callback(*f, caml_copy_string(g_src))));
printf("[compiler] compiled the running program from inside it, in %.3f ms\n",
ms_since(t0));
}
caml_release_runtime_system();
atomic_store(&compiler_ready, 1);
return NULL;
}
int main(int argc, char **argv) {
pthread_t comp;
int rc;
g_argv = argv;
g_src = argc > 1 ? argv[1] : "test/programs/edn.flan";
if (pthread_create(&comp, NULL, compiler_side, NULL) != 0) return 1;
while (!atomic_load(&compiler_ready)) {
struct timespec t = { 0, 2000000L };
nanosleep(&t, NULL);
}
/* The main thread is the program's, and it never enters OCaml. */
printf("[program] running on the main thread\n");
rc = flan_program_main(argc, argv);
printf("[program] returned %d\n", rc);
pthread_join(comp, NULL);
printf("one process: a Flan program and the OCaml compiler, same binary\n");
return 0;
}