426 lines
16 KiB
C
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;
|
|
}
|