calog/src/calog.h

233 lines
10 KiB
C

// calog.h -- embed multi-language scripting into a C/C++ host.
//
// calog is a script broker: register native C functions once and call them from any
// embedded engine (Lua, JavaScript/QuickJS, Squirrel, my-basic), passing values by a
// single canonical type. This header is the entire public surface. A CalogT is a
// self-contained runtime -- independent runtimes may coexist in one process, and one
// host thread may drive several by pumping each in turn (don't share values across
// them). See design.md.
//
// Threading model (design.md sec 6):
// - Scripts run fire-and-forget on their own context threads; calogContextEval
// returns immediately.
// - A registered native runs on the HOST thread (the thread that calls
// calogCreate/calogPump), serialized, so host C code is never called
// concurrently and needs no locking. The host services those calls by calling
// calogPump() in its own loop.
// - calogRegisterInline is the escape hatch: an inline native runs directly on the
// calling script's thread (no host hop) -- YOU guarantee it is thread-safe.
// - A script error is delivered to the error handler (calogSetErrorHandler) on the
// host thread during calogPump; script results come back by calling natives.
#ifndef CALOG_H
#define CALOG_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
// Maximum nesting depth honored by the recursive copy/marshal paths. A deeper
// structure, or a cycle, fails with calogErrDepthE; the free path relies on the same
// invariant (aggregates must be acyclic).
#define CALOG_MAX_DEPTH 64
// Growth multiplier for growable buffers. Must stay a power of two (the registry
// derives its probe mask from a power-of-two slot count).
#define CALOG_GROWTH_FACTOR 2
typedef enum CalogStatusE {
calogOkE = 0,
calogErrOomE = 1,
calogErrDepthE = 2,
calogErrTypeE = 3,
calogErrNotFoundE = 4,
calogErrArgE = 5,
calogErrRangeE = 6,
calogErrUnsupportedE = 7,
calogErrDeadE = 8
} CalogStatusE;
typedef enum CalogTypeE {
calogNilE = 0,
calogBoolE = 1,
calogIntE = 2,
calogRealE = 3,
calogStringE = 4,
calogAggE = 5,
calogFnE = 6
} CalogTypeE;
// Disambiguates an empty or mixed container so it round-trips deterministically onto
// engines (such as my-basic) that model lists and dicts as distinct values.
typedef enum CalogKindE {
calogListE = 0,
calogMapE = 1,
calogBothE = 2
} CalogKindE;
typedef struct CalogT CalogT; // the scripting runtime (registry + actor)
typedef struct CalogContextT CalogContextT; // one engine instance on its own thread
typedef struct CalogAggT CalogAggT;
typedef struct CalogFnT CalogFnT; // a script function value (refcounted)
typedef struct CalogPairT CalogPairT;
typedef struct CalogStringT CalogStringT;
typedef struct CalogValueT CalogValueT;
// Length-prefixed, binary-safe string. bytes is always NUL-terminated at
// bytes[length] so plain C consumers can read it, while length permits embedded NULs.
struct CalogStringT {
char *bytes;
int64_t length;
};
struct CalogValueT {
CalogTypeE type;
union {
bool b;
int64_t i;
double r;
CalogStringT s;
CalogAggT *agg;
CalogFnT *fn;
} as;
};
struct CalogPairT {
CalogValueT key;
CalogValueT value;
};
// Hybrid container: the sequence part lives in array[0, arrayCount), the keyed part
// in pairs[0, pairCount). Only scalar and string keys are meaningful (see
// calogValueEquals); aggregate/function keys match by pointer identity only.
struct CalogAggT {
CalogKindE kind;
CalogValueT *array;
int64_t arrayCount;
int64_t arrayCap;
CalogPairT *pairs;
int64_t pairCount;
int64_t pairCap;
};
// The uniform native-function signature. result is nil on entry; on failure write a
// message into result with calogFail and return a non-zero CalogStatusE.
typedef int32_t (*CalogNativeFnT)(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
// Delivered on the host thread (during calogPump) when a fire-and-forget script
// fails: contextId is the failing context, message is the engine's error text.
typedef void (*CalogErrorFnT)(uint64_t contextId, const char *message, void *userData);
// Per-engine interpreter lifecycle + execution. Every hook runs on the context's OWN
// thread. createInterpreter exposes every registered native (calog does this via the
// adapter); a NULL engine makes a synthetic context with no interpreter. Embedders
// may supply a custom engine, but normally pass a built-in vtable below.
typedef struct CalogEngineT {
const char *name;
// NULL-terminated list of file extensions this engine handles, without the leading
// dot (e.g. {"lua", NULL}). calogContextLoad uses it to pick an engine by filename.
const char *const *extensions;
int32_t (*createInterpreter)(CalogContextT *context, void **interpOut);
void (*destroyInterpreter)(void *interp);
int32_t (*runSource)(void *interp, const char *source, CalogValueT *result);
} CalogEngineT;
// ---- runtime ----
CalogT *calogCreate(void);
void calogDestroy(CalogT *calog);
void calogPump(CalogT *calog); // run pending script->native calls on THIS thread
void calogSetErrorHandler(CalogT *calog, CalogErrorFnT fn, void *userData);
// ---- natives ----
int32_t calogRegister(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the host thread
int32_t calogRegisterInline(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the calling script thread
int32_t calogCall(CalogT *calog, const char *name, CalogValueT *args, int32_t argCount, CalogValueT *result);
int32_t calogFail(CalogValueT *result, int32_t status, const char *message);
const char *calogTypeName(CalogTypeE type);
// ---- values ----
void calogValueNil(CalogValueT *value);
void calogValueBool(CalogValueT *value, bool b);
void calogValueInt(CalogValueT *value, int64_t i);
void calogValueReal(CalogValueT *value, double r);
int32_t calogValueString(CalogValueT *value, const char *bytes, int64_t length);
void calogValueAgg(CalogValueT *value, CalogAggT *aggregate);
void calogValueFn(CalogValueT *value, CalogFnT *fn);
int32_t calogValueCopy(CalogValueT *dst, const CalogValueT *src);
void calogValueMove(CalogValueT *dst, CalogValueT *src);
void calogValueFree(CalogValueT *value);
bool calogValueEquals(const CalogValueT *a, const CalogValueT *b);
// ---- aggregates ----
int32_t calogAggCreate(CalogAggT **out, CalogKindE kind);
void calogAggFree(CalogAggT *aggregate);
CalogValueT *calogAggGet(CalogAggT *aggregate, const CalogValueT *key);
int32_t calogAggPush(CalogAggT *aggregate, CalogValueT *value);
int32_t calogAggSet(CalogAggT *aggregate, CalogValueT *key, CalogValueT *value);
// ---- function values ----
// Wrap a host native as a host-owned function value (runs on the host thread during
// calogPump, like calogRegister, but anonymous). Hand the result to a script via a
// native's result to give the script a callable; release your reference with
// calogFnRelease once you no longer hold it.
int32_t calogFnFromNative(CalogFnT **out, CalogT *calog, CalogNativeFnT fn, void *userData);
int32_t calogFnInvoke(CalogFnT *fn, CalogValueT *args, int32_t argCount, CalogValueT *result);
void calogFnRetain(CalogFnT *fn);
void calogFnRelease(CalogFnT *fn);
// ---- contexts + engines ----
CalogContextT *calogContextOpen(CalogT *calog, const CalogEngineT *engine); // create + start; NULL on failure
// Make an engine available to calogContextLoad. Call at setup, before loading;
// registration order is the search priority.
void calogRegisterEngine(CalogT *calog, const CalogEngineT *engine);
// Search the registered engines for a readable file "<baseFileName>.<ext>" (first
// engine, then first extension, that names an existing file wins), open a context on
// that engine, load the file into it (fire-and-forget), and return the context. NULL if
// no file matched or the load failed.
CalogContextT *calogContextLoad(CalogT *calog, const char *baseFileName);
int32_t calogContextEval(CalogContextT *context, const char *source); // fire-and-forget
void calogContextClose(CalogContextT *context);
uint64_t calogContextId(const CalogContextT *context);
uint64_t calogCurrentId(void); // context id of the calling thread (0 = host thread)
CalogT *calogCurrent(void); // runtime of the calling context (NULL if none)
extern const CalogEngineT calogLuaEngine;
extern const CalogEngineT calogJsEngine;
extern const CalogEngineT calogSquirrelEngine;
extern const CalogEngineT calogMyBasicEngine;
extern const CalogEngineT calogBerryEngine;
extern const CalogEngineT calogS7Engine;
extern const CalogEngineT calogWrenEngine;
// Register the built-in engines selected at build time, so calogContextLoad works
// without hand-registering each. Define CALOG_WITH_LUA / _JS / _SQUIRREL / _MYBASIC (in
// the TU that calls this -- normally via your build's engine flags) for each engine you
// link; only those are referenced here, so unselected engines and their archives stay
// unlinked. The listed order is the load-search priority. Being header-inline, this
// emits nothing unless actually called.
static inline void calogRegisterBuiltinEngines(CalogT *calog) {
#ifdef CALOG_WITH_LUA
calogRegisterEngine(calog, &calogLuaEngine);
#endif
#ifdef CALOG_WITH_JS
calogRegisterEngine(calog, &calogJsEngine);
#endif
#ifdef CALOG_WITH_SQUIRREL
calogRegisterEngine(calog, &calogSquirrelEngine);
#endif
#ifdef CALOG_WITH_MYBASIC
calogRegisterEngine(calog, &calogMyBasicEngine);
#endif
#ifdef CALOG_WITH_BERRY
calogRegisterEngine(calog, &calogBerryEngine);
#endif
#ifdef CALOG_WITH_S7
calogRegisterEngine(calog, &calogS7Engine);
#endif
#ifdef CALOG_WITH_WREN
calogRegisterEngine(calog, &calogWrenEngine);
#endif
(void)calog; // no-op if the build selected no engines
}
#endif