233 lines
10 KiB
C
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
|