713 lines
23 KiB
C
713 lines
23 KiB
C
// value.c -- CalogValueT / CalogAggT / CalogFnT lifecycle for the broker core.
|
|
//
|
|
// All cross-boundary data is by-value: calogValueCopy deep-copies aggregates and
|
|
// strings so no two owners (and, later, no two threads) ever share a heap
|
|
// pointer. The single exception is calogFnE: a function value is a refcounted
|
|
// CalogFnT handle that is shared by reference and only ever invoked, never
|
|
// inspected. Aggregates are required to be acyclic and bounded by
|
|
// CALOG_MAX_DEPTH; the copy path enforces the bound, and the free path relies
|
|
// on it.
|
|
|
|
#include "calogInternal.h"
|
|
|
|
#include <math.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define BROKER_MIN_CAPACITY 8
|
|
#define CALLABLE_INITIAL_REFCOUNT 1
|
|
|
|
struct CalogFnT {
|
|
CalogNativeFnT fn;
|
|
void *userData;
|
|
CalogReleaseFnT release;
|
|
CalogT *runtime; // owning runtime; its hooks route invoke/release
|
|
uint64_t ownerCtxId; // the owning context's 64-bit id (0 = host)
|
|
_Atomic int32_t refCount;
|
|
_Atomic bool alive;
|
|
// Set by calogFnReclaim once the owner has run the engine release, so a later finalize does not
|
|
// run it again. Atomic, and the ONLY thing reclaim changes: fn/userData/release are written once
|
|
// at create and read by other threads (actorInvokeCallable marshals them), so mutating those
|
|
// would be a data race against an invoke already in flight.
|
|
_Atomic bool reclaimed;
|
|
};
|
|
|
|
static int32_t aggregateCopyDepth(CalogAggT **out, const CalogAggT *src, int32_t depth);
|
|
static int32_t valueCopyDepth(CalogValueT *dst, const CalogValueT *src, int32_t depth);
|
|
|
|
|
|
static int32_t aggregateCopyDepth(CalogAggT **out, const CalogAggT *src, int32_t depth) {
|
|
CalogAggT *copy;
|
|
void *buffer;
|
|
int32_t status;
|
|
int64_t index;
|
|
|
|
*out = NULL;
|
|
if (depth >= CALOG_MAX_DEPTH) {
|
|
return calogErrDepthE;
|
|
}
|
|
status = calogAggCreate(©, src->kind);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
for (index = 0; index < src->arrayCount; index++) {
|
|
CalogValueT element;
|
|
status = valueCopyDepth(&element, &src->array[index], depth + 1);
|
|
if (status != calogOkE) {
|
|
calogAggFree(copy);
|
|
return status;
|
|
}
|
|
status = calogAggPush(copy, &element);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&element);
|
|
calogAggFree(copy);
|
|
return status;
|
|
}
|
|
}
|
|
for (index = 0; index < src->pairCount; index++) {
|
|
CalogValueT key;
|
|
CalogValueT value;
|
|
status = valueCopyDepth(&key, &src->pairs[index].key, depth + 1);
|
|
if (status != calogOkE) {
|
|
calogAggFree(copy);
|
|
return status;
|
|
}
|
|
status = valueCopyDepth(&value, &src->pairs[index].value, depth + 1);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&key);
|
|
calogAggFree(copy);
|
|
return status;
|
|
}
|
|
// Source pairs are unique by construction, so append directly instead of via
|
|
// calogAggSet, whose per-insert duplicate scan made this copy O(n^2).
|
|
buffer = copy->pairs;
|
|
status = calogGrow(&buffer, ©->pairCap, copy->pairCount + 1, sizeof(CalogPairT));
|
|
if (status != calogOkE) {
|
|
calogValueFree(&key);
|
|
calogValueFree(&value);
|
|
calogAggFree(copy);
|
|
return status;
|
|
}
|
|
copy->pairs = (CalogPairT *)buffer;
|
|
calogValueMove(©->pairs[copy->pairCount].key, &key);
|
|
calogValueMove(©->pairs[copy->pairCount].value, &value);
|
|
copy->pairCount++;
|
|
}
|
|
*out = copy;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
int32_t calogAggCreate(CalogAggT **out, CalogKindE kind) {
|
|
CalogAggT *aggregate;
|
|
|
|
*out = NULL;
|
|
aggregate = (CalogAggT *)calloc(1, sizeof(*aggregate));
|
|
if (aggregate == NULL) {
|
|
return calogErrOomE;
|
|
}
|
|
aggregate->kind = kind;
|
|
*out = aggregate;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
void calogAggFree(CalogAggT *aggregate) {
|
|
int64_t index;
|
|
|
|
if (aggregate == NULL) {
|
|
return;
|
|
}
|
|
for (index = 0; index < aggregate->arrayCount; index++) {
|
|
calogValueFree(&aggregate->array[index]);
|
|
}
|
|
for (index = 0; index < aggregate->pairCount; index++) {
|
|
calogValueFree(&aggregate->pairs[index].key);
|
|
calogValueFree(&aggregate->pairs[index].value);
|
|
}
|
|
free(aggregate->array);
|
|
free(aggregate->pairs);
|
|
free(aggregate);
|
|
}
|
|
|
|
|
|
CalogValueT *calogAggGet(CalogAggT *aggregate, const CalogValueT *key) {
|
|
int64_t index;
|
|
|
|
for (index = 0; index < aggregate->pairCount; index++) {
|
|
if (calogValueEquals(&aggregate->pairs[index].key, key)) {
|
|
return &aggregate->pairs[index].value;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
bool calogAggIsKeyed(const CalogAggT *aggregate) {
|
|
return aggregate->pairCount > 0 || aggregate->kind == calogMapE;
|
|
}
|
|
|
|
|
|
int32_t calogAggPush(CalogAggT *aggregate, CalogValueT *value) {
|
|
void *buffer;
|
|
int32_t status;
|
|
|
|
// Pun through a real void* lvalue, never (void **)&typedPointer, to avoid a
|
|
// strict-aliasing violation when calogGrow stores the resized pointer.
|
|
buffer = aggregate->array;
|
|
status = calogGrow(&buffer, &aggregate->arrayCap, aggregate->arrayCount + 1, sizeof(CalogValueT));
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
aggregate->array = (CalogValueT *)buffer;
|
|
calogValueMove(&aggregate->array[aggregate->arrayCount], value);
|
|
aggregate->arrayCount++;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
int32_t calogAggSet(CalogAggT *aggregate, CalogValueT *key, CalogValueT *value) {
|
|
void *buffer;
|
|
int32_t status;
|
|
int64_t index;
|
|
|
|
for (index = 0; index < aggregate->pairCount; index++) {
|
|
if (calogValueEquals(&aggregate->pairs[index].key, key)) {
|
|
calogValueFree(&aggregate->pairs[index].value);
|
|
calogValueMove(&aggregate->pairs[index].value, value);
|
|
calogValueFree(key);
|
|
return calogOkE;
|
|
}
|
|
}
|
|
buffer = aggregate->pairs;
|
|
status = calogGrow(&buffer, &aggregate->pairCap, aggregate->pairCount + 1, sizeof(CalogPairT));
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
aggregate->pairs = (CalogPairT *)buffer;
|
|
calogValueMove(&aggregate->pairs[aggregate->pairCount].key, key);
|
|
calogValueMove(&aggregate->pairs[aggregate->pairCount].value, value);
|
|
aggregate->pairCount++;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
int32_t calogFnCreate(CalogFnT **out, CalogT *runtime, CalogNativeFnT fn, void *userData, CalogReleaseFnT release, uint64_t ownerCtxId) {
|
|
CalogFnT *callable;
|
|
int32_t status;
|
|
|
|
*out = NULL;
|
|
callable = (CalogFnT *)calloc(1, sizeof(*callable));
|
|
if (callable == NULL) {
|
|
return calogErrOomE;
|
|
}
|
|
callable->fn = fn;
|
|
callable->userData = userData;
|
|
callable->release = release;
|
|
callable->runtime = runtime;
|
|
callable->ownerCtxId = ownerCtxId;
|
|
atomic_init(&callable->refCount, CALLABLE_INITIAL_REFCOUNT);
|
|
atomic_init(&callable->alive, true);
|
|
atomic_init(&callable->reclaimed, false);
|
|
// List it against the owning context, which reclaims the engine handle if it is torn down while
|
|
// something else still holds this callable (design.md sec 26). No list, no reclaim -- so a
|
|
// failure here fails the create rather than handing back a callable that could strand its handle.
|
|
status = calogContextTrackFn(runtime, ownerCtxId, callable);
|
|
if (status != calogOkE) {
|
|
free(callable);
|
|
return status;
|
|
}
|
|
*out = callable;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
int32_t calogFnFromNative(CalogFnT **out, CalogT *calog, CalogNativeFnT fn, void *userData) {
|
|
// A host-owned callable (owner id CALOG_HOST_ID = host): no engine handle to release.
|
|
return calogFnCreate(out, calog, fn, userData, NULL, CALOG_HOST_ID);
|
|
}
|
|
|
|
|
|
void calogFnFinalize(CalogFnT *callable) {
|
|
if (callable == NULL) {
|
|
return;
|
|
}
|
|
// Last reference is gone. If the owner context is still alive, run the engine's closure
|
|
// release (luaL_unref / sq_release / ...), which also frees the engine's per-callable
|
|
// struct; the actor layer routes this to the owner's thread (design.md sec 10) so the
|
|
// interpreter op is safe. If the owner is GONE, its interpreter -- and this callable's
|
|
// handle inside it -- were already destroyed, so we must NOT touch it: free the engine's
|
|
// per-callable struct directly. This is what lets an exported function outlive its
|
|
// context (e.g. a script unloaded and reloaded, re-exporting new versions).
|
|
//
|
|
// (Freeing userData directly requires every engine's per-callable struct to be a single
|
|
// heap allocation; all adapters honor that -- see CalogReleaseFnT in calogInternal.h.)
|
|
//
|
|
// A callable the owner already RECLAIMED (calogFnReclaim, as its interpreter was torn down)
|
|
// has no release hook left, so this frees only the shell -- the engine handle went with the
|
|
// interpreter that owned it.
|
|
calogContextUntrackFn(callable->runtime, callable->ownerCtxId, callable);
|
|
if (callable->release != NULL && !atomic_exchange_explicit(&callable->reclaimed, true, memory_order_acq_rel)) {
|
|
if (callable->ownerCtxId == CALOG_HOST_ID || calogContextRegistered(callable->runtime, callable->ownerCtxId)) {
|
|
callable->release(callable);
|
|
} else {
|
|
free(callable->userData);
|
|
}
|
|
}
|
|
free(callable);
|
|
}
|
|
|
|
|
|
// The owning context is tearing its interpreter down; this runs on that context's own thread, with
|
|
// the interpreter still alive, for every callable the context still owns. Running the engine release
|
|
// HERE is what keeps the handle from outliving its VM: whoever else still holds a reference -- a
|
|
// pubsub subscription, an export, a value sitting in another engine, an invoke in flight -- finds a
|
|
// dead callable and finalizes an empty shell. Marking it dead first makes any such invoke fail
|
|
// cleanly instead of reaching into a VM that is going away.
|
|
void calogFnReclaim(CalogFnT *callable) {
|
|
if (callable == NULL) {
|
|
return;
|
|
}
|
|
calogFnMarkDead(callable);
|
|
// Claim the release with an atomic exchange so it runs exactly once, here or in a finalize --
|
|
// never both. Nothing else about the callable changes: an invoke already past its alive check
|
|
// may still be reading fn/userData on another thread to marshal this call, and those reads must
|
|
// not race a write. (That marshal cannot land: reclaim runs after serveLoop has closed this
|
|
// context's queue, so the dispatch is refused -- see sec 28.)
|
|
if (callable->release != NULL && !atomic_exchange_explicit(&callable->reclaimed, true, memory_order_acq_rel)) {
|
|
callable->release(callable); // frees the engine handle AND the adapter's userData block
|
|
}
|
|
}
|
|
|
|
|
|
CalogNativeFnT calogFnNative(const CalogFnT *callable) {
|
|
return callable->fn;
|
|
}
|
|
|
|
|
|
int32_t calogFnInvoke(CalogFnT *callable, CalogValueT *args, int32_t argCount, CalogValueT *result) {
|
|
calogValueNil(result);
|
|
// The owner reclaimed this callable as its interpreter went away (calogFnReclaim), so there is
|
|
// nothing left to call. calogErrDeadE is the same answer the routing path gives for a context
|
|
// that is gone -- the timer library cancels a timer on exactly that, and pubsub stops counting
|
|
// the subscriber as delivered.
|
|
if (!atomic_load_explicit(&callable->alive, memory_order_acquire)) {
|
|
return calogFail(result, calogErrDeadE, "callable owner no longer exists");
|
|
}
|
|
// The caller has been stopped (calogAbortAll, or this script's own calogAbortCurrent): a script
|
|
// function is script code, so refuse it for the same reason calogCall refuses a native -- a timer
|
|
// or subscriber callback already queued must not start running a script body afterwards.
|
|
if (callable->runtime != NULL && calogAborting(callable->runtime)) {
|
|
return calogFail(result, calogErrAbortE, CALOG_ABORT_MESSAGE);
|
|
}
|
|
// The owning runtime's actor layer, if present, marshals a foreign-thread invoke
|
|
// to the owner's thread; inline otherwise (a bare broker has no hook).
|
|
if (callable->runtime != NULL && callable->runtime->invokeHook != NULL) {
|
|
return callable->runtime->invokeHook(callable, args, argCount, result);
|
|
}
|
|
return callable->fn(args, argCount, result, callable->userData);
|
|
}
|
|
|
|
|
|
// Finalize a callable WITHOUT running the engine release, for a caller that is not on the owner's
|
|
// thread and could not marshal the release there (actorReleaseCallable's fallback). The handle
|
|
// INSIDE the interpreter is deliberately left behind -- it is pinned until that interpreter is
|
|
// destroyed, which for a long-lived context can be a long time, but leaking beats reaching into a
|
|
// live VM from the wrong thread.
|
|
//
|
|
// userData is freed on exactly the same condition as calogFnFinalize: only when a release hook
|
|
// exists. That hook is what makes userData calog's to free (an adapter's single heap block, per the
|
|
// CalogReleaseFnT contract). A host-owned callable from calogFnFromNative has NO hook and a userData
|
|
// the EMBEDDER owns -- freeing that would corrupt their heap, which is worse than anything this
|
|
// function exists to prevent. A callable already reclaimed has both cleared, so it frees the shell.
|
|
void calogFnFinalizeForeign(CalogFnT *callable) {
|
|
if (callable == NULL) {
|
|
return;
|
|
}
|
|
calogContextUntrackFn(callable->runtime, callable->ownerCtxId, callable);
|
|
if (callable->release != NULL && !atomic_exchange_explicit(&callable->reclaimed, true, memory_order_acq_rel)) {
|
|
free(callable->userData);
|
|
}
|
|
free(callable);
|
|
}
|
|
|
|
|
|
// Weak-to-strong upgrade: take a reference ONLY if this callable has not already dropped to zero.
|
|
// A plain calogFnRetain cannot be used to adopt a callable found in a list, because the count
|
|
// reaching zero is what COMMITS calogFnRelease to finalizing it -- retaining after that resurrects a
|
|
// corpse the in-flight finalize is about to free, and the second drop would free it twice. The CAS
|
|
// loop refuses exactly that case. The caller must hold the owner context's queueMutex, which is what
|
|
// keeps the shell itself alive to be inspected: every path that frees one (calogFnFinalize,
|
|
// calogFnFinalizeForeign) untracks first, and untrack needs that lock.
|
|
bool calogFnRetainIfLive(CalogFnT *callable) {
|
|
int32_t current;
|
|
|
|
current = atomic_load_explicit(&callable->refCount, memory_order_acquire);
|
|
while (current > 0) {
|
|
if (atomic_compare_exchange_weak_explicit(&callable->refCount, ¤t, current + 1,
|
|
memory_order_acq_rel, memory_order_acquire)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
void calogFnMarkDead(CalogFnT *callable) {
|
|
if (callable == NULL) {
|
|
return;
|
|
}
|
|
atomic_store_explicit(&callable->alive, false, memory_order_release);
|
|
}
|
|
|
|
|
|
uint64_t calogFnOwner(const CalogFnT *callable) {
|
|
return callable->ownerCtxId;
|
|
}
|
|
|
|
|
|
void calogFnRelease(CalogFnT *callable) {
|
|
int32_t previous;
|
|
|
|
if (callable == NULL) {
|
|
return;
|
|
}
|
|
previous = atomic_fetch_sub_explicit(&callable->refCount, 1, memory_order_acq_rel);
|
|
if (previous == 1) {
|
|
// This drop took the count to zero. The owning runtime's actor layer (if
|
|
// installed) routes the finalize to the owner's thread; otherwise inline.
|
|
if (callable->runtime != NULL && callable->runtime->releaseHook != NULL) {
|
|
callable->runtime->releaseHook(callable);
|
|
} else {
|
|
calogFnFinalize(callable);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void calogFnRetain(CalogFnT *callable) {
|
|
if (callable == NULL) {
|
|
return;
|
|
}
|
|
atomic_fetch_add_explicit(&callable->refCount, 1, memory_order_relaxed);
|
|
}
|
|
|
|
|
|
CalogT *calogFnRuntime(const CalogFnT *callable) {
|
|
return callable->runtime;
|
|
}
|
|
|
|
|
|
void *calogFnUserData(const CalogFnT *callable) {
|
|
return callable->userData;
|
|
}
|
|
|
|
|
|
int32_t calogGrow(void **buffer, int64_t *cap, int64_t needed, size_t elemSize) {
|
|
int64_t newCap;
|
|
void *resized;
|
|
|
|
if (*cap >= needed) {
|
|
return calogOkE;
|
|
}
|
|
newCap = (*cap == 0) ? BROKER_MIN_CAPACITY : *cap;
|
|
while (newCap < needed) {
|
|
if (newCap > INT64_MAX / CALOG_GROWTH_FACTOR) {
|
|
return calogErrOomE;
|
|
}
|
|
newCap *= CALOG_GROWTH_FACTOR;
|
|
}
|
|
if ((uint64_t)newCap > (uint64_t)(SIZE_MAX / elemSize)) {
|
|
return calogErrOomE;
|
|
}
|
|
resized = realloc(*buffer, (size_t)newCap * elemSize);
|
|
if (resized == NULL) {
|
|
return calogErrOomE;
|
|
}
|
|
*buffer = resized;
|
|
*cap = newCap;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
int32_t calogValueCopy(CalogValueT *dst, const CalogValueT *src) {
|
|
return valueCopyDepth(dst, src, 0);
|
|
}
|
|
|
|
|
|
static int32_t valueCopyDepth(CalogValueT *dst, const CalogValueT *src, int32_t depth) {
|
|
CalogAggT *aggregate;
|
|
int32_t status;
|
|
|
|
calogValueNil(dst);
|
|
switch (src->type) {
|
|
case calogNilE:
|
|
break;
|
|
case calogBoolE:
|
|
dst->as.b = src->as.b;
|
|
break;
|
|
case calogIntE:
|
|
dst->as.i = src->as.i;
|
|
break;
|
|
case calogRealE:
|
|
dst->as.r = src->as.r;
|
|
break;
|
|
case calogStringE:
|
|
return calogValueString(dst, src->as.s.bytes, src->as.s.length);
|
|
case calogAggE:
|
|
status = aggregateCopyDepth(&aggregate, src->as.agg, depth);
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
calogValueAgg(dst, aggregate);
|
|
return calogOkE;
|
|
case calogFnE:
|
|
calogFnRetain(src->as.fn);
|
|
dst->as.fn = src->as.fn;
|
|
break;
|
|
}
|
|
dst->type = src->type;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
// Equality used for map-key matching. Scalars and strings compare by value;
|
|
// aggregate and function values compare by pointer identity, so they are only
|
|
// useful as keys when the exact same handle is reused (a deep copy yields a new
|
|
// pointer). Real keys use IEEE semantics: NaN never matches and +0.0 == -0.0.
|
|
bool calogValueEquals(const CalogValueT *a, const CalogValueT *b) {
|
|
if (a->type != b->type) {
|
|
return false;
|
|
}
|
|
switch (a->type) {
|
|
case calogNilE:
|
|
return true;
|
|
case calogBoolE:
|
|
return a->as.b == b->as.b;
|
|
case calogIntE:
|
|
return a->as.i == b->as.i;
|
|
case calogRealE:
|
|
return a->as.r == b->as.r;
|
|
case calogStringE:
|
|
if (a->as.s.length != b->as.s.length) {
|
|
return false;
|
|
}
|
|
return memcmp(a->as.s.bytes, b->as.s.bytes, (size_t)a->as.s.length) == 0;
|
|
case calogAggE:
|
|
return a->as.agg == b->as.agg;
|
|
case calogFnE:
|
|
return a->as.fn == b->as.fn;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
void calogValueFree(CalogValueT *value) {
|
|
if (value == NULL) {
|
|
return;
|
|
}
|
|
switch (value->type) {
|
|
case calogNilE:
|
|
case calogBoolE:
|
|
case calogIntE:
|
|
case calogRealE:
|
|
break;
|
|
case calogStringE:
|
|
free(value->as.s.bytes);
|
|
break;
|
|
case calogAggE:
|
|
calogAggFree(value->as.agg);
|
|
break;
|
|
case calogFnE:
|
|
calogFnRelease(value->as.fn);
|
|
break;
|
|
}
|
|
calogValueNil(value);
|
|
}
|
|
|
|
|
|
void calogValueAgg(CalogValueT *value, CalogAggT *aggregate) {
|
|
value->type = calogAggE;
|
|
value->as.agg = aggregate;
|
|
}
|
|
|
|
|
|
void calogValueBool(CalogValueT *value, bool b) {
|
|
value->type = calogBoolE;
|
|
value->as.b = b;
|
|
}
|
|
|
|
|
|
void calogValueFn(CalogValueT *value, CalogFnT *callable) {
|
|
value->type = calogFnE;
|
|
value->as.fn = callable;
|
|
}
|
|
|
|
|
|
void calogValueFromDouble(CalogValueT *out, double number) {
|
|
if (isfinite(number) && number == floor(number) && number >= CALOG_INT64_MIN_DOUBLE && number < CALOG_INT64_MAX_DOUBLE) {
|
|
calogValueInt(out, (int64_t)number);
|
|
} else {
|
|
calogValueReal(out, number);
|
|
}
|
|
}
|
|
|
|
|
|
void calogValueInt(CalogValueT *value, int64_t i) {
|
|
value->type = calogIntE;
|
|
value->as.i = i;
|
|
}
|
|
|
|
|
|
void calogValueNil(CalogValueT *value) {
|
|
memset(value, 0, sizeof(*value));
|
|
value->type = calogNilE;
|
|
}
|
|
|
|
|
|
void calogValueReal(CalogValueT *value, double r) {
|
|
value->type = calogRealE;
|
|
value->as.r = r;
|
|
}
|
|
|
|
|
|
int32_t calogValueString(CalogValueT *value, const char *bytes, int64_t length) {
|
|
char *buffer;
|
|
|
|
if (length < 0) {
|
|
calogValueNil(value);
|
|
return calogErrRangeE;
|
|
}
|
|
if (bytes == NULL && length > 0) {
|
|
calogValueNil(value);
|
|
return calogErrArgE;
|
|
}
|
|
buffer = (char *)malloc((size_t)length + 1);
|
|
if (buffer == NULL) {
|
|
calogValueNil(value);
|
|
return calogErrOomE;
|
|
}
|
|
if (length > 0) {
|
|
memcpy(buffer, bytes, (size_t)length);
|
|
}
|
|
buffer[length] = '\0';
|
|
value->type = calogStringE;
|
|
value->as.s.bytes = buffer;
|
|
value->as.s.length = length;
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
void calogValueMove(CalogValueT *dst, CalogValueT *src) {
|
|
if (dst == src) {
|
|
return;
|
|
}
|
|
*dst = *src;
|
|
calogValueNil(src);
|
|
}
|
|
|
|
|
|
const char *calogTypeName(CalogTypeE type) {
|
|
switch (type) {
|
|
case calogNilE:
|
|
return "nil";
|
|
case calogBoolE:
|
|
return "bool";
|
|
case calogIntE:
|
|
return "int";
|
|
case calogRealE:
|
|
return "real";
|
|
case calogStringE:
|
|
return "string";
|
|
case calogAggE:
|
|
return "aggregate";
|
|
case calogFnE:
|
|
return "function";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
|
|
// ---- shared library helpers (see calogInternal.h) ----
|
|
|
|
|
|
int32_t calogMapSetBool(CalogAggT *map, const char *key, bool flag) {
|
|
CalogValueT keyValue;
|
|
CalogValueT boolValue;
|
|
int32_t status;
|
|
|
|
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
calogValueBool(&boolValue, flag);
|
|
status = calogAggSet(map, &keyValue, &boolValue);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&keyValue);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
|
|
int32_t calogMapSetInt(CalogAggT *map, const char *key, int64_t value) {
|
|
CalogValueT keyValue;
|
|
CalogValueT intValue;
|
|
int32_t status;
|
|
|
|
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
calogValueInt(&intValue, value);
|
|
status = calogAggSet(map, &keyValue, &intValue);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&keyValue);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
|
|
int32_t calogMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length) {
|
|
CalogValueT keyValue;
|
|
CalogValueT stringValue;
|
|
int32_t status;
|
|
|
|
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
|
|
if (status != calogOkE) {
|
|
return status;
|
|
}
|
|
status = calogValueString(&stringValue, bytes, length);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&keyValue);
|
|
return status;
|
|
}
|
|
status = calogAggSet(map, &keyValue, &stringValue);
|
|
if (status != calogOkE) {
|
|
calogValueFree(&keyValue);
|
|
calogValueFree(&stringValue);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
|
|
void calogRegistryRelease(pthread_mutex_t *initMutex, int32_t *refCount, void (*freeAll)(void)) {
|
|
pthread_mutex_lock(initMutex);
|
|
// freeAll runs exactly once, on the 1 -> 0 transition. A release at refcount 0 (e.g. an
|
|
// auto-shutdown after the caller already shut the library down by hand) is a safe no-op.
|
|
if (*refCount > 0) {
|
|
(*refCount)--;
|
|
if (*refCount == 0) {
|
|
freeAll();
|
|
}
|
|
}
|
|
pthread_mutex_unlock(initMutex);
|
|
}
|
|
|
|
|
|
void calogRegistryRetain(pthread_mutex_t *initMutex, int32_t *refCount) {
|
|
pthread_mutex_lock(initMutex);
|
|
(*refCount)++;
|
|
pthread_mutex_unlock(initMutex);
|
|
}
|