// 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 #include #include #include #include #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 #ifdef CALOG_WITH_JANET { "janet", &calogJanetEngine }, #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 calogAtDestroy(calog, calogTaskShutdown, calogDestroyAfterContextsE); } 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; }