Files
scriptc/packages/runtime/src/scr_ffi_queue.c
T
Chris TateandDemonGPT a737fde346 feat(ffi): marshal foreign-thread callbacks (#164)
* feat(ffi): marshal foreign-thread callbacks

- Add format 5 foreign callback descriptors with strict retained, context, and void-return validation.
- Marshal copied callback arguments through an optional MPSC event-loop queue in both C and LLVM backends.
- Cover concurrent delivery, liveness, fairness, teardown, cross-platform builds, and document the boundary contract.

Co-authored-by: DemonGPT <127974011+DemonGPT@users.noreply.github.com>

* fix(ffi): preserve callback checkpoints

---------

Co-authored-by: DemonGPT <127974011+DemonGPT@users.noreply.github.com>
2026-08-16 22:59:02 -05:00

380 lines
13 KiB
C

#include "scr_runtime.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#elif defined(__wasi__)
/* Native FFI is rejected for WASI. This keeps direct runtime builds honest. */
#else
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#endif
typedef enum {
SCR_FFI_ARG_NONE = 0,
SCR_FFI_ARG_F64,
SCR_FFI_ARG_BOOL,
SCR_FFI_ARG_U8,
SCR_FFI_ARG_U32,
SCR_FFI_ARG_I32,
SCR_FFI_ARG_DATA,
} ScrFfiArgKind;
typedef struct {
ScrFfiArgKind kind;
union {
double f64;
uint32_t u32;
int32_t i32;
struct {
uint8_t *data;
size_t len;
} span;
} value;
} ScrFfiArg;
struct ScrFfiCall {
ScrFfiTable *table; /* opaque on the posting thread */
void *loop; /* reserved instance/loop identity */
ScrClosure *callback; /* opaque; the table owns its script RC pin */
ScrFfiDispatchFn dispatch;
size_t nargs;
ScrFfiArg *args;
struct ScrFfiCall *next;
};
static ScrFfiCall *scr_ffi_head;
static ScrFfiCall *scr_ffi_tail;
static size_t scr_ffi_foreign_registrations;
static bool scr_ffi_installed;
static bool scr_ffi_stopping;
static int scr_ffi_wake_pipe[2] = {-1, -1};
#ifdef _WIN32
static SRWLOCK scr_ffi_lock = SRWLOCK_INIT;
static void scr_ffi_lock_enter(void) { AcquireSRWLockExclusive(&scr_ffi_lock); }
static void scr_ffi_lock_leave(void) { ReleaseSRWLockExclusive(&scr_ffi_lock); }
#elif defined(__wasi__)
static void scr_ffi_lock_enter(void) {}
static void scr_ffi_lock_leave(void) {}
#else
static pthread_mutex_t scr_ffi_lock = PTHREAD_MUTEX_INITIALIZER;
static void scr_ffi_lock_enter(void) { (void)pthread_mutex_lock(&scr_ffi_lock); }
static void scr_ffi_lock_leave(void) { (void)pthread_mutex_unlock(&scr_ffi_lock); }
#endif
static void scr_ffi_oom(void) { scr_trap("scriptc: out of memory\n"); }
/* Foreign trampolines cannot enter the script exception machinery. Allocation
* failure and malformed native pointers are fatal process-boundary failures,
* reported without touching any runtime object. */
static void scr_ffi_foreign_fatal(const char *message) {
fputs(message, stderr);
abort();
}
static void *scr_ffi_foreign_alloc(size_t size) {
void *ptr = malloc(size == 0 ? 1 : size);
if (ptr == NULL) scr_ffi_foreign_fatal("scriptc: out of memory\n");
return ptr;
}
static void scr_ffi_call_free(ScrFfiCall *call) {
if (call == NULL) return;
for (size_t i = 0; i < call->nargs; i++) {
if (call->args[i].kind == SCR_FFI_ARG_DATA) free(call->args[i].value.span.data);
}
free(call->args);
free(call);
}
static bool scr_ffi_table_contains(const ScrFfiTable *table, const ScrClosure *callback) {
for (size_t i = 0; i < table->len; i++) {
if (table->entries[i] == callback) return true;
}
return false;
}
static void scr_ffi_table_retire(ScrFfiTable *table, ScrClosure *callback) {
if (table->retired_len == table->retired_cap) {
size_t cap = table->retired_cap == 0 ? 4 : table->retired_cap * 2;
if (cap < table->retired_cap || cap > SIZE_MAX / sizeof *table->retired) scr_ffi_oom();
ScrClosure **retired = realloc(table->retired, cap * sizeof *retired);
if (retired == NULL) scr_ffi_oom();
table->retired = retired;
table->retired_cap = cap;
}
table->retired[table->retired_len++] = callback;
}
static void scr_ffi_release_retired(ScrFfiTable *table) {
ScrClosure **retired = NULL;
size_t len = 0;
scr_ffi_lock_enter();
if (table->queued == 0 && table->retired_len > 0) {
retired = table->retired;
len = table->retired_len;
table->retired = NULL;
table->retired_len = 0;
table->retired_cap = 0;
}
scr_ffi_lock_leave();
for (size_t i = 0; i < len; i++) scr_closure_release(retired[i]);
free(retired);
}
void scr_ffi_retain_foreign(ScrFfiTable *table, ScrClosure *callback) {
table->teardown = &scr_ffi_teardown_foreign;
scr_ffi_link(table);
scr_ffi_lock_enter();
if (table->len == table->cap) {
size_t cap = table->cap == 0 ? 4 : table->cap * 2;
if (cap < table->cap || cap > SIZE_MAX / sizeof *table->entries) scr_ffi_oom();
ScrClosure **entries = realloc(table->entries, cap * sizeof *entries);
if (entries == NULL) scr_ffi_oom();
table->entries = entries;
table->cap = cap;
}
table->entries[table->len++] = scr_closure_retain(callback);
scr_ffi_foreign_registrations++;
scr_ffi_lock_leave();
}
void scr_ffi_require_foreign(ScrFfiTable *table, ScrClosure *callback) {
scr_ffi_lock_enter();
bool found = scr_ffi_table_contains(table, callback);
scr_ffi_lock_leave();
if (!found) scr_trap("scriptc: releasing a native callback registration that does not exist\n");
}
void scr_ffi_release_foreign(ScrFfiTable *table, ScrClosure *callback) {
scr_ffi_lock_enter();
for (size_t i = 0; i < table->len; i++) {
if (table->entries[i] != callback) continue;
ScrClosure *owned = table->entries[i];
table->len--;
if (i != table->len) table->entries[i] = table->entries[table->len];
if (scr_ffi_foreign_registrations > 0) scr_ffi_foreign_registrations--;
bool deferred = table->queued > 0;
if (deferred) scr_ffi_table_retire(table, owned);
scr_ffi_lock_leave();
if (!deferred) scr_closure_release(owned);
return;
}
scr_ffi_lock_leave();
scr_trap("scriptc: releasing a native callback registration that does not exist\n");
}
ScrFfiCall *scr_ffi_call_new(ScrFfiTable *table, ScrClosure *callback,
ScrFfiDispatchFn dispatch, size_t nargs) {
ScrFfiCall *call = scr_ffi_foreign_alloc(sizeof *call);
call->table = table;
call->loop = table->loop;
call->callback = callback;
call->dispatch = dispatch;
call->nargs = nargs;
call->args = nargs == 0 ? NULL : scr_ffi_foreign_alloc(nargs * sizeof *call->args);
if (nargs > 0) memset(call->args, 0, nargs * sizeof *call->args);
call->next = NULL;
return call;
}
static ScrFfiArg *scr_ffi_call_arg(ScrFfiCall *call, size_t index) {
if (index >= call->nargs) scr_ffi_foreign_fatal("scriptc: invalid staged FFI callback argument\n");
return &call->args[index];
}
void scr_ffi_call_set_f64(ScrFfiCall *call, size_t i, double v) {
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_F64; arg->value.f64 = v;
}
void scr_ffi_call_set_bool(ScrFfiCall *call, size_t i, uint8_t v) {
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_BOOL; arg->value.u32 = v != 0;
}
void scr_ffi_call_set_u8(ScrFfiCall *call, size_t i, uint8_t v) {
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_U8; arg->value.u32 = v;
}
void scr_ffi_call_set_u32(ScrFfiCall *call, size_t i, uint32_t v) {
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_U32; arg->value.u32 = v;
}
void scr_ffi_call_set_i32(ScrFfiCall *call, size_t i, int32_t v) {
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_I32; arg->value.i32 = v;
}
static void scr_ffi_call_copy_data(ScrFfiCall *call, size_t i,
const uint8_t *v, size_t len,
const char *null_message) {
if (v == NULL && len != 0) scr_ffi_foreign_fatal(null_message);
ScrFfiArg *arg = scr_ffi_call_arg(call, i);
arg->kind = SCR_FFI_ARG_DATA;
arg->value.span.len = len;
arg->value.span.data = len == 0 ? NULL : scr_ffi_foreign_alloc(len);
if (len > 0) memcpy(arg->value.span.data, v, len);
}
void scr_ffi_call_copy_string(ScrFfiCall *call, size_t i,
const uint8_t *v, size_t len) {
scr_ffi_call_copy_data(call, i, v, len,
"scriptc: native callback passed a NULL string span with nonzero length\n");
}
void scr_ffi_call_copy_bytes(ScrFfiCall *call, size_t i,
const uint8_t *v, size_t len) {
scr_ffi_call_copy_data(call, i, v, len,
"scriptc: native callback passed a NULL bytes span with nonzero length\n");
}
void scr_ffi_call_copy_cstring(ScrFfiCall *call, size_t i, const char *v) {
if (v == NULL) scr_ffi_foreign_fatal("scriptc: native callback passed a NULL cstring\n");
scr_ffi_call_copy_data(call, i, (const uint8_t *)v, strlen(v),
"scriptc: native callback passed a NULL cstring\n");
}
void scr_ffi_post(ScrFfiCall *call) {
scr_ffi_lock_enter();
if (scr_ffi_stopping || !scr_ffi_table_contains(call->table, call->callback)) {
scr_ffi_lock_leave();
scr_ffi_call_free(call);
return;
}
call->table->queued++;
if (scr_ffi_tail != NULL) scr_ffi_tail->next = call;
else scr_ffi_head = call;
scr_ffi_tail = call;
#if !defined(_WIN32) && !defined(__wasi__)
if (scr_ffi_wake_pipe[1] >= 0) {
ssize_t ignored = write(scr_ffi_wake_pipe[1], "f", 1);
(void)ignored; /* a full pipe is already readable */
}
#endif
scr_ffi_lock_leave();
}
static const ScrFfiArg *scr_ffi_get_arg(const ScrFfiCall *call, size_t index) {
if (index >= call->nargs) scr_trap("scriptc: invalid staged FFI callback argument\n");
return &call->args[index];
}
double scr_ffi_call_get_f64(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.f64; }
bool scr_ffi_call_get_bool(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.u32 != 0; }
double scr_ffi_call_get_u8(const ScrFfiCall *call, size_t i) { return (double)scr_ffi_get_arg(call, i)->value.u32; }
double scr_ffi_call_get_u32(const ScrFfiCall *call, size_t i) { return (double)scr_ffi_get_arg(call, i)->value.u32; }
double scr_ffi_call_get_i32(const ScrFfiCall *call, size_t i) { return (double)scr_ffi_get_arg(call, i)->value.i32; }
const uint8_t *scr_ffi_call_get_data(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.span.data; }
size_t scr_ffi_call_get_len(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.span.len; }
static bool scr_ffi_pending(void) {
bool pending;
scr_ffi_lock_enter();
pending = scr_ffi_foreign_registrations > 0 || scr_ffi_head != NULL;
scr_ffi_lock_leave();
return pending;
}
static void scr_ffi_wake_drain(void) {
#if !defined(_WIN32) && !defined(__wasi__)
if (scr_ffi_wake_pipe[0] < 0) return;
char buf[64];
while (read(scr_ffi_wake_pipe[0], buf, sizeof buf) > 0) {}
#endif
}
static bool scr_ffi_dispatch(void) {
scr_ffi_lock_enter();
ScrFfiCall *call = scr_ffi_head;
if (call != NULL) {
scr_ffi_head = call->next;
if (scr_ffi_head == NULL) scr_ffi_tail = NULL;
}
scr_ffi_lock_leave();
if (call == NULL) return false;
call->dispatch(call->callback, call);
ScrFfiTable *table = call->table;
scr_ffi_lock_enter();
if (table->queued > 0) table->queued--;
/* Keep the wake fd readable while queued calls remain. Drain only while
* holding the same lock writers use, closing the post-vs-drain lost-wakeup
* race for the transition to an empty queue. */
bool more = scr_ffi_head != NULL;
if (!more) scr_ffi_wake_drain();
scr_ffi_lock_leave();
scr_ffi_call_free(call);
scr_ffi_release_retired(table);
return true;
}
static int scr_ffi_pollfd(void) { return scr_ffi_wake_pipe[0]; }
void scr_ffi_install(void) {
if (scr_ffi_installed) return;
scr_ffi_installed = true;
#if !defined(_WIN32) && !defined(__wasi__)
if (pipe(scr_ffi_wake_pipe) == 0) {
for (int i = 0; i < 2; i++) {
(void)fcntl(scr_ffi_wake_pipe[i], F_SETFL, O_NONBLOCK);
(void)fcntl(scr_ffi_wake_pipe[i], F_SETFD, FD_CLOEXEC);
}
} else {
scr_ffi_wake_pipe[0] = scr_ffi_wake_pipe[1] = -1;
}
#endif
scr_loop_set_ffi(&scr_ffi_pending, &scr_ffi_dispatch, &scr_ffi_pollfd,
&scr_ffi_stop);
}
void scr_ffi_stop(void) {
scr_ffi_lock_enter();
if (scr_ffi_stopping) {
scr_ffi_lock_leave();
return;
}
scr_ffi_stopping = true;
ScrFfiCall *calls = scr_ffi_head;
scr_ffi_head = scr_ffi_tail = NULL;
for (ScrFfiCall *call = calls; call != NULL; call = call->next) {
if (call->table->queued > 0) call->table->queued--;
}
scr_ffi_wake_drain();
#if !defined(_WIN32) && !defined(__wasi__)
if (scr_ffi_wake_pipe[0] >= 0) close(scr_ffi_wake_pipe[0]);
if (scr_ffi_wake_pipe[1] >= 0) close(scr_ffi_wake_pipe[1]);
#endif
scr_ffi_wake_pipe[0] = scr_ffi_wake_pipe[1] = -1;
scr_ffi_lock_leave();
while (calls != NULL) {
ScrFfiCall *next = calls->next;
ScrFfiTable *table = calls->table;
scr_ffi_call_free(calls);
scr_ffi_release_retired(table);
calls = next;
}
}
void scr_ffi_teardown_foreign(ScrFfiTable *table) {
scr_ffi_stop();
scr_ffi_lock_enter();
ScrClosure **entries = table->entries;
size_t len = table->len;
ScrClosure **retired = table->retired;
size_t retired_len = table->retired_len;
if (scr_ffi_foreign_registrations >= len) scr_ffi_foreign_registrations -= len;
table->entries = NULL;
table->len = 0;
table->cap = 0;
table->retired = NULL;
table->retired_len = 0;
table->retired_cap = 0;
table->queued = 0;
scr_ffi_lock_leave();
for (size_t i = 0; i < len; i++) scr_closure_release(entries[i]);
free(entries);
for (size_t i = 0; i < retired_len; i++) scr_closure_release(retired[i]);
free(retired);
}