calog/libs/calogTask.c
2026-07-04 23:22:22 -05:00

426 lines
16 KiB
C

// calogTask.c -- calog task library (see calogTask.h). Spawns and manages other calog
// script contexts over the shared handle table; every native is inline, running on the
// calling script's own context thread.
#include "calogTask.h"
#include "calogHandle.h"
#include "calogInternal.h"
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TASK_TYPE_CONTEXT 1u
// Process-wide task registry shared by every runtime that registers the natives; refCount
// is one per registered runtime, so the singleton is freed only when the LAST runtime shuts
// down (never out from under a still-live runtime's captured userData).
typedef struct TaskLibT {
CalogHandleTableT *handles;
int32_t refCount;
} TaskLibT;
// What a task handle owns: the spawned context plus the id of the context that spawned it.
// ONLY the owner may eval or close a task. Because a context is single-threaded, that rules
// out the two ways taskClose's pthread_join could misbehave: a task can never own its own
// handle (so it can't self-join and free itself mid-run), and two tasks can never each own
// the other's handle (so they can't dead-lock closing each other). The get-then-check-
// ownerId sequence in taskEval/taskClose still races a concurrent taskClose's get-then-
// remove-then-free of the SAME handle from another context, so both natives serialize their
// whole get/check/(remove) critical section under gTaskLibMutex -- see taskEval/taskClose.
typedef struct TaskT {
CalogContextT *context;
uint64_t ownerId;
} TaskT;
// Maps an engine name to its vtable, for the engines compiled in. References the extern
// vtables only under their CALOG_WITH_* guard, so unselected engines stay unlinked. The
// names below are taskSpawn's own short spellings (e.g. "js", "s7") and are intentionally
// NOT the same string as the vtable's own CalogEngineT.name (e.g. "javascript", "scheme"
// -- used for engine self-identification elsewhere, not for lookup here); collapsing this
// table to key off ->name would change taskSpawn's public argument spelling and break the
// tested API (tests/testTask.c uses taskSpawn('js', ...)).
typedef struct TaskEngineT {
const char *name;
const CalogEngineT *engine;
} TaskEngineT;
static const TaskEngineT gTaskEngines[] = {
#ifdef CALOG_WITH_LUA
{ "lua", &calogLuaEngine },
#endif
#ifdef CALOG_WITH_JS
{ "js", &calogJsEngine },
#endif
#ifdef CALOG_WITH_SQUIRREL
{ "squirrel", &calogSquirrelEngine },
#endif
#ifdef CALOG_WITH_MYBASIC
{ "mybasic", &calogMyBasicEngine },
#endif
#ifdef CALOG_WITH_BERRY
{ "berry", &calogBerryEngine },
#endif
#ifdef CALOG_WITH_S7
{ "s7", &calogS7Engine },
#endif
#ifdef CALOG_WITH_WREN
{ "wren", &calogWrenEngine },
#endif
#ifdef CALOG_WITH_MRUBY
{ "mruby", &calogMrubyEngine },
#endif
#ifdef CALOG_WITH_TCL
{ "tcl", &calogTclEngine },
#endif
{ NULL, NULL }
};
static pthread_mutex_t gTaskLibMutex = PTHREAD_MUTEX_INITIALIZER;
static TaskLibT *gTaskLib = NULL;
static int32_t taskActive(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t taskClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void taskCloser(uint32_t type, void *resource);
static bool taskContextFinished(void *resource, void *userData);
static int32_t taskCount(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static const CalogEngineT *taskEngineByName(const char *name);
static int32_t taskEval(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t taskExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t taskLoad(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t taskSelf(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t taskSpawn(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int64_t taskWrapContext(TaskLibT *lib, CalogContextT *context, CalogValueT *result, const char *label);
int32_t calogTaskRegister(CalogT *calog) {
pthread_mutex_lock(&gTaskLibMutex);
if (gTaskLib == NULL) {
TaskLibT *lib;
lib = (TaskLibT *)calloc(1, sizeof(*lib));
if (lib == NULL) {
pthread_mutex_unlock(&gTaskLibMutex);
return calogErrOomE;
}
lib->handles = calogHandleTableCreate();
if (lib->handles == NULL) {
free(lib);
pthread_mutex_unlock(&gTaskLibMutex);
return calogErrOomE;
}
gTaskLib = lib;
}
gTaskLib->refCount++;
pthread_mutex_unlock(&gTaskLibMutex);
calogRegisterInline(calog, "taskSpawn", taskSpawn, gTaskLib);
calogRegisterInline(calog, "taskLoad", taskLoad, gTaskLib);
calogRegisterInline(calog, "taskEval", taskEval, gTaskLib);
calogRegisterInline(calog, "taskClose", taskClose, gTaskLib);
calogRegisterInline(calog, "taskActive", taskActive, gTaskLib);
calogRegisterInline(calog, "taskExit", taskExit, gTaskLib);
calogRegisterInline(calog, "taskSelf", taskSelf, gTaskLib);
calogRegisterInline(calog, "taskCount", taskCount, gTaskLib);
return calogOkE;
}
void calogTaskShutdown(void) {
pthread_mutex_lock(&gTaskLibMutex);
if (gTaskLib == NULL) {
pthread_mutex_unlock(&gTaskLibMutex);
return;
}
gTaskLib->refCount--;
if (gTaskLib->refCount <= 0) {
// Last runtime is gone. Free the per-task wrappers; the contexts themselves are
// owned by their runtime and were closed by calogDestroy.
calogHandleTableDestroy(gTaskLib->handles, taskCloser);
free(gTaskLib);
gTaskLib = NULL;
}
pthread_mutex_unlock(&gTaskLibMutex);
}
// Reap every task whose context THREAD has already exited -- a task that called taskExit(), or
// one that simply ran off the end while its owner never closed it. Each wrapper is removed
// under gTaskLibMutex (so it cannot race taskClose on the same handle), then the finished
// context is joined + freed outside the lock. Safe from ANY thread, including a non-owner host:
// a finished thread joins instantly, so this sidesteps the owner-only rule that guards LIVE
// joins. The host runner should call it periodically (e.g. once per calogPump). Returns the
// number reaped.
int32_t calogTaskReap(void) {
int32_t reaped;
reaped = 0;
for (;;) {
TaskT *task;
pthread_mutex_lock(&gTaskLibMutex);
task = (gTaskLib != NULL) ? (TaskT *)calogHandleTakeIf(gTaskLib->handles, TASK_TYPE_CONTEXT, taskContextFinished, NULL) : NULL;
pthread_mutex_unlock(&gTaskLibMutex);
if (task == NULL) {
break;
}
calogContextClose(task->context);
free(task);
reaped++;
}
return reaped;
}
// taskActive() -> bool: the SELF form a long-running task polls to cooperate with taskClose.
// taskClose is cooperative -- it flips this context's shutdown flag then blocks joining the
// thread -- so a task busy in a pure loop never notices unless it checks. It returns false
// once the owner has asked this task to stop; a loop does `while taskActive() do ... end`.
// A task has no handle to itself (it can never own its own handle -- that invariant is what
// keeps taskClose's join from self-joining), so the self form takes no argument.
//
// taskActive(handle) -> bool: the OWNER form -- true while the task the caller spawned under
// handle is still open (not yet taskClose'd). Only the owner gets a meaningful answer.
static int32_t taskActive(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
TaskLibT *lib;
TaskT *task;
lib = (TaskLibT *)userData;
calogValueNil(result);
if (argCount == 0) {
calogValueBool(result, !calogCurrentShuttingDown());
return calogOkE;
}
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "taskActive expects () or (handle)");
}
pthread_mutex_lock(&gTaskLibMutex);
task = (TaskT *)calogHandleGet(lib->handles, args[0].as.i, TASK_TYPE_CONTEXT);
calogValueBool(result, task != NULL && task->ownerId == calogCurrentId());
pthread_mutex_unlock(&gTaskLibMutex);
return calogOkE;
}
static int32_t taskClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
TaskLibT *lib;
TaskT *task;
lib = (TaskLibT *)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "taskClose expects (handle)");
}
// Only the owner may close (see TaskT). Get, the ownerId check, and Remove all run
// under gTaskLibMutex so a concurrent taskEval/taskClose on the SAME handle from
// another context can never observe this task between its removal and its free below.
pthread_mutex_lock(&gTaskLibMutex);
task = (TaskT *)calogHandleGet(lib->handles, args[0].as.i, TASK_TYPE_CONTEXT);
if (task == NULL) {
pthread_mutex_unlock(&gTaskLibMutex);
return calogFail(result, calogErrArgE, "taskClose: invalid task handle");
}
if (task->ownerId != calogCurrentId()) {
pthread_mutex_unlock(&gTaskLibMutex);
return calogFail(result, calogErrArgE, "taskClose: only the task's owner may close it");
}
task = (TaskT *)calogHandleRemove(lib->handles, args[0].as.i, TASK_TYPE_CONTEXT);
pthread_mutex_unlock(&gTaskLibMutex);
if (task == NULL) {
return calogFail(result, calogErrArgE, "taskClose: invalid task handle");
}
calogContextClose(task->context);
free(task);
return calogOkE;
}
static void taskCloser(uint32_t type, void *resource) {
(void)type;
// Free only the wrapper; the context is owned by its runtime (closed by calogDestroy).
free(resource);
}
static bool taskContextFinished(void *resource, void *userData) {
(void)userData;
return calogContextFinished(((TaskT *)resource)->context);
}
static int32_t taskCount(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
TaskLibT *lib;
(void)args;
(void)argCount;
lib = (TaskLibT *)userData;
calogValueInt(result, (int64_t)calogHandleCount(lib->handles, TASK_TYPE_CONTEXT));
return calogOkE;
}
static const CalogEngineT *taskEngineByName(const char *name) {
int32_t index;
for (index = 0; gTaskEngines[index].name != NULL; index++) {
if (strcmp(gTaskEngines[index].name, name) == 0) {
return gTaskEngines[index].engine;
}
}
return NULL;
}
static int32_t taskEval(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
TaskLibT *lib;
TaskT *task;
CalogContextT *context;
lib = (TaskLibT *)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "taskEval expects (handle, code)");
}
// See taskClose: Get and the ownerId check run under gTaskLibMutex so a concurrent
// taskClose on the SAME handle from the owner can never free this task while we are
// still reading it. Once we know we are the owner, the context pointer is ours to use
// outside the lock -- the owner is single-threaded, so it cannot be closing the same
// task from another thread while this call is in flight.
pthread_mutex_lock(&gTaskLibMutex);
task = (TaskT *)calogHandleGet(lib->handles, args[0].as.i, TASK_TYPE_CONTEXT);
if (task == NULL) {
pthread_mutex_unlock(&gTaskLibMutex);
return calogFail(result, calogErrArgE, "taskEval: invalid task handle");
}
if (task->ownerId != calogCurrentId()) {
pthread_mutex_unlock(&gTaskLibMutex);
return calogFail(result, calogErrArgE, "taskEval: only the task's owner may control it");
}
context = task->context;
pthread_mutex_unlock(&gTaskLibMutex);
if (calogContextEval(context, args[1].as.s.bytes) != calogOkE) {
return calogFail(result, calogErrArgE, "taskEval: could not queue the code");
}
return calogOkE;
}
static int32_t taskExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
// A task retires ITSELF. It cannot free its own context synchronously (it runs on the very
// thread a close would join), so it flags the context to retire once the CURRENT code
// finishes (the rest of this eval runs normally); the finished context is then reaped by
// calogTaskReap, which the host calls while pumping. No handle -- a task has none to itself.
calogCurrentRetire();
return calogOkE;
}
static int32_t taskLoad(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
TaskLibT *lib;
CalogT *calog;
CalogContextT *context;
int64_t handle;
lib = (TaskLibT *)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "taskLoad expects (baseName)");
}
calog = calogCurrent();
if (calog == NULL) {
return calogFail(result, calogErrArgE, "taskLoad: must be called from a script");
}
context = calogContextLoad(calog, args[0].as.s.bytes);
if (context == NULL) {
return calogFail(result, calogErrArgE, "taskLoad: no matching script file, or the load failed");
}
handle = taskWrapContext(lib, context, result, "taskLoad");
if (handle == 0) {
return calogErrOomE;
}
calogValueInt(result, handle);
return calogOkE;
}
static int32_t taskSelf(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
calogValueInt(result, (int64_t)calogCurrentId());
return calogOkE;
}
static int32_t taskSpawn(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
TaskLibT *lib;
CalogT *calog;
const CalogEngineT *engine;
CalogContextT *context;
TaskT *task;
int64_t handle;
lib = (TaskLibT *)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "taskSpawn expects (engine, code)");
}
engine = taskEngineByName(args[0].as.s.bytes);
if (engine == NULL) {
return calogFail(result, calogErrUnsupportedE, "taskSpawn: unknown or uncompiled engine");
}
calog = calogCurrent();
if (calog == NULL) {
return calogFail(result, calogErrArgE, "taskSpawn: must be called from a script");
}
context = calogContextOpen(calog, engine);
if (context == NULL) {
return calogFail(result, calogErrArgE, "taskSpawn: could not open a context");
}
handle = taskWrapContext(lib, context, result, "taskSpawn");
if (handle == 0) {
return calogErrOomE;
}
if (calogContextEval(context, args[1].as.s.bytes) != calogOkE) {
task = (TaskT *)calogHandleRemove(lib->handles, handle, TASK_TYPE_CONTEXT);
free(task);
calogContextClose(context);
return calogFail(result, calogErrArgE, "taskSpawn: could not queue the code");
}
calogValueInt(result, handle);
return calogOkE;
}
// Shared tail of taskLoad/taskSpawn: wrap an already-open context in a TaskT and file it in
// the handle table. Returns the new handle, or 0 after calling calogFail (with calogErrOomE)
// and closing the context. label is the caller's name, used only in the failure message.
static int64_t taskWrapContext(TaskLibT *lib, CalogContextT *context, CalogValueT *result, const char *label) {
TaskT *task;
int64_t handle;
char message[CALOG_ERR_MSG_CAP];
task = (TaskT *)malloc(sizeof(*task));
if (task == NULL) {
calogContextClose(context);
snprintf(message, sizeof(message), "%s: out of memory", label);
calogFail(result, calogErrOomE, message);
return 0;
}
task->context = context;
task->ownerId = calogCurrentId();
handle = calogHandleAdd(lib->handles, TASK_TYPE_CONTEXT, task);
if (handle == 0) {
free(task);
calogContextClose(context);
snprintf(message, sizeof(message), "%s: out of memory", label);
calogFail(result, calogErrOomE, message);
return 0;
}
return handle;
}