/* Loading a compiled macro into the compiler's own process. * * NEXT.md's expander design: there is no interpreter, so running a macro means * compiling it and dlopening it. The reload primitive does exactly this * already, but its host is a running Flan program written in C; here the host * is the OCaml compiler, which has no dlopen of its own -- Dynlink loads * OCaml, not ELF. So the boundary needs stubs, and this is all of them. * * Two rules shape what is here: * * - Nothing but pointers and scalars crosses. A Flan `string`/slice is * {ptr,len} and a `Form` is {i32, [2 x i64]}, and LLVM's calling * convention for an aggregate passed or returned *by value* in hand-written * IR is not promised to be clang's C ABI for the equivalent struct. The * unions lane verified memory layout, so memory is the agreement we have: * every macro is reached through a thunk taking (ptr,i64,ptr,ptr) and * writing its result through the out pointer. * * - The macro module is self-contained: it links the runtime in and has no * undefined Flan symbols, so the OCaml executable needs no -rdynamic and * nothing in it has to be exported. * * The peek/poke family is how the marshaller writes a Form image into memory * the macro can read. OCaml cannot address raw memory, so the bytes are laid * out from here one field at a time. */ #include #include #include #include #include #include #include #include #include CAMLprim value flan_dl_open(value path) { CAMLparam1(path); void *h = dlopen(String_val(path), RTLD_NOW | RTLD_LOCAL); if (!h) caml_failwith(dlerror()); CAMLreturn(caml_copy_nativeint((intnat)h)); } CAMLprim value flan_dl_sym(value handle, value name) { CAMLparam2(handle, name); void *p = dlsym((void *)Nativeint_val(handle), String_val(name)); if (!p) caml_failwith(dlerror()); CAMLreturn(caml_copy_nativeint((intnat)p)); } CAMLprim value flan_dl_close(value handle) { dlclose((void *)Nativeint_val(handle)); return Val_unit; } /* The one call shape a macro is reached through. See the thunk Emit writes. */ typedef void (*flan_macro_fn)(void *args, int64_t n, void *out, void *xfer); CAMLprim value flan_macro_call(value fn, value args, value n, value out) { CAMLparam4(fn, args, n, out); /* The transfer channel every Flan signature carries (spec-conditions.md, section 6). A macro that signals a condition with nothing above it to handle it aborts inside the compiler, which is loud rather than silent; the channel still has to be a real, zeroed slot. */ int64_t xfer[4] = { 0, 0, 0, 0 }; ((flan_macro_fn)Nativeint_val(fn))((void *)Nativeint_val(args), Int64_val(n), (void *)Nativeint_val(out), xfer); CAMLreturn(Val_unit); } CAMLprim value flan_mem_alloc(value n) { CAMLparam1(n); /* Zeroed, because ZII is the language's rule and an unwritten Form field must read as the zero of its type rather than as whatever malloc had. */ void *p = calloc((size_t)Long_val(n), 1); if (!p) caml_failwith("out of memory laying out a macro's arguments"); CAMLreturn(caml_copy_nativeint((intnat)p)); } CAMLprim value flan_mem_free(value p) { free((void *)Nativeint_val(p)); return Val_unit; } CAMLprim value flan_poke_i32(value p, value off, value x) { int32_t v = (int32_t)Int32_val(x); memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 4); return Val_unit; } CAMLprim value flan_poke_i64(value p, value off, value x) { int64_t v = Int64_val(x); memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 8); return Val_unit; } CAMLprim value flan_poke_f64(value p, value off, value x) { double v = Double_val(x); memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 8); return Val_unit; } CAMLprim value flan_poke_ptr(value p, value off, value q) { void *v = (void *)Nativeint_val(q); memcpy((char *)Nativeint_val(p) + Long_val(off), &v, sizeof v); return Val_unit; } CAMLprim value flan_poke_bytes(value p, value off, value s) { memcpy((char *)Nativeint_val(p) + Long_val(off), String_val(s), caml_string_length(s)); return Val_unit; } CAMLprim value flan_peek_i32(value p, value off) { int32_t v; memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 4); return caml_copy_int32(v); } CAMLprim value flan_peek_i64(value p, value off) { int64_t v; memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 8); return caml_copy_int64(v); } CAMLprim value flan_peek_f64(value p, value off) { double v; memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 8); return caml_copy_double(v); } CAMLprim value flan_peek_ptr(value p, value off) { void *v; memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), sizeof v); return caml_copy_nativeint((intnat)v); } CAMLprim value flan_peek_bytes(value p, value off, value n) { CAMLparam3(p, off, n); CAMLlocal1(s); s = caml_alloc_string((mlsize_t)Long_val(n)); memcpy((char *)Bytes_val(s), (char *)Nativeint_val(p) + Long_val(off), (size_t)Long_val(n)); CAMLreturn(s); } /* ── The agent, when it is in this same process ─────────────────────── */ /* [flan dev] builds one binary that is the compiled program and holds this * compiler, so a request to the agent need not leave the address space. The * two ends still meet at one line of text and one answer — that is * vendor/agent/flan_agent.c's [handle_line], and this is a call to it. * * Weak, because the same [flan] binary that builds a merged program does not * itself contain an agent: the launcher, [flan reload], the two-process daemon * and every test link no flan_agent.o, and there the symbol is null. So "is * there an agent in this process" is answered by the linker rather than by a * flag that could disagree with reality, and [None] here is what makes * lib/dev.ml fall back to the socket. * * The runtime system is released across the call. A delivery does a [dlopen], * which is milliseconds of relocation and the loader lock, and holding OCaml's * lock through it stalls every other OCaml thread for no reason — DISCUSS.md * §14's third cost, in the one place this lane creates it. Nothing the agent * does touches an OCaml value, so there is nothing to keep alive across it. */ extern char *flan_agent_request(const char *line, uint64_t *len) __attribute__((weak)); extern void flan_agent_request_free(char *p) __attribute__((weak)); CAMLprim value flan_agent_direct(value line) { CAMLparam1(line); CAMLlocal2(s, r); char *out; uint64_t n = 0; if (flan_agent_request == NULL) CAMLreturn(Val_int(0)); /* None */ { /* Copied out first: [String_val] points into the OCaml heap, which may move once the runtime system is released. */ size_t k = caml_string_length(line); char *copy = malloc(k + 1); if (copy == NULL) caml_failwith("out of memory asking the agent"); memcpy(copy, String_val(line), k); copy[k] = '\0'; caml_release_runtime_system(); out = flan_agent_request(copy, &n); caml_acquire_runtime_system(); free(copy); } s = caml_alloc_initialized_string((mlsize_t)n, out == NULL ? "" : out); if (out != NULL && flan_agent_request_free != NULL) flan_agent_request_free(out); r = caml_alloc_some(s); CAMLreturn(r); }