API more or less ready for use.

This commit is contained in:
Scott Duensing 2026-07-01 02:28:07 -05:00
parent 6fb3d0367c
commit 50fad18341
36 changed files with 21987 additions and 1030 deletions

49
LICENSE.md Normal file
View file

@ -0,0 +1,49 @@
# License
## calog
MIT License
Copyright (c) 2026 Scott Duensing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## Third-party components
calog vendors the following scripting engines under `vendor/`, built from
source. Each is distributed under its own license (all MIT), which applies to
its files; the full text ships alongside each engine's source.
- **Lua 5.4** — Copyright (C) 1994-2023 Lua.org, PUC-Rio. MIT License.
`vendor/lua/` (see `vendor/lua/README` and the notice in `src/lua.h`).
- **Duktape** (JavaScript) — Copyright (c) 2013-2019 by Duktape authors. MIT
License. `vendor/duktape/LICENSE.txt`.
- **Squirrel 3.2** — Copyright (c) 2003-2022 Alberto Demichelis. MIT License.
`vendor/squirrel-src/COPYRIGHT`.
- **MY-BASIC** — Copyright (C) 2011-2026 Tony Wang. MIT License. Notice in
`vendor/mybasic/myBasic.h`.
> Note: `vendor/mybasic/myBasic.c` carries one small calog patch (making the
> global allocation counter `_Atomic` so interpreters are thread-safe); the
> unmodified original is preserved as `vendor/mybasic/myBasic.c.orig`.

View file

@ -89,8 +89,8 @@ DUKFLAGS = -std=c99 -w -g -O1
DUKOBJ = obj/duktape.o
BINS = bin/testBroker bin/testLua bin/testMyBasic bin/testPolyglot bin/testActor \
bin/testEngineLua bin/testSquirrel bin/testEngineSquirrel bin/testJs bin/testEngineJs \
bin/embed
bin/testEngineLua bin/testEngineMyBasic bin/testSquirrel bin/testEngineSquirrel bin/testJs bin/testEngineJs \
bin/testLoad bin/embed
all: $(BINS)
@ -102,7 +102,7 @@ $(STRICTOBJ): obj/%.o: %.c | obj
$(CC) $(COREFLAGS) $(INC) -c -o $@ $<
# strict C, threaded
THREADOBJ = obj/context.o obj/testActor.o obj/testEngineLua.o obj/testEngineSquirrel.o obj/testEngineJs.o
THREADOBJ = obj/context.o obj/mybasicEngine.o obj/testActor.o obj/testEngineLua.o obj/testEngineSquirrel.o obj/testEngineJs.o obj/testEngineMyBasic.o
$(THREADOBJ): obj/%.o: %.c | obj
$(CC) $(COREFLAGS) $(INC) -pthread -c -o $@ $<
@ -150,7 +150,7 @@ obj/duktape.o: $(DUKDIR)/duktape.c | obj
# (static members are pulled only when referenced). See examples/embed.c.
CALOGLIB = obj/value.o obj/broker.o obj/context.o \
obj/luaAdapter.o obj/luaEngine.o obj/jsAdapter.o obj/jsEngine.o \
obj/squirrelAdapter.o obj/squirrelEngine.o obj/mybasicAdapter.o
obj/squirrelAdapter.o obj/squirrelEngine.o obj/mybasicAdapter.o obj/mybasicEngine.o
lib/libcalog.a: $(CALOGLIB) | lib
ar rcs $@ $^
@ -179,6 +179,9 @@ bin/testEngineLua: obj/testEngineLua.o lib/libcalog.a lib/liblua.a | bin
bin/testMyBasic: obj/testMyBasic.o lib/libcalog.a lib/libmybasic.a | bin
$(CC) $(LDFLAGS) -o $@ $^ -lm
bin/testEngineMyBasic: obj/testEngineMyBasic.o lib/libcalog.a lib/libmybasic.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ -lm
bin/testPolyglot: obj/testPolyglot.o lib/libcalog.a lib/liblua.a lib/libmybasic.a | bin
$(CC) $(LDFLAGS) -o $@ $^ $(LUALIBS)
@ -201,13 +204,22 @@ obj/embed.o: examples/embed.c src/calog.h | obj
bin/embed: obj/embed.o lib/libcalog.a lib/libduktape.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ -lm
# load test: links every engine (compiled with all CALOG_WITH_* engine flags) so it
# exercises calogContextLoad + calogRegisterBuiltinEngines across all four languages --
# including my-basic under the actor model. Uses only calog.h (the extern engine vtables).
obj/testLoad.o: tests/testLoad.c src/calog.h | obj
$(CC) $(COREFLAGS) -Isrc -pthread -DCALOG_WITH_LUA -DCALOG_WITH_JS -DCALOG_WITH_SQUIRREL -DCALOG_WITH_MYBASIC -c -o $@ $<
bin/testLoad: obj/testLoad.o lib/libcalog.a lib/liblua.a lib/libduktape.a lib/libsquirrel.a lib/libmybasic.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS) -lstdc++ -lm
obj bin lib:
mkdir -p $@
test: all
./bin/testBroker && ./bin/testLua && ./bin/testMyBasic && ./bin/testPolyglot && \
./bin/testActor && ./bin/testEngineLua && ./bin/testSquirrel && ./bin/testEngineSquirrel && \
./bin/testJs && ./bin/testEngineJs
./bin/testActor && ./bin/testEngineLua && ./bin/testEngineMyBasic && ./bin/testSquirrel && ./bin/testEngineSquirrel && \
./bin/testJs && ./bin/testEngineJs && ./bin/testLoad
# ThreadSanitizer build of the actor core and the Lua engine path (cannot combine
# with ASan). Recompiled from source under TSan; the vendored Lua objects are
@ -245,9 +257,21 @@ tsanjs: | bin obj
setarch -R ./bin/testEngineJsTsan
rm -f obj/duktape.tsan.o
# ThreadSanitizer build of the my-basic engine path: the vendored interpreter is
# recompiled under TSan (throwaway obj) so the engine's serialization of my-basic's
# process-global state (mb_init singletons, the _mb_allocated counter) is verified
# across the whole stack -- it races without the engine lock.
tsanmb: | bin obj
$(CC) $(MBFLAGS) -fsanitize=thread -c $(MBDIR)/myBasic.c -o obj/myBasic.tsan.o
$(CC) -std=c11 $(WARN) -g -O1 -fsanitize=thread -pthread $(INC) $(MBINC) -DMB_DOUBLE_FLOAT -o bin/testEngineMyBasicTsan \
tests/testEngineMyBasic.c src/mybasic/mybasicEngine.c src/mybasic/mybasicAdapter.c src/context.c src/value.c src/broker.c \
obj/myBasic.tsan.o -lm
setarch -R ./bin/testEngineMyBasicTsan
rm -f obj/myBasic.tsan.o
clean:
rm -rf obj bin lib
-include $(wildcard obj/*.d)
.PHONY: all test tsan tsansq tsanjs clean
.PHONY: all test tsan tsansq tsanjs tsanmb clean

374
README.md Normal file
View file

@ -0,0 +1,374 @@
# calog
**C: Any Language, One Gateway.**
calog embeds scripting into a C or C++ program. You register your native C functions
**once**, and they become callable from **any** embedded engine — Lua, JavaScript,
Squirrel, or MY-BASIC — with values passed through a single canonical type. Add a new
scripting language to your app by linking one more archive; your native functions don't
change.
```c
#include "calog.h"
CalogT *calog = calogCreate();
calogRegister(calog, "hostLog", hostLog, NULL); // your C function, once
CalogContextT *ctx = calogContextOpen(calog, &calogJsEngine);
calogContextEval(ctx, "hostLog('hello from JavaScript, ' + 6 * 7)");
```
Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`, or
`&calogMyBasicEngine` — nothing else changes.
---
## Why calog
- **One API, many languages.** Register a native once; call it from every engine.
`calog.h` is the entire public surface.
- **One value type.** Everything crosses the boundary as a `CalogValueT` — nil, bool,
int, real, binary-safe string, a hybrid list/map aggregate, or a script function
value. No per-engine glue in your code.
- **Natives run on your thread, serialized.** A script calling a native is marshalled to
the host thread and run there during `calogPump()`, one at a time — so your C code is
never called concurrently and needs **no locking**. (An opt-in escape hatch runs a
native inline on the script's thread when you want it.)
- **Fire-and-forget scripts.** Scripts run on their own context threads; the host stays
responsive and drives everything from its own loop.
- **Callbacks both ways.** A script can hand you a function value; you keep it and call
it later, and calog routes the call back to the engine that owns it.
- **Many runtimes.** Independent `CalogT` runtimes coexist in one process; one host
thread can drive several.
- **Load by filename.** `calogContextLoad(calog, "config")` finds `config.lua` /
`config.js` / `config.nut` / `config.bas` and runs it on the matching engine.
- **Reproducible & rigorous.** Every engine is vendored and built from source (no system
packages). The core is warning-clean on gcc **and** clang (`-Wall -Wextra -Werror
-Wconversion -Wsign-conversion`) and runs clean under AddressSanitizer, UBSan, and
ThreadSanitizer.
## Engines
| Engine | Vtable | Extension | Notes |
|---------------|------------------------|-----------|--------------------------------|
| Lua 5.4 | `calogLuaEngine` | `.lua` | |
| JavaScript | `calogJsEngine` | `.js` | Duktape |
| Squirrel 3.2 | `calogSquirrelEngine` | `.nut` | C++ VM |
| MY-BASIC | `calogMyBasicEngine` | `.bas` | interpreters serialize at load |
You can also bring your own: `CalogEngineT` is a public four-function vtable.
---
## Build
Requirements: a POSIX system (pthreads), `gcc` or `clang`, and GNU `make`. Developed on
Linux; macOS should work, Windows via WSL.
```sh
make # builds lib/libcalog.a, the per-engine archives, tests, and the example
make test # runs the full suite under AddressSanitizer + UBSan
```
The engines are vendored under `vendor/` and compiled from source, so the build is
self-contained.
### Linking your program
Link against `lib/libcalog.a` plus the archive for **each engine you actually use**
unused engines (and their vendored runtimes) stay out of your binary.
```sh
# a JavaScript-only host:
cc -Isrc myhost.c lib/libcalog.a lib/libduktape.a -pthread -lm -o myhost
```
Per-engine link needs:
| Engine | Archive | Extra libs (Linux) |
|----------|--------------------|----------------------|
| Lua | `lib/liblua.a` | `-ldl -lm` |
| JS | `lib/libduktape.a` | `-lm` |
| Squirrel | `lib/libsquirrel.a`| `-lstdc++ -lm` |
| MY-BASIC | `lib/libmybasic.a` | `-lm` |
Everything links with `-pthread`.
---
## A complete example
```c
#include "calog.h"
#include <stdio.h>
#include <time.h>
// A native the host provides. It runs on the host thread during calogPump, serialized
// with every other native, so it can touch host state without locking.
static int32_t hostLog(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "hostLog expects one string");
}
printf("[host] %s\n", args[0].as.s.bytes);
return calogOkE;
}
int main(void) {
CalogT *calog = calogCreate();
calogRegister(calog, "hostLog", hostLog, NULL);
CalogContextT *ctx = calogContextOpen(calog, &calogJsEngine);
calogContextEval(ctx, "hostLog('hello from JavaScript, ' + (6 * 7))");
// The script runs on its own thread and calls hostLog; the host services those
// calls by pumping in its own loop. (A real app pumps inside its main loop.)
struct timespec tick = { 0, 1000000 }; // 1 ms
for (int i = 0; i < 50; i++) {
calogPump(calog);
nanosleep(&tick, NULL);
}
calogContextClose(ctx);
calogDestroy(calog);
return 0;
}
```
See [`examples/embed.c`](examples/embed.c) for this, ready to build (`make` produces
`bin/embed`).
---
## How it works
### The canonical value
Every value crossing the C/script boundary is a `CalogValueT`:
```c
typedef enum { calogNilE, calogBoolE, calogIntE, calogRealE,
calogStringE, calogAggE, calogFnE } CalogTypeE;
struct CalogValueT {
CalogTypeE type;
union { bool b; int64_t i; double r;
CalogStringT s; CalogAggT *agg; CalogFnT *fn; } as;
};
```
Build them with the constructors, and free owned values you're done with:
```c
CalogValueT v;
calogValueInt(&v, 42);
calogValueString(&v, "binary\0safe", 11); // length-prefixed, NUL-safe
// ... calogValueBool / calogValueReal / calogValueAgg / calogValueFn ...
calogValueFree(&v);
```
`CalogAggT` is a hybrid **list + map** (`calogAggPush`, `calogAggSet`, `calogAggGet`),
so it round-trips onto engines that model sequences and dictionaries differently.
Strings are binary-safe (length-prefixed, always NUL-terminated for convenience).
### Registering natives
```c
// Runs on the HOST thread (serialized). This is what you want almost always.
calogRegister(calog, "add", add, NULL);
// Escape hatch: runs INLINE on the calling script's thread. Faster (no host hop),
// but YOU guarantee it is thread-safe.
calogRegisterInline(calog, "clockNow", clockNow, NULL);
```
A native has one signature and reports failure through `result`:
```c
static int32_t add(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "add expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
```
Every registered native is automatically visible in every context you open.
### Contexts, scripts, and the pump
A **context** is one engine instance on its own thread. `calogContextEval` is
**fire-and-forget**: it queues the script and returns immediately.
```c
CalogContextT *ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx, "hostLog('running')"); // returns now; script runs on ctx's thread
```
Because natives are serviced on the host thread, the host must **pump**:
```c
for (;;) {
calogPump(calog); // runs any pending native calls here, then returns
do_other_host_work();
}
```
Script results come back by **calling natives**`calogContextEval` doesn't return a
value. Script errors are delivered to an optional handler on the host thread during
`calogPump`:
```c
static void onError(uint64_t contextId, const char *message, void *userData) {
fprintf(stderr, "script %llu failed: %s\n", (unsigned long long)contextId, message);
}
calogSetErrorHandler(calog, onError, NULL); // default logs to stderr
```
### Callbacks: script functions as values
A script can pass you a function. Keep it (retain), call it later, release it when done —
calog runs the call on the engine that owns it and returns the result to you:
```c
static CalogFnT *savedCb = NULL;
static int32_t onEvent(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogFnE) {
return calogFail(result, calogErrArgE, "onEvent expects a function");
}
calogFnRetain(args[0].as.fn); // keep it beyond this call
savedCb = args[0].as.fn;
return calogOkE;
}
// ...later, from the host:
CalogValueT arg, out;
calogValueInt(&arg, 42);
calogFnInvoke(savedCb, &arg, 1, &out); // routed to the script's thread, result returned here
calogValueFree(&out);
calogValueFree(&arg);
calogFnRelease(savedCb);
```
### Loading a script by filename
Register the engines you want searchable (order is priority), then load by base name:
```c
calogRegisterEngine(calog, &calogLuaEngine);
calogRegisterEngine(calog, &calogJsEngine);
CalogContextT *ctx = calogContextLoad(calog, "config"); // tries config.lua, then config.js
```
Or let the build decide which engines exist. Compile the calling file with
`-DCALOG_WITH_LUA -DCALOG_WITH_JS …` and call the header-inline helper — it registers
exactly the engines you linked, and references no others:
```c
calogRegisterBuiltinEngines(calog);
CalogContextT *ctx = calogContextLoad(calog, "config");
```
### Multiple runtimes
A `CalogT` is a self-contained runtime; you can create several in one process. Each is
driven by a host thread, and one thread may drive several by pumping each in turn. Keep
values and callbacks within a single runtime — they don't cross between runtimes.
---
## API at a glance
```c
// runtime
CalogT *calogCreate(void);
void calogDestroy(CalogT *);
void calogPump(CalogT *); // service native calls here
void calogSetErrorHandler(CalogT *, CalogErrorFnT, void *);
// natives
int32_t calogRegister(CalogT *, const char *name, CalogNativeFnT, void *userData); // host thread
int32_t calogRegisterInline(CalogT *, const char *name, CalogNativeFnT, void *userData); // caller thread
int32_t calogCall(CalogT *, const char *name, CalogValueT *args, int32_t n, CalogValueT *result);
int32_t calogFail(CalogValueT *result, int32_t status, const char *message);
const char *calogTypeName(CalogTypeE);
// values
void calogValueNil/Bool/Int/Real/Agg/Fn(...);
int32_t calogValueString(CalogValueT *, const char *bytes, int64_t length);
int32_t calogValueCopy(CalogValueT *dst, const CalogValueT *src);
void calogValueMove(CalogValueT *dst, CalogValueT *src);
void calogValueFree(CalogValueT *);
bool calogValueEquals(const CalogValueT *, const CalogValueT *);
// aggregates (hybrid list + map)
int32_t calogAggCreate(CalogAggT **, CalogKindE);
void calogAggFree(CalogAggT *);
int32_t calogAggPush(CalogAggT *, CalogValueT *value);
int32_t calogAggSet(CalogAggT *, CalogValueT *key, CalogValueT *value);
CalogValueT *calogAggGet(CalogAggT *, const CalogValueT *key);
// function values (script callbacks)
int32_t calogFnInvoke(CalogFnT *, CalogValueT *args, int32_t n, CalogValueT *result);
void calogFnRetain(CalogFnT *);
void calogFnRelease(CalogFnT *);
// contexts + engines
CalogContextT *calogContextOpen(CalogT *, const CalogEngineT *);
void calogRegisterEngine(CalogT *, const CalogEngineT *);
CalogContextT *calogContextLoad(CalogT *, const char *baseFileName);
int32_t calogContextEval(CalogContextT *, const char *source); // fire-and-forget
void calogContextClose(CalogContextT *);
uint64_t calogContextId(const CalogContextT *);
uint64_t calogCurrentId(void); // 0 on the host thread
CalogT *calogCurrent(void);
```
Statuses are `CalogStatusE` (`calogOkE`, `calogErrArgE`, `calogErrOomE`, …); `calogOkE`
is 0.
---
## Testing
```sh
make test # full suite under AddressSanitizer + UBSan
make tsan # ThreadSanitizer: actor core + Lua
make tsansq # ThreadSanitizer: Squirrel
make tsanjs # ThreadSanitizer: JavaScript
make tsanmb # ThreadSanitizer: MY-BASIC
```
The ThreadSanitizer targets run under `setarch -R` (ASLR off) so TSan's shadow allocator
is happy on all kernels.
## Project layout
```
src/ core (broker, values, actor) + one subdir per engine (lua, js, squirrel, mybasic)
calog.h the entire public API
tests/ the test programs
examples/ embed.c — a minimal host
vendor/ vendored engine sources, built from source
design.md the full design rationale and internals
```
## Design
[`design.md`](design.md) is the long-form record: the canonical value model, the
actor/threading design, the function-value lifecycle, per-engine notes, and the
build system.
## License
calog is released under the [MIT License](LICENSE.md). The vendored engines (Lua,
Duktape, Squirrel, MY-BASIC) each retain their own MIT licenses — see
[`LICENSE.md`](LICENSE.md) for the third-party notices.

117
design.md
View file

@ -627,7 +627,122 @@ dependencies automatically. `make clean` removes `obj/` and `bin/`. `make test`
runs all ten binaries; `make tsan`/`make tsansq`/`make tsanjs` are the ThreadSanitizer
variants.
## 15. Public embedding API (`calog.h`) -- as-built
## 16. Threading model rewrite -- host-thread natives, fire-and-forget scripts
Supersedes the earlier "natives run inline on the calling context thread" model. The
host's own thread is now an implicit **host context (id 0)**: it has a queue but no OS
thread of its own, and the host drives it by calling **`calogPump`** in its loop.
- **Scripts are fire-and-forget.** `calogContextEval(ctx, src)` enqueues the script
onto the context's thread and returns a status (not the result); the script runs
asynchronously, and results come back by calling natives.
- **A registered native runs on the host thread, serialized.** A script calling one
posts a CALL onto the host queue and parks; the host runs it during `calogPump`. So
host C code is never called concurrently and needs no locking. `actorRoute` inlines a
call already on the host thread; otherwise it marshals to the host context (id 0).
- **`calogRegisterInline`** is the escape hatch: the registry entry's `runInline` flag
(which replaced `ownerCtxId`) makes the native run on the calling script's thread.
- **Errors** from a fire-and-forget script are posted to the host queue and delivered
to the `CalogErrorFnT` handler (`calogSetErrorHandler`) during `calogPump` (default:
log to stderr).
- **Function values** (`CalogFnT`) still run on their owning engine's thread -- sec 10
routing is unchanged, and `calogFnInvoke` from the host blocks-and-pumps the host
queue while it waits (the same nested pump, now applied to the host context).
- **Nested eval is allowed:** a new eval that arrives while a context is mid-script
(parked on a native call) runs nested via `pumpUntil` -- consistent with the sec-6.1
re-entrancy contract (interpreters support nested `pcall`/`peval`).
API shape: `calogRegister(c,name,fn,ud)` / `calogRegisterInline(...)`;
`calogContextOpen(c,engine) -> CalogContextT*` (create+start merged, since nothing is
registered between them anymore) and `calogContextClose`; `calogContextEval(ctx,src)`
fire-and-forget; `calogPump`; `calogSetErrorHandler`. `CalogConfigT` and the
`createInterpreter` config parameter are gone -- a context now exposes *every*
registered native (the engine binding walks the registry via the internal
`calogForEach`). Tests rewritten to drive calog the host way (register, open, eval,
pump-until-a-native-records-the-result); `testActor` is now engine-free, exercising the
dispatch machinery with C callables (`calogFnCreate`) on synthetic contexts. Verified:
`make test` 441 checks across 11 binaries (incl. `examples/embed.c`), gcc + clang
strict, ASan/UBSan + TSan clean (`make tsan`/`tsansq`/`tsanjs`).
**`CalogT` owns its contexts; ids are unbounded.** The active-context registry moved
from `context.c` file-static globals *into* `struct CalogT` (now defined in
`calogInternal.h`): a runtime owns both its native-function registry and its
active-context registry (`ctxMutex`, `ctxSlots`, freelist). `context.c` reaches it via
one `runtime` pointer set in `calogActorInit` (which also refuses a second runtime).
So `calogDestroy` closes every still-open context automatically -- the host need not
track them (a test opens 32 and never closes them; ASan confirms no leak). Context ids
widened to **`uint64`** (32-bit slot index + 32-bit generation), so neither the live
count nor open/close churn hits a preset ceiling; `calogContextId`/`calogCurrentId`
return `uint64_t`. The now-dead `ownerGen` parameter was dropped from `calogFnCreate`
(generation lives in the packed id). Re-verified: `make test` 473 checks, ASan no
leaks, TSan clean, gcc + clang strict.
**Independent runtimes in one process.** The one-runtime limit was not fundamental --
just process-global state that hadn't moved into `CalogT`. All of it now has: the host
context, the routing hooks (`routeHook`/`invokeHook`/`releaseHook`), and the error sink
are `CalogT` fields; a `CalogFnT` carries a `runtime` pointer so `calogFnInvoke`/release
reach the right hooks (the callable path has no `CalogT` otherwise). The dispatch
reaches its runtime through the object it already holds -- the route hook is handed its
`calog`, the callable hooks read `calogFnRuntime`, context-thread code uses
`context->broker`, and the rest take an explicit `calog` argument. The **only** remaining
process global is `currentContext`, and it is thread-local (it names the calling
thread's context). So the setters (`calogSetRouteHook` etc.) are gone -- `calogActorInit`
assigns the fields directly -- and the `runtime` static that the earlier review flagged
is deleted. Runtimes are isolated: don't pass a value or callable between them (a
cross-runtime reply cannot route). A test spawns N threads, each creating, driving, and
destroying its own runtime concurrently.
**One thread may host several runtimes.** `calogPump(calog)` sets `currentContext` to
`calog`'s host context for the drain and restores it after, so a single thread can drive
many runtimes by pumping each in turn -- a native serviced during `calogPump(A)` sees
`calogCurrent() == A` even if the thread also hosts B. Two consequences fall out and are
handled: context ids number from 1 in every runtime, so the "already on the owner's
thread" (inline) and "caller can take the token/pump path" (reply) decisions match the
*runtime* too, not the id alone -- a foreign or wrong-runtime caller takes a reply box,
which cannot misroute. A test creates two runtimes on one thread, runs a script in each,
and pumps both in a loop, asserting each runtime's native resolved `calogCurrent()` to
its own runtime (it fails if the pump doesn't rebind `currentContext`). Re-verified:
`make test` 480 checks, ASan no leaks, TSan clean (both concurrent runtimes and one
thread pumping two), gcc + clang strict.
**Loading a script by filename.** Each engine carries a NULL-terminated `extensions`
list (`{"lua"}` / `{"js"}` / `{"nut"}` / `{"bas"}`), and a host makes engines available
for filename-based loading with `calogRegisterEngine(calog, &engine)`.
`calogContextLoad(calog, base)` then walks the registered engines in registration order
(each engine's extensions in order), forms `"<base>.<ext>"`, and the first one that
`fopen`s wins: it reads the file on the calling thread, opens a context on that engine,
and loads the contents fire-and-forget -- returning the context (NULL if nothing matched
or the load failed). Registration matters for more than search order: hardcoding the
built-in engine vtables in the core would force-link *all* of them (and their vendored
runtimes) into every binary, defeating the per-engine archives -- so the host opts in,
and a binary that never references an engine pulls in none (`testActor` stays
engine-free). Engine selection is fundamentally a build-time (link) choice, so
`calogRegisterBuiltinEngines` (a header-inline in `calog.h`) registers exactly the
engines whose `CALOG_WITH_<ENGINE>` macro is set -- the host defines those alongside the
archives it links, and the inline emits nothing (references no engine) unless called, so
it never force-links.
**my-basic as an actor engine.** Making my-basic loadable meant running it under the
actor model for the first time, which exposed two things. (1) Its native dispatch called
the C function *directly* instead of through `calogCall`, so natives ran on the my-basic
context thread rather than marshalling to the host -- fixed by routing `mbDispatch`
through `calogCall` (the binding now stores the registry name), matching the other
engines. (2) my-basic keeps process-global state -- lazy `mb_init` singletons and a
global `_mb_allocated` counter touched on every allocation (forced on in the vendored
header) -- so two my-basic contexts on different threads race (TSan-confirmed). The
singletons are built once by `mb_init` and read-only thereafter, so the only
execution-time shared write is that counter; a one-line vendored patch makes it
`_Atomic` (the original is preserved as `vendor/mybasic/myBasic.c.orig`). With the
counter safe, the my-basic *engine* (not the adapter, which stays usable single-threaded
and lock-free) needs a lock only across *lifecycle* -- `mb_init`'s first-context build,
`mb_dispose`'s last-context teardown, and the shared context refcount -- and NOT across
`runSource`, so several my-basic scripts execute concurrently. A `tsanmb` target proves
the parallel case is race-free (verified further by a 4-context stress running
arithmetic, strings, lists, and booleans). Verified: `make test` 494 checks (13
binaries), ASan no leaks, TSan clean on all four engines
(`tsan`/`tsansq`/`tsanjs`/`tsanmb`), gcc + clang strict.
## 15. Public embedding API (`calog.h`) -- as-built (superseded by sec 16 for threading/API)
calog is packaged as an embedding library: a host links it, registers its own native
C functions, creates script contexts on an engine, and runs scripts. Every public

View file

@ -1,15 +1,20 @@
// examples/embed.c -- add scripting to a C program with calog, in ~30 lines.
// examples/embed.c -- add scripting to a C program with calog, in ~40 lines.
//
// The entire embedding surface is calog.h: create a runtime, register a native C
// function the host provides, create a script context on an engine that exposes it,
// and run a script that calls back into the host. Swap calogJsEngine for
// calogLuaEngine / calogSquirrelEngine to change languages -- nothing else changes.
// function the host provides, open a script context on an engine, fire-and-forget a
// script, and calogPump on the host thread to service the script's calls back into
// the host. Swap calogJsEngine for calogLuaEngine / calogSquirrelEngine to change
// languages -- nothing else changes.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include <stdio.h>
#include <time.h>
// A host-provided native, callable from any embedded engine: hostLog(message).
// A host-provided native, callable from any embedded engine. Runs on the host thread
// (this one), during calogPump -- so it can touch host state without locking.
static int32_t hostLog(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
@ -24,19 +29,23 @@ static int32_t hostLog(CalogValueT *args, int32_t argCount, CalogValueT *result,
int main(void) {
CalogT *calog;
CalogContextT *ctx;
CalogValueT result;
const char *expose[] = { "hostLog" };
CalogConfigT config = { expose, 1 };
struct timespec tick = { 0, 1000000 }; // 1 ms
int i;
calog = calogCreate();
calogRegister(calog, "hostLog", hostLog, NULL, 0);
calogRegister(calog, "hostLog", hostLog, NULL);
calogContextCreate(calog, &calogJsEngine, &config, &ctx);
calogContextStart(ctx);
calogContextEval(ctx, "hostLog('hello from JavaScript, ' + (6 * 7))", &result);
calogValueFree(&result);
ctx = calogContextOpen(calog, &calogJsEngine);
calogContextEval(ctx, "hostLog('hello from JavaScript, ' + (6 * 7))");
calogContextDestroy(ctx);
// A real host pumps calog inside its own main loop; here we tick briefly so the
// fire-and-forget script gets to run and call hostLog on this thread.
for (i = 0; i < 50; i++) {
calogPump(calog);
nanosleep(&tick, NULL);
}
calogContextClose(ctx);
calogDestroy(calog);
return 0;
}

View file

@ -25,17 +25,10 @@
_Static_assert((BROKER_INITIAL_SLOTS & (BROKER_INITIAL_SLOTS - 1)) == 0, "registry slot count must be a power of two");
_Static_assert((CALOG_GROWTH_FACTOR & (CALOG_GROWTH_FACTOR - 1)) == 0, "registry growth factor must be a power of two");
struct CalogT {
CalogEntryT *slots;
int64_t slotCount;
int64_t entryCount;
};
static CalogRouteFnT routeHook = NULL;
static int32_t brokerGrow(CalogT *broker);
static uint64_t hashName(const char *name);
static CalogEntryT *probeSlot(CalogT *broker, const char *name);
static int32_t registerEntry(CalogT *broker, const char *name, CalogNativeFnT fn, void *userData, bool runInline);
int32_t calogCall(CalogT *broker, const char *name, CalogValueT *args, int32_t argCount, CalogValueT *result) {
@ -46,8 +39,8 @@ int32_t calogCall(CalogT *broker, const char *name, CalogValueT *args, int32_t a
if (entry == NULL) {
return calogFail(result, calogErrNotFoundE, "no such function");
}
if (routeHook != NULL) {
return routeHook(entry, args, argCount, result);
if (broker->routeHook != NULL) {
return broker->routeHook(broker, entry, args, argCount, result);
}
return entry->fn(args, argCount, result, entry->userData);
}
@ -81,6 +74,7 @@ void calogBrokerDestroy(CalogT *broker) {
free(broker->slots[index].name);
}
free(broker->slots);
free(broker->engines);
free(broker);
}
@ -97,6 +91,17 @@ int32_t calogFail(CalogValueT *result, int32_t status, const char *message) {
}
void calogForEach(CalogT *broker, void (*visit)(const CalogEntryT *entry, void *ud), void *ud) {
int64_t index;
for (index = 0; index < broker->slotCount; index++) {
if (broker->slots[index].name != NULL) {
visit(&broker->slots[index], ud);
}
}
}
static int32_t brokerGrow(CalogT *broker) {
CalogEntryT *oldSlots;
CalogEntryT *newSlots;
@ -137,18 +142,28 @@ CalogEntryT *calogLookup(CalogT *broker, const char *name) {
}
int32_t calogRegister(CalogT *broker, const char *name, CalogNativeFnT fn, void *userData, uint32_t ownerCtxId) {
int32_t calogRegister(CalogT *broker, const char *name, CalogNativeFnT fn, void *userData) {
return registerEntry(broker, name, fn, userData, false);
}
int32_t calogRegisterInline(CalogT *broker, const char *name, CalogNativeFnT fn, void *userData) {
return registerEntry(broker, name, fn, userData, true);
}
static int32_t registerEntry(CalogT *broker, const char *name, CalogNativeFnT fn, void *userData, bool runInline) {
CalogEntryT *slot;
char *nameCopy;
int32_t status;
slot = probeSlot(broker, name);
if (slot->name != NULL) {
// Re-registration replaces the existing binding in place: allocation
// free, so it never grows and never fails.
// Re-registration replaces the existing binding in place: allocation free,
// so it never grows and never fails.
slot->fn = fn;
slot->userData = userData;
slot->ownerCtxId = ownerCtxId;
slot->runInline = runInline;
return calogOkE;
}
if ((broker->entryCount + 1) * BROKER_PERCENT_SCALE >= broker->slotCount * BROKER_LOAD_PERCENT) {
@ -165,17 +180,12 @@ int32_t calogRegister(CalogT *broker, const char *name, CalogNativeFnT fn, void
slot->name = nameCopy;
slot->fn = fn;
slot->userData = userData;
slot->ownerCtxId = ownerCtxId;
slot->runInline = runInline;
broker->entryCount++;
return calogOkE;
}
void calogSetRouteHook(CalogRouteFnT hook) {
routeHook = hook;
}
static uint64_t hashName(const char *name) {
uint64_t hash;
size_t index;

View file

@ -1,13 +1,23 @@
// 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/Duktape, Squirrel, my-basic), passing values
// by a single canonical type. This header is the entire public surface -- create a
// runtime, register natives, create script contexts on an engine, run scripts, and
// exchange values. Internal machinery lives in calogInternal.h and is not needed to
// embed calog. One runtime per process (the actor/registry state is process-global).
// calog is a script broker: register native C functions once and call them from any
// embedded engine (Lua, JavaScript/Duktape, 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.
//
// See design.md for the full architecture.
// 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
@ -17,13 +27,12 @@
#include <stdint.h>
// Maximum nesting depth honored by the recursive copy/marshal paths. A deeper
// structure, or a cycle, fails with calogErrDepthE instead of recursing without
// bound; the free path relies on the same invariant (aggregates must be acyclic).
// 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 (aggregate storage, registry table). Must
// stay a power of two: the registry derives its probe mask from a power-of-two slot
// count.
// 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 {
@ -48,8 +57,8 @@ typedef enum CalogTypeE {
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.
// 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,
@ -65,8 +74,7 @@ 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
// NUL bytes.
// bytes[length] so plain C consumers can read it, while length permits embedded NULs.
struct CalogStringT {
char *bytes;
int64_t length;
@ -90,8 +98,7 @@ struct CalogPairT {
};
// Hybrid container: the sequence part lives in array[0, arrayCount), the keyed part
// in pairs[0, pairCount). A Lua/JS table fills both as needed; a my-basic LIST maps
// to array, a DICT to pairs. Only scalar and string keys are meaningful (see
// 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;
@ -103,37 +110,38 @@ struct CalogAggT {
int64_t pairCap;
};
// The uniform native-function signature. Write a function once against this and
// register it once; every engine can then call it. result is nil on entry; on
// failure write a message into result with calogFail and return a non-zero
// CalogStatusE.
// 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 receives the config passed to calogContextCreate; a NULL
// engine makes a synthetic context with no interpreter. Embedders may supply a custom
// engine, but normally pass one of the built-in vtables below.
// 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;
int32_t (*createInterpreter)(CalogContextT *context, void *config, void **interpOut);
// 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;
// Config for the built-in engine vtables: the broker natives to expose into the new
// interpreter (installed on the context's own thread). May be passed as NULL.
typedef struct CalogConfigT {
const char *const *exposeNames;
int32_t exposeCount;
} CalogConfigT;
// ---- runtime ----
CalogT *calogCreate(void);
void calogDestroy(CalogT *calog);
int32_t calogRegister(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData, uint32_t ownerCtxId);
int32_t calogCall(CalogT *calog, const char *name, CalogValueT *args, int32_t argCount, CalogValueT *result);
void calogPump(CalogT *calog); // run pending script->native calls on THIS thread
void calogSetErrorHandler(CalogT *calog, CalogErrorFnT fn, void *userData);
// ---- writing natives ----
// ---- 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);
@ -163,16 +171,46 @@ void calogFnRetain(CalogFnT *fn);
void calogFnRelease(CalogFnT *fn);
// ---- contexts + engines ----
int32_t calogContextCreate(CalogT *calog, const CalogEngineT *engine, void *config, CalogContextT **out);
int32_t calogContextStart(CalogContextT *context);
int32_t calogContextEval(CalogContextT *context, const char *source, CalogValueT *result);
void calogContextDestroy(CalogContextT *context);
uint32_t calogContextId(const CalogContextT *context);
uint32_t calogCurrentId(void); // context id of the calling thread (0 if none)
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;
// 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
(void)calog; // no-op if the build selected no engines
}
#endif

View file

@ -1,58 +1,100 @@
// calogInternal.h -- internal API shared across calog's own translation units.
//
// This is NOT part of the embedding surface (that is calog.h). It exposes the
// registry entry type, the routing/lifecycle hooks the actor layer installs, the
// low-level callable lifecycle the engine adapters drive, and the split
// registry/actor init used internally. Host programs include calog.h only.
// NOT part of the embedding surface (that is calog.h). Host programs include calog.h
// only.
#ifndef CALOG_INTERNAL_H
#define CALOG_INTERNAL_H
#include "calog.h"
// A registry entry, resolved by name. ownerCtxId 0 means thread-agnostic.
#include <pthread.h>
// A registry entry, resolved by name. runInline true means the native runs on the
// calling thread (calogRegisterInline); false means it runs on the host thread.
struct CalogEntryT {
char *name;
CalogNativeFnT fn;
void *userData;
uint32_t ownerCtxId;
bool runInline;
};
typedef struct CalogEntryT CalogEntryT;
// One active-context registry slot. context is NULL when free; generation is bumped
// on reuse so a stale id (of a closed + recycled context) resolves dead, never
// misrouting to the recycler.
typedef struct CalogRegistrySlotT {
CalogContextT *context;
uint32_t generation;
} CalogRegistrySlotT;
// Invoked when a CalogFnT's last reference drops, so the owning engine can release
// its closure. The broker frees the CalogFnT shell after this returns; reach the
// closure handle with calogFnUserData.
typedef void (*CalogReleaseFnT)(CalogFnT *fn);
// Hooks installed by the actor layer (calogActorInit) so calogCall, calogFnInvoke,
// and the final calogFnRelease marshal to the owning context's thread instead of
// running inline. NULL by default (single-threaded use runs inline). See design.md
// sec 6/10. NB: the registry must be frozen before any context starts -- calogCall
// reads it locklessly from context threads, and calogRegister may grow (and free)
// the slot table.
typedef int32_t (*CalogRouteFnT)(CalogEntryT *entry, CalogValueT *args, int32_t argCount, CalogValueT *result);
// Hooks the actor layer installs on the runtime (calogActorInit stores them in CalogT)
// so calogCall, calogFnInvoke, and the final calogFnRelease marshal to the owning
// thread instead of running inline. NULL on a bare broker (single-threaded use runs
// inline). The route hook is handed its runtime; the callable hooks reach it through
// the CalogFnT (calogFnRuntime). See design.md sec 6/10.
typedef int32_t (*CalogRouteFnT)(CalogT *calog, CalogEntryT *entry, CalogValueT *args, int32_t argCount, CalogValueT *result);
typedef int32_t (*CalogInvokeHookT)(CalogFnT *fn, CalogValueT *args, int32_t argCount, CalogValueT *result);
typedef void (*CalogReleaseHookT)(CalogFnT *fn);
// The runtime. Owns the native-function registry AND the active-context registry, so
// a host's contexts are its property -- calogDestroy closes every open one. Both
// registries grow dynamically with no preset limit (context ids are 64-bit: a 32-bit
// slot index + 32-bit generation, so neither the live count nor open/close churn is
// capped in practice). All per-runtime actor state (host context, routing hooks, error
// sink) lives here too, so independent CalogT runtimes coexist in one process; each is
// created and pumped by its own host thread.
struct CalogT {
// native-function registry (broker.c)
CalogEntryT *slots;
int64_t slotCount;
int64_t entryCount;
// active-context registry (context.c), guarded by ctxMutex
pthread_mutex_t ctxMutex;
CalogRegistrySlotT *ctxSlots;
int64_t ctxCount; // high-water slot count
int64_t ctxCap;
int64_t *ctxFree; // recycled slot indices
int64_t ctxFreeCount;
int64_t ctxFreeCap;
// actor layer (context.c): installed by calogActorInit, all NULL on a bare broker
CalogContextT *hostContext; // id 0, no thread; driven by calogPump
CalogRouteFnT routeHook;
CalogInvokeHookT invokeHook;
CalogReleaseHookT releaseHook;
CalogErrorFnT errorHandler;
void *errorUserData;
// engines available to calogContextLoad (calogRegisterEngine), search-priority order
const CalogEngineT **engines;
int64_t engineCount;
int64_t engineCap;
};
// ---- registry (calogCreate/calogDestroy compose these with the actor layer) ----
CalogT *calogBrokerCreate(void);
void calogBrokerDestroy(CalogT *calog);
CalogEntryT *calogLookup(CalogT *calog, const char *name);
void calogSetRouteHook(CalogRouteFnT hook);
// Visit every registered entry (used by createInterpreter to expose all natives).
// Safe only while the registry is frozen -- i.e. before any context starts.
void calogForEach(CalogT *calog, void (*visit)(const CalogEntryT *entry, void *ud), void *ud);
// ---- callable lifecycle (driven by the engine adapters) ----
int32_t calogFnCreate(CalogFnT **out, CalogNativeFnT fn, void *userData, CalogReleaseFnT release, uint32_t ownerCtxId, uint32_t ownerGen);
int32_t calogFnCreate(CalogFnT **out, CalogT *runtime, CalogNativeFnT fn, void *userData, CalogReleaseFnT release, uint64_t ownerCtxId);
void calogFnFinalize(CalogFnT *fn);
CalogNativeFnT calogFnNative(const CalogFnT *fn);
void calogFnMarkDead(CalogFnT *fn);
uint32_t calogFnOwner(const CalogFnT *fn);
uint64_t calogFnOwner(const CalogFnT *fn);
CalogT *calogFnRuntime(const CalogFnT *fn);
void *calogFnUserData(const CalogFnT *fn);
void calogSetInvokeHook(CalogInvokeHookT hook);
void calogSetReleaseHook(CalogReleaseHookT hook);
// ---- actor layer (composed into calogCreate/calogDestroy) ----
int32_t calogActorInit(CalogT *calog);
void calogActorShutdown(void);
void calogActorShutdown(CalogT *calog);
CalogT *calogContextBroker(const CalogContextT *context);
void *calogContextInterp(CalogContextT *context);

File diff suppressed because it is too large Load diff

View file

@ -54,7 +54,7 @@ typedef struct JsExportT {
struct CalogJsT {
duk_context *ctx;
CalogT *broker;
uint32_t ctxId;
uint64_t ctxId;
BindingT **bindings;
int32_t bindingCount;
int32_t bindingCap;
@ -118,7 +118,7 @@ static void jsCallableRelease(CalogFnT *callable) {
}
int32_t calogJsCreate(CalogJsT **out, CalogT *broker, uint32_t ctxId) {
int32_t calogJsCreate(CalogJsT **out, CalogT *broker, uint64_t ctxId) {
CalogJsT *context;
duk_context *ctx;
@ -231,7 +231,7 @@ static int32_t jsExportAt(CalogJsT *context, duk_idx_t idx, CalogFnT **out) {
export->context = context;
export->heapPtr = heapPtr;
export->ref = ref;
status = calogFnCreate(out, jsCallableInvoke, export, jsCallableRelease, context->ctxId, 0);
status = calogFnCreate(out, context->broker, jsCallableInvoke, export, jsCallableRelease, context->ctxId);
if (status != calogOkE) {
duk_push_global_stash(ctx);
duk_get_prop_string(ctx, -1, JS_EXPORTS_KEY);

View file

@ -17,7 +17,7 @@
typedef struct CalogJsT CalogJsT;
int32_t calogJsCreate(CalogJsT **out, CalogT *broker, uint32_t ctxId);
int32_t calogJsCreate(CalogJsT **out, CalogT *broker, uint64_t ctxId);
void calogJsDestroy(CalogJsT *context);
int32_t calogJsExport(CalogJsT *context, const char *globalName, CalogFnT **out);
int32_t calogJsExpose(CalogJsT *context, const char *name);

View file

@ -8,19 +8,23 @@
#include "calogInternal.h"
#include "jsAdapter.h"
static int32_t jsEngineCreate(CalogContextT *context, void *config, void **interpOut);
static int32_t jsEngineCreate(CalogContextT *context, void **interpOut);
static void jsEngineDestroy(void *interp);
static int32_t jsEngineRun(void *interp, const char *source, CalogValueT *result);
static void jsExposeVisitor(const CalogEntryT *entry, void *ud);
static const char *const jsExtensions[] = { "js", NULL };
const CalogEngineT calogJsEngine = {
"javascript",
jsExtensions,
jsEngineCreate,
jsEngineDestroy,
jsEngineRun
};
static int32_t jsEngineCreate(CalogContextT *context, void *config, void **interpOut) {
static int32_t jsEngineCreate(CalogContextT *context, void **interpOut) {
CalogJsT *jc;
int32_t status;
@ -29,23 +33,17 @@ static int32_t jsEngineCreate(CalogContextT *context, void *config, void **inter
if (status != calogOkE) {
return status;
}
if (config != NULL) {
CalogConfigT *cfg;
int32_t index;
cfg = (CalogConfigT *)config;
for (index = 0; index < cfg->exposeCount; index++) {
status = calogJsExpose(jc, cfg->exposeNames[index]);
if (status != calogOkE) {
calogJsDestroy(jc);
return status;
}
}
}
calogForEach(calogContextBroker(context), jsExposeVisitor, jc);
*interpOut = jc;
return calogOkE;
}
static void jsExposeVisitor(const CalogEntryT *entry, void *ud) {
calogJsExpose((CalogJsT *)ud, entry->name);
}
static void jsEngineDestroy(void *interp) {
calogJsDestroy((CalogJsT *)interp);
}

View file

@ -40,7 +40,7 @@ typedef struct LuaExportT {
struct CalogLuaT {
lua_State *L;
CalogT *broker;
uint32_t ctxId;
uint64_t ctxId;
BindingT **bindings;
int32_t bindingCount;
int32_t bindingCap;
@ -103,7 +103,7 @@ static void luaCallableRelease(CalogFnT *callable) {
}
int32_t calogLuaCreate(CalogLuaT **out, CalogT *broker, uint32_t ctxId) {
int32_t calogLuaCreate(CalogLuaT **out, CalogT *broker, uint64_t ctxId) {
CalogLuaT *context;
lua_State *L;
@ -191,7 +191,7 @@ static int32_t luaExportAt(CalogLuaT *context, int idx, CalogFnT **out) {
ref = luaL_ref(L, LUA_REGISTRYINDEX);
export->context = context;
export->ref = ref;
status = calogFnCreate(out, luaCallableInvoke, export, luaCallableRelease, context->ctxId, 0);
status = calogFnCreate(out, context->broker, luaCallableInvoke, export, luaCallableRelease, context->ctxId);
if (status != calogOkE) {
luaL_unref(L, LUA_REGISTRYINDEX, ref);
free(export);

View file

@ -16,7 +16,7 @@
typedef struct CalogLuaT CalogLuaT;
int32_t calogLuaCreate(CalogLuaT **out, CalogT *broker, uint32_t ctxId);
int32_t calogLuaCreate(CalogLuaT **out, CalogT *broker, uint64_t ctxId);
void calogLuaDestroy(CalogLuaT *context);
int32_t calogLuaExport(CalogLuaT *context, const char *globalName, CalogFnT **out);
int32_t calogLuaExpose(CalogLuaT *context, const char *name);

View file

@ -9,19 +9,23 @@
#include "calogInternal.h"
#include "luaAdapter.h"
static int32_t luaEngineCreate(CalogContextT *context, void *config, void **interpOut);
static int32_t luaEngineCreate(CalogContextT *context, void **interpOut);
static void luaEngineDestroy(void *interp);
static int32_t luaEngineRun(void *interp, const char *source, CalogValueT *result);
static void luaExposeVisitor(const CalogEntryT *entry, void *ud);
static const char *const luaExtensions[] = { "lua", NULL };
const CalogEngineT calogLuaEngine = {
"lua",
luaExtensions,
luaEngineCreate,
luaEngineDestroy,
luaEngineRun
};
static int32_t luaEngineCreate(CalogContextT *context, void *config, void **interpOut) {
static int32_t luaEngineCreate(CalogContextT *context, void **interpOut) {
CalogLuaT *lc;
int32_t status;
@ -30,23 +34,17 @@ static int32_t luaEngineCreate(CalogContextT *context, void *config, void **inte
if (status != calogOkE) {
return status;
}
if (config != NULL) {
CalogConfigT *cfg;
int32_t index;
cfg = (CalogConfigT *)config;
for (index = 0; index < cfg->exposeCount; index++) {
status = calogLuaExpose(lc, cfg->exposeNames[index]);
if (status != calogOkE) {
calogLuaDestroy(lc);
return status;
}
}
}
calogForEach(calogContextBroker(context), luaExposeVisitor, lc);
*interpOut = lc;
return calogOkE;
}
static void luaExposeVisitor(const CalogEntryT *entry, void *ud) {
calogLuaExpose((CalogLuaT *)ud, entry->name);
}
static void luaEngineDestroy(void *interp) {
calogLuaDestroy((CalogLuaT *)interp);
}

View file

@ -21,8 +21,8 @@
#define MB_NATIVE_ERROR SE_RN_FAILED_TO_OPERATE
typedef struct BindingT {
CalogNativeFnT fn;
void *userData;
const char *name; // registry name; dispatched through calogCall so the actor
// layer marshals the native to the host thread
} BindingT;
typedef struct MyBasicRoutineT {
@ -33,7 +33,7 @@ typedef struct MyBasicRoutineT {
struct CalogMyBasicT {
struct mb_interpreter_t *bas;
CalogT *broker;
uint32_t ctxId;
uint64_t ctxId;
void **currentL;
BindingT bank[MB_BANK_SIZE];
int32_t bankCount;
@ -344,9 +344,12 @@ static int mbDispatch(int32_t slot, struct mb_interpreter_t *s, void **l) {
goto cleanupArgs;
}
// Route through calogCall so the actor layer marshals the native to the host thread
// (like the other engines); currentL is published first so a host native that calls
// back into a BASIC routine finds this context's serving frame.
savedL = context->currentL;
context->currentL = l;
status = binding->fn(args, argCount, &result, binding->userData);
status = calogCall(context->broker, binding->name, args, argCount, &result);
context->currentL = savedL;
for (index = 0; index < argCount; index++) {
@ -540,7 +543,7 @@ static int32_t mbWrapRoutine(CalogMyBasicT *context, mb_value_t routine, CalogFn
// corrupt the scope. The handle is therefore valid only while that scope is
// alive -- i.e. while the context is serving (a native call is on the stack,
// which the actor layer's parked serve frame guarantees).
status = calogFnCreate(out, mbCallableInvoke, holder, mbCallableRelease, context->ctxId, 0);
status = calogFnCreate(out, context->broker, mbCallableInvoke, holder, mbCallableRelease, context->ctxId);
if (status != calogOkE) {
free(holder);
return status;
@ -549,7 +552,7 @@ static int32_t mbWrapRoutine(CalogMyBasicT *context, mb_value_t routine, CalogFn
}
int32_t calogMyBasicCreate(CalogMyBasicT **out, CalogT *broker, uint32_t ctxId) {
int32_t calogMyBasicCreate(CalogMyBasicT **out, CalogT *broker, uint64_t ctxId) {
CalogMyBasicT *context;
struct mb_interpreter_t *bas;
@ -639,8 +642,7 @@ int32_t calogMyBasicExpose(CalogMyBasicT *context, const char *name) {
return calogErrRangeE;
}
slot = context->bankCount;
context->bank[slot].fn = entry->fn;
context->bank[slot].userData = entry->userData;
context->bank[slot].name = entry->name;
if (mb_register_func(context->bas, name, mbTrampTable[slot]) == 0) {
return calogErrArgE;
}

View file

@ -17,7 +17,7 @@
typedef struct CalogMyBasicT CalogMyBasicT;
int32_t calogMyBasicCreate(CalogMyBasicT **out, CalogT *broker, uint32_t ctxId);
int32_t calogMyBasicCreate(CalogMyBasicT **out, CalogT *broker, uint64_t ctxId);
void calogMyBasicDestroy(CalogMyBasicT *context);
int32_t calogMyBasicExportRoutine(CalogMyBasicT *context, const char *routineName, CalogFnT **out);
int32_t calogMyBasicExpose(CalogMyBasicT *context, const char *name);

View file

@ -0,0 +1,77 @@
// mybasicEngine.c -- bridges the my-basic adapter to the actor layer's CalogEngineT
// vtable, so a my-basic interpreter runs on its own context thread and is loadable by
// file extension (.bas) via calogContextLoad.
//
// my-basic keeps a little process-global state, but only two things are shared across
// interpreters: an allocation counter (patched to _Atomic in vendor/mybasic/myBasic.c,
// preserved as myBasic.c.orig) and the mb_init singletons, which are built once and are
// read-only thereafter. So interpreters EXECUTE in parallel; only lifecycle needs
// serializing -- mb_init's lazy build on the first context, mb_dispose on the last, and
// the adapter's shared context refcount. This lock covers just create and destroy;
// runSource runs unlocked, so several my-basic scripts run at once.
#define _POSIX_C_SOURCE 200809L
#include "calogInternal.h"
#include "mybasicAdapter.h"
#include <pthread.h>
static int32_t mybasicEngineCreate(CalogContextT *context, void **interpOut);
static void mybasicEngineDestroy(void *interp);
static int32_t mybasicEngineRun(void *interp, const char *source, CalogValueT *result);
static void mybasicExposeVisitor(const CalogEntryT *entry, void *ud);
// Serializes my-basic context lifecycle (mb_init/mb_dispose + the shared refcount); it
// is NOT held during execution, so scripts run concurrently.
static pthread_mutex_t mbLifecycleLock = PTHREAD_MUTEX_INITIALIZER;
static const char *const mybasicExtensions[] = { "bas", NULL };
const CalogEngineT calogMyBasicEngine = {
"mybasic",
mybasicExtensions,
mybasicEngineCreate,
mybasicEngineDestroy,
mybasicEngineRun
};
static int32_t mybasicEngineCreate(CalogContextT *context, void **interpOut) {
CalogMyBasicT *mb;
int32_t status;
*interpOut = NULL;
pthread_mutex_lock(&mbLifecycleLock);
status = calogMyBasicCreate(&mb, calogContextBroker(context), calogContextId(context));
if (status == calogOkE) {
calogForEach(calogContextBroker(context), mybasicExposeVisitor, mb);
*interpOut = mb;
}
pthread_mutex_unlock(&mbLifecycleLock);
return status;
}
static void mybasicEngineDestroy(void *interp) {
pthread_mutex_lock(&mbLifecycleLock);
calogMyBasicDestroy((CalogMyBasicT *)interp);
pthread_mutex_unlock(&mbLifecycleLock);
}
static int32_t mybasicEngineRun(void *interp, const char *source, CalogValueT *result) {
int32_t status;
calogValueNil(result);
status = calogMyBasicRun((CalogMyBasicT *)interp, source);
if (status != calogOkE) {
return calogFail(result, status, "my-basic script failed");
}
return calogOkE;
}
static void mybasicExposeVisitor(const CalogEntryT *entry, void *ud) {
calogMyBasicExpose((CalogMyBasicT *)ud, entry->name);
}

View file

@ -42,7 +42,7 @@ typedef struct SquirrelExportT {
struct CalogSquirrelT {
HSQUIRRELVM v;
CalogT *broker;
uint32_t ctxId;
uint64_t ctxId;
BindingT **bindings;
int32_t bindingCount;
int32_t bindingCap;
@ -100,7 +100,7 @@ static void squirrelCallableRelease(CalogFnT *callable) {
}
int32_t calogSquirrelCreate(CalogSquirrelT **out, CalogT *broker, uint32_t ctxId) {
int32_t calogSquirrelCreate(CalogSquirrelT **out, CalogT *broker, uint64_t ctxId) {
CalogSquirrelT *context;
HSQUIRRELVM v;
@ -193,7 +193,7 @@ static int32_t squirrelExportAt(CalogSquirrelT *context, SQInteger idx, CalogFnT
sq_addref(v, &obj);
export->context = context;
export->obj = obj;
status = calogFnCreate(out, squirrelCallableInvoke, export, squirrelCallableRelease, context->ctxId, 0);
status = calogFnCreate(out, context->broker, squirrelCallableInvoke, export, squirrelCallableRelease, context->ctxId);
if (status != calogOkE) {
sq_release(v, &obj);
free(export);

View file

@ -23,7 +23,7 @@
typedef struct CalogSquirrelT CalogSquirrelT;
int32_t calogSquirrelCreate(CalogSquirrelT **out, CalogT *broker, uint32_t ctxId);
int32_t calogSquirrelCreate(CalogSquirrelT **out, CalogT *broker, uint64_t ctxId);
void calogSquirrelDestroy(CalogSquirrelT *context);
int32_t calogSquirrelExport(CalogSquirrelT *context, const char *globalName, CalogFnT **out);
int32_t calogSquirrelExpose(CalogSquirrelT *context, const char *name);

View file

@ -8,19 +8,23 @@
#include "calogInternal.h"
#include "squirrelAdapter.h"
static int32_t squirrelEngineCreate(CalogContextT *context, void *config, void **interpOut);
static int32_t squirrelEngineCreate(CalogContextT *context, void **interpOut);
static void squirrelEngineDestroy(void *interp);
static int32_t squirrelEngineRun(void *interp, const char *source, CalogValueT *result);
static void squirrelExposeVisitor(const CalogEntryT *entry, void *ud);
static const char *const squirrelExtensions[] = { "nut", NULL };
const CalogEngineT calogSquirrelEngine = {
"squirrel",
squirrelExtensions,
squirrelEngineCreate,
squirrelEngineDestroy,
squirrelEngineRun
};
static int32_t squirrelEngineCreate(CalogContextT *context, void *config, void **interpOut) {
static int32_t squirrelEngineCreate(CalogContextT *context, void **interpOut) {
CalogSquirrelT *sc;
int32_t status;
@ -29,23 +33,17 @@ static int32_t squirrelEngineCreate(CalogContextT *context, void *config, void *
if (status != calogOkE) {
return status;
}
if (config != NULL) {
CalogConfigT *cfg;
int32_t index;
cfg = (CalogConfigT *)config;
for (index = 0; index < cfg->exposeCount; index++) {
status = calogSquirrelExpose(sc, cfg->exposeNames[index]);
if (status != calogOkE) {
calogSquirrelDestroy(sc);
return status;
}
}
}
calogForEach(calogContextBroker(context), squirrelExposeVisitor, sc);
*interpOut = sc;
return calogOkE;
}
static void squirrelExposeVisitor(const CalogEntryT *entry, void *ud) {
calogSquirrelExpose((CalogSquirrelT *)ud, entry->name);
}
static void squirrelEngineDestroy(void *interp) {
calogSquirrelDestroy((CalogSquirrelT *)interp);
}

View file

@ -22,8 +22,8 @@ struct CalogFnT {
CalogNativeFnT fn;
void *userData;
CalogReleaseFnT release;
uint32_t ownerCtxId;
uint32_t ownerGen;
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;
};
@ -176,15 +176,7 @@ int32_t calogAggSet(CalogAggT *aggregate, CalogValueT *key, CalogValueT *value)
}
// Optional hooks installed by the actor layer to route function-value invocation
// and the final release to the owner context's thread (design.md sec 10). NULL by
// default, leaving calogFnInvoke/calogFnRelease as plain inline operations for
// single-threaded use.
static CalogInvokeHookT invokeHook = NULL;
static CalogReleaseHookT releaseHook = NULL;
int32_t calogFnCreate(CalogFnT **out, CalogNativeFnT fn, void *userData, CalogReleaseFnT release, uint32_t ownerCtxId, uint32_t ownerGen) {
int32_t calogFnCreate(CalogFnT **out, CalogT *runtime, CalogNativeFnT fn, void *userData, CalogReleaseFnT release, uint64_t ownerCtxId) {
CalogFnT *callable;
*out = NULL;
@ -195,8 +187,8 @@ int32_t calogFnCreate(CalogFnT **out, CalogNativeFnT fn, void *userData, CalogRe
callable->fn = fn;
callable->userData = userData;
callable->release = release;
callable->runtime = runtime;
callable->ownerCtxId = ownerCtxId;
callable->ownerGen = ownerGen;
atomic_init(&callable->refCount, CALLABLE_INITIAL_REFCOUNT);
atomic_init(&callable->alive, true);
*out = callable;
@ -229,10 +221,10 @@ int32_t calogFnInvoke(CalogFnT *callable, CalogValueT *args, int32_t argCount, C
if (!atomic_load_explicit(&callable->alive, memory_order_acquire)) {
return calogFail(result, calogErrNotFoundE, "callable owner no longer exists");
}
// The actor layer, if present, marshals a foreign-thread invoke to the owner's
// thread; inline otherwise.
if (invokeHook != NULL) {
return invokeHook(callable, args, argCount, result);
// 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);
}
@ -246,7 +238,7 @@ void calogFnMarkDead(CalogFnT *callable) {
}
uint32_t calogFnOwner(const CalogFnT *callable) {
uint64_t calogFnOwner(const CalogFnT *callable) {
return callable->ownerCtxId;
}
@ -259,10 +251,10 @@ void calogFnRelease(CalogFnT *callable) {
}
previous = atomic_fetch_sub_explicit(&callable->refCount, 1, memory_order_acq_rel);
if (previous == 1) {
// This drop took the count to zero. The actor layer (if installed) routes
// the finalize to the owner's thread; otherwise finalize inline.
if (releaseHook != NULL) {
releaseHook(callable);
// 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);
}
@ -278,13 +270,8 @@ void calogFnRetain(CalogFnT *callable) {
}
void calogSetInvokeHook(CalogInvokeHookT hook) {
invokeHook = hook;
}
void calogSetReleaseHook(CalogReleaseHookT hook) {
releaseHook = hook;
CalogT *calogFnRuntime(const CalogFnT *callable) {
return callable->runtime;
}

View file

@ -1,12 +1,16 @@
// testActor.c -- tests for the actor threading core using synthetic contexts.
// testActor.c -- the actor dispatch machinery, engine-free.
//
// Synthetic contexts have no interpreter (engine == NULL); their "natives" are
// plain C functions routed to the owning context's thread. This isolates the
// threading machinery -- registry, queue, cross-context calls, the nested pump,
// the reply token+stash, and shutdown -- from any engine. Built under ASan+UBSan
// here and under ThreadSanitizer via `make tsan`.
// Uses synthetic contexts (engine == NULL) and plain C callables (calogFnCreate) to
// exercise the cross-thread invoke/reply path, the always-live nested pump (the
// re-entrant A->B->A deadlock test), the generationed registry (a callable owned by
// a closed+recycled context is rejected as dead, not misrouted), and concurrent
// drivers. calogFnInvoke on the host thread blocks-and-pumps the host queue while it
// waits, so no explicit calogPump is needed here. Built under ASan+UBSan and, via
// `make tsan`, ThreadSanitizer.
#include "calog.h"
#define _POSIX_C_SOURCE 200809L
#include "calogInternal.h"
#include <pthread.h>
#include <stdio.h>
@ -17,34 +21,30 @@
#define DRIVER_COUNT 4
#define STRESS_ITERATIONS 100
#define RELAY_EXPECTED 6 // ctx ids 1 + 2 + 3
#define INNER_VALUE 100
#define REENTRANT_EXPECTED 101
#define BOOM_MESSAGE "boom went off"
typedef struct DriverArgT {
CalogT *broker;
int32_t iterations;
int32_t failures;
} DriverArgT;
static CalogT *broker = NULL;
static CalogT *calog = NULL;
static CalogContextT *ctx1 = NULL;
static CalogContextT *ctx2 = NULL;
static CalogContextT *ctx3 = NULL;
static CalogFnT *innerA = NULL; // owned by ctx1: returns INNER_VALUE
static CalogFnT *relayB = NULL; // owned by ctx2: innerA() + 1
static CalogFnT *entryA = NULL; // owned by ctx1: relayB()
static CalogFnT *whoFn = NULL; // owned by ctx1: returns its running context id
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static void *driver(void *arg);
static int32_t nativeAEntry(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeAInner(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBoom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBRelay(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeRelay(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeWhoAmI(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fnEntryA(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fnInnerA(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fnRelayB(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fnWho(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void *runtimeThread(void *arg);
static void testConcurrentRuntimes(void);
static void testConcurrentStress(void);
static void testCrossContextCall(void);
static void testErrorChannel(void);
static void testCrossThreadInvoke(void);
static void testDestroyClosesOpenContexts(void);
static void testGeneration(void);
static void testReentrant(void);
@ -59,16 +59,16 @@ static void checkImpl(bool condition, const char *message, const char *file, int
static void *driver(void *arg) {
DriverArgT *driverArg;
int32_t *failures;
int32_t index;
driverArg = (DriverArgT *)arg;
for (index = 0; index < driverArg->iterations; index++) {
failures = (int32_t *)arg;
for (index = 0; index < STRESS_ITERATIONS; index++) {
CalogValueT result;
int32_t status;
status = calogCall(driverArg->broker, "relay", NULL, 0, &result);
if (status != calogOkE || result.type != calogIntE || result.as.i != RELAY_EXPECTED) {
driverArg->failures++;
status = calogFnInvoke(whoFn, NULL, 0, &result);
if (status != calogOkE || result.type != calogIntE || result.as.i != (int64_t)calogContextId(ctx1)) {
(*failures)++;
}
calogValueFree(&result);
}
@ -76,59 +76,45 @@ static void *driver(void *arg) {
}
static int32_t nativeAEntry(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogT *callBroker;
static int32_t fnEntryA(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogValueT inner;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
callBroker = calogCurrent();
status = calogCall(callBroker, "bRelay", NULL, 0, &inner);
// Runs on ctx1; calls relayB (ctx2), which calls back into ctx1 -- the deadlock
// test, resolved by ctx1's nested pump.
status = calogFnInvoke(relayB, NULL, 0, &inner);
if (status != calogOkE) {
calogValueFree(&inner);
return calogFail(result, status, "aEntry: bRelay failed");
return calogFail(result, status, "entryA: relayB failed");
}
calogValueMove(result, &inner);
return calogOkE;
}
static int32_t nativeAInner(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
static int32_t fnInnerA(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
calogValueInt(result, 100);
calogValueInt(result, INNER_VALUE);
return calogOkE;
}
static int32_t nativeBoom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
// Fails on a context other than the caller's: exercises the single error
// channel -- the message rides back in result, status carries the code.
return calogFail(result, calogErrArgE, BOOM_MESSAGE);
}
static int32_t nativeBRelay(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogT *callBroker;
static int32_t fnRelayB(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogValueT inner;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
// Calls back into context 1 while context 1 is blocked waiting on this call:
// the deadlock test. Context 1 services it via the nested pump.
callBroker = calogCurrent();
status = calogCall(callBroker, "aInner", NULL, 0, &inner);
status = calogFnInvoke(innerA, NULL, 0, &inner);
if (status != calogOkE) {
calogValueFree(&inner);
return calogFail(result, status, "bRelay: aInner failed");
return calogFail(result, status, "relayB: innerA failed");
}
calogValueInt(result, inner.as.i + 1);
calogValueFree(&inner);
@ -136,35 +122,7 @@ static int32_t nativeBRelay(CalogValueT *args, int32_t argCount, CalogValueT *re
}
static int32_t nativeRelay(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogT *callBroker;
CalogValueT v2;
CalogValueT v3;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
callBroker = calogCurrent();
status = calogCall(callBroker, "who2", NULL, 0, &v2);
if (status != calogOkE) {
calogValueFree(&v2);
return calogFail(result, status, "relay: who2 failed");
}
status = calogCall(callBroker, "who3", NULL, 0, &v3);
if (status != calogOkE) {
calogValueFree(&v2);
calogValueFree(&v3);
return calogFail(result, status, "relay: who3 failed");
}
calogValueInt(result, (int64_t)calogCurrentId() + v2.as.i + v3.as.i);
calogValueFree(&v2);
calogValueFree(&v3);
return calogOkE;
}
static int32_t nativeWhoAmI(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
static int32_t fnWho(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
@ -173,83 +131,111 @@ static int32_t nativeWhoAmI(CalogValueT *args, int32_t argCount, CalogValueT *re
}
static void testConcurrentStress(void) {
static void *runtimeThread(void *arg) {
int32_t *ok;
CalogT *rt;
CalogContextT *ctx;
CalogFnT *fn;
CalogValueT result;
int32_t status;
// Each thread creates, drives, and destroys its OWN independent runtime -- no
// process-global state is shared. A callable owned by this runtime's context must
// route to that context's thread and back, entirely within this runtime.
ok = (int32_t *)arg;
rt = calogCreate();
if (rt == NULL) {
return NULL;
}
ctx = calogContextOpen(rt, NULL);
if (ctx == NULL) {
calogDestroy(rt);
return NULL;
}
calogFnCreate(&fn, rt, fnWho, NULL, NULL, calogContextId(ctx));
status = calogFnInvoke(fn, NULL, 0, &result);
if (status == calogOkE && result.type == calogIntE && result.as.i == (int64_t)calogContextId(ctx)) {
*ok = 1;
}
calogValueFree(&result);
calogFnRelease(fn);
calogContextClose(ctx);
calogDestroy(rt);
return NULL;
}
static void testConcurrentRuntimes(void) {
pthread_t threads[DRIVER_COUNT];
DriverArgT driverArgs[DRIVER_COUNT];
int32_t oks[DRIVER_COUNT];
int32_t index;
int32_t totalFailures;
for (index = 0; index < DRIVER_COUNT; index++) {
driverArgs[index].broker = broker;
driverArgs[index].iterations = STRESS_ITERATIONS;
driverArgs[index].failures = 0;
pthread_create(&threads[index], NULL, driver, &driverArgs[index]);
oks[index] = 0;
pthread_create(&threads[index], NULL, runtimeThread, &oks[index]);
}
totalFailures = 0;
for (index = 0; index < DRIVER_COUNT; index++) {
pthread_join(threads[index], NULL);
totalFailures += driverArgs[index].failures;
CHECK(oks[index] == 1, "an independent runtime on its own host thread dispatched correctly");
}
CHECK(totalFailures == 0, "concurrent drivers: every fan-out call returned the expected sum");
}
static void testCrossContextCall(void) {
static void testConcurrentStress(void) {
pthread_t threads[DRIVER_COUNT];
int32_t failures[DRIVER_COUNT];
int32_t index;
int32_t total;
for (index = 0; index < DRIVER_COUNT; index++) {
failures[index] = 0;
pthread_create(&threads[index], NULL, driver, &failures[index]);
}
total = 0;
for (index = 0; index < DRIVER_COUNT; index++) {
pthread_join(threads[index], NULL);
total += failures[index];
}
CHECK(total == 0, "concurrent drivers: every callable invoke ran on ctx1 and returned its id");
}
static void testCrossThreadInvoke(void) {
CalogValueT result;
int32_t status;
status = calogCall(broker, "who1", NULL, 0, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 1, "call ran on context 1's thread");
calogValueFree(&result);
status = calogCall(broker, "who2", NULL, 0, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 2, "call ran on context 2's thread");
calogValueFree(&result);
status = calogCall(broker, "who3", NULL, 0, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 3, "call ran on context 3's thread");
calogValueFree(&result);
}
static void testErrorChannel(void) {
CalogValueT result;
int32_t status;
// A cross-context call that fails must surface its message through result
// (the single error channel) and the failure status -- freed once, no leak.
status = calogCall(broker, "boom", NULL, 0, &result);
CHECK(status == calogErrArgE && result.type == calogStringE && strcmp(result.as.s.bytes, BOOM_MESSAGE) == 0, "cross-context error rides back in result, freed once");
// whoFn is owned by ctx1; invoking it from the host thread must run it on ctx1.
status = calogFnInvoke(whoFn, NULL, 0, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == (int64_t)calogContextId(ctx1), "callable invoke routed to its owner context's thread");
calogValueFree(&result);
}
static void testGeneration(void) {
CalogContextT *tmpA;
CalogContextT *tmpB;
uint32_t staleId;
CalogContextT *tmp;
CalogFnT *orphan;
CalogValueT result;
uint64_t staleId;
int32_t status;
calogContextCreate(broker, NULL, NULL, &tmpA);
calogContextStart(tmpA);
staleId = calogContextId(tmpA);
calogContextDestroy(tmpA);
tmp = calogContextOpen(calog, NULL);
staleId = calogContextId(tmp);
status = calogFnCreate(&orphan, calog, fnWho, NULL, NULL, staleId);
CHECK(status == calogOkE, "created a callable owned by a soon-to-close context");
// The next create recycles tmpA's slot but with a bumped generation, so its
// id differs from the destroyed one.
calogContextCreate(broker, NULL, NULL, &tmpB);
CHECK(calogContextId(tmpB) != staleId, "recycled slot yields a new generationed id");
calogContextStart(tmpB);
calogContextClose(tmp);
// A binding still naming the destroyed context's id must fail dead, never
// misroute to the context that recycled the slot.
calogRegister(broker, "ghost", nativeWhoAmI, NULL, staleId);
status = calogCall(broker, "ghost", NULL, 0, &result);
CHECK(status == calogErrDeadE, "call to a recycled id is rejected as dead, not misrouted");
// A new context recycles tmp's slot with a bumped generation.
tmp = calogContextOpen(calog, NULL);
CHECK(calogContextId(tmp) != staleId, "recycled slot yields a new generationed id");
// Invoking the orphan (old id) must fail dead, not misroute to the recycler.
status = calogFnInvoke(orphan, NULL, 0, &result);
CHECK(status == calogErrDeadE, "callable of a recycled context is rejected as dead, not misrouted");
calogValueFree(&result);
calogContextDestroy(tmpB);
calogFnRelease(orphan);
calogContextClose(tmp);
}
@ -257,45 +243,56 @@ static void testReentrant(void) {
CalogValueT result;
int32_t status;
// 1 -> 2 -> 1: context 1 calls context 2, which calls back into context 1
// while context 1 is blocked. The nested pump must service it (no deadlock).
status = calogCall(broker, "aEntry", NULL, 0, &result);
// host -> entryA(ctx1) -> relayB(ctx2) -> innerA(ctx1): the re-entrant call back
// into ctx1 must be serviced by its nested pump (no deadlock).
status = calogFnInvoke(entryA, NULL, 0, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == REENTRANT_EXPECTED, "re-entrant A->B->A resolved without deadlock");
calogValueFree(&result);
}
int main(void) {
broker = calogCreate();
if (broker == NULL) {
printf("broker create failed\n");
return 1;
static void testDestroyClosesOpenContexts(void) {
int32_t i;
// Open several contexts and deliberately DO NOT close them: because CalogT owns
// its active-context list, calogDestroy (in main) must close them all. ASan
// verifies there is no leak at exit -- this is the cleanup guarantee.
for (i = 0; i < 32; i++) {
CalogContextT *leaked;
leaked = calogContextOpen(calog, NULL);
CHECK(leaked != NULL, "opened a context left for calogDestroy to close");
}
}
calogContextCreate(broker, NULL, NULL, &ctx1);
calogContextCreate(broker, NULL, NULL, &ctx2);
calogContextCreate(broker, NULL, NULL, &ctx3);
calogRegister(broker, "who1", nativeWhoAmI, NULL, calogContextId(ctx1));
calogRegister(broker, "who2", nativeWhoAmI, NULL, calogContextId(ctx2));
calogRegister(broker, "who3", nativeWhoAmI, NULL, calogContextId(ctx3));
calogRegister(broker, "aEntry", nativeAEntry, NULL, calogContextId(ctx1));
calogRegister(broker, "bRelay", nativeBRelay, NULL, calogContextId(ctx2));
calogRegister(broker, "aInner", nativeAInner, NULL, calogContextId(ctx1));
calogRegister(broker, "relay", nativeRelay, NULL, calogContextId(ctx1));
calogRegister(broker, "boom", nativeBoom, NULL, calogContextId(ctx2));
int main(void) {
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
ctx1 = calogContextOpen(calog, NULL);
ctx2 = calogContextOpen(calog, NULL);
calogContextStart(ctx1);
calogContextStart(ctx2);
calogContextStart(ctx3);
calogFnCreate(&whoFn, calog, fnWho, NULL, NULL, calogContextId(ctx1));
calogFnCreate(&innerA, calog, fnInnerA, NULL, NULL, calogContextId(ctx1));
calogFnCreate(&relayB, calog, fnRelayB, NULL, NULL, calogContextId(ctx2));
calogFnCreate(&entryA, calog, fnEntryA, NULL, NULL, calogContextId(ctx1));
testCrossContextCall();
testCrossThreadInvoke();
testReentrant();
testConcurrentStress();
testErrorChannel();
testGeneration();
testDestroyClosesOpenContexts();
testConcurrentRuntimes();
calogDestroy(broker);
calogFnRelease(whoFn);
calogFnRelease(innerA);
calogFnRelease(relayB);
calogFnRelease(entryA);
calogContextClose(ctx1);
calogContextClose(ctx2);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);

View file

@ -140,7 +140,7 @@ static void testAggregateCopyCleanup(void) {
int32_t level;
releaseHookCalls = 0;
status = calogFnCreate(&callable, nativeAdd, NULL, countingRelease, 0, 0);
status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
CHECK(status == calogOkE, "cleanup callable create");
status = calogAggCreate(&list, calogListE);
@ -234,7 +234,7 @@ static void testCallable(void) {
releaseHookCalls = 0;
status = calogFnCreate(&callable, nativeAdd, NULL, countingRelease, 0, 0);
status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
CHECK(status == calogOkE, "callable create");
calogValueFn(&handle, callable);
@ -267,7 +267,7 @@ static void testCallableDead(void) {
int32_t status;
releaseHookCalls = 0;
status = calogFnCreate(&callable, nativeAdd, NULL, countingRelease, 0, 0);
status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
CHECK(status == calogOkE, "dead callable create");
calogValueFn(&handle, callable);
@ -354,21 +354,21 @@ static void testRegistry(void) {
broker = calogBrokerCreate();
CHECK(broker != NULL, "broker create");
status = calogRegister(broker, "add", nativeAdd, NULL, 0);
status = calogRegister(broker, "add", nativeAdd, NULL);
CHECK(status == calogOkE, "register add");
status = calogRegister(broker, "boom", nativeBoom, NULL, 0);
status = calogRegister(broker, "boom", nativeBoom, NULL);
CHECK(status == calogOkE, "register boom");
for (index = 0; index < BULK_REGISTER; index++) {
snprintf(name, sizeof(name), "fn%d", index);
status = calogRegister(broker, name, nativeAdd, NULL, 0);
status = calogRegister(broker, name, nativeAdd, NULL);
CHECK(status == calogOkE, "register bulk");
}
CHECK(calogLookup(broker, "fn50") != NULL, "bulk lookup hit");
CHECK(calogLookup(broker, "nope") == NULL, "lookup miss");
// Re-registration must replace in place without adding a slot.
status = calogRegister(broker, "add", nativeBoom, NULL, 0);
status = calogRegister(broker, "add", nativeBoom, NULL);
CHECK(status == calogOkE, "re-register add");
calogValueInt(&args[0], 3);
@ -376,7 +376,7 @@ static void testRegistry(void) {
status = calogCall(broker, "boom", args, ADD_ARITY, &result);
CHECK(status == calogErrArgE, "re-registered add now boom");
calogValueFree(&result);
status = calogRegister(broker, "add", nativeAdd, NULL, 0);
status = calogRegister(broker, "add", nativeAdd, NULL);
CHECK(status == calogOkE, "restore add");
status = calogCall(broker, "add", args, ADD_ARITY, &result);

View file

@ -1,12 +1,9 @@
// testEngineJs.c -- a real Duktape (JavaScript) heap running on a CalogContextT
// thread, the fourth engine.
//
// Proves the CalogEngineT wiring and that the actor core drives JS exactly as it drives
// Lua/Squirrel: a JS script on jsCtx's thread calls a thread-agnostic native
// (report), a native owned by a different context (doubleIt, routed cross-thread),
// and -- exercising design.md sec 10 -- a JS closure captured on jsCtx's thread is
// invoked and released from the main thread, both marshalled back to the owner.
// Built under ASan+UBSan.
// testEngineJs.c -- Duktape (JavaScript) on a context thread, driven the host way:
// register natives, open a context, fire-and-forget a script, and calogPump on the
// host thread. Verifies host-thread dispatch, the inline escape hatch, the sec-10
// cross-thread callback, the error handler, and concurrent contexts.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
@ -14,29 +11,35 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define DOUBLE_EXPECTED 42
#define CALLBACK_INPUT 7
#define CALLBACK_EXPECTED 107
#define PUMP_LIMIT 4000
static CalogT *broker = NULL;
static CalogContextT *jsCtx = NULL;
static CalogContextT *workerCtx = NULL;
static CalogT *calog = NULL;
static _Atomic int64_t reportedValue = 0;
static _Atomic uint32_t reportedCtxId = 0;
static _Atomic uint32_t doubleItCtxId = 0;
static _Atomic uint64_t reportedCtxId = 0xFFFFu;
static _Atomic uint64_t inlineCtxId = 0xFFFFu;
static _Atomic int32_t bumpCount = 0;
static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void testCallbackAcrossContexts(void);
static void testCrossContextFromScript(void);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void);
static void testScriptError(void);
@ -49,24 +52,43 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&doubleItCtxId, calogCurrentId());
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "doubleIt expects one integer");
atomic_fetch_add(&bumpCount, 1);
calogValueNil(result);
return calogOkE;
}
calogValueInt(result, args[0].as.i * 2);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&scriptDone, true);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "report expects one integer");
}
atomic_store(&reportedCtxId, calogCurrentId());
atomic_store(&reportedValue, args[0].as.i);
return calogOkE;
}
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&inlineCtxId, calogCurrentId());
calogValueNil(result);
return calogOkE;
}
@ -78,96 +100,139 @@ static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *res
if (argCount != 1 || args[0].type != calogFnE) {
return calogFail(result, calogErrArgE, "setCb expects one function");
}
// Runs on jsCtx's thread; the closure is owned by jsCtx. Retain it past this call.
calogFnRetain(args[0].as.fn);
atomic_store(&storedCb, args[0].as.fn);
return calogOkE;
}
static void testCallbackAcrossContexts(void) {
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)message;
(void)userData;
atomic_store(&errorCtxId, contextId);
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(void) {
struct timespec ts = { 0, 500000 };
int errorsBefore;
int i;
errorsBefore = atomic_load(&errorCount);
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&scriptDone) || atomic_load(&errorCount) != errorsBefore) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
static void testHostAndInlineNatives(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogJsEngine);
CHECK(ctx != NULL, "opened a JS context");
atomic_store(&scriptDone, false);
calogContextEval(ctx, "report(42); reportInline(1); done();");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 42, "host native received the argument");
CHECK(atomic_load(&reportedCtxId) == 0, "default native ran on the host thread (id 0)");
CHECK(atomic_load(&inlineCtxId) == calogContextId(ctx), "inline native ran on the script's own thread");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) {
CalogContextT *ctx;
CalogFnT *callback;
CalogValueT arg;
CalogValueT result;
int32_t status;
// Capture a JS closure on jsCtx's thread via setCb (owned by jsCtx).
status = calogContextEval(jsCtx, "setCb(function(x) { return x + 100; })", &result);
CHECK(status == calogOkE, "captured a JS closure as a CalogFnT on its owner thread");
calogValueFree(&result);
ctx = calogContextOpen(calog, &calogJsEngine);
atomic_store(&scriptDone, false);
atomic_store(&storedCb, NULL);
calogContextEval(ctx, "setCb(function(x) { return x + 100; }); done();");
pumpUntilDone();
callback = atomic_load(&storedCb);
CHECK(callback != NULL, "callback was stored");
CHECK(callback != NULL, "a JS closure was captured as a CalogFnT");
// Invoke from the MAIN thread: calogFnInvoke must marshal to jsCtx's thread
// (design.md sec 10), so the closure runs on its owner heap, never here.
calogValueInt(&arg, CALLBACK_INPUT);
calogValueInt(&arg, 7);
status = calogFnInvoke(callback, &arg, 1, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == CALLBACK_EXPECTED, "cross-thread callable invoke routed to the owner context");
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 107, "cross-thread callable invoke routed to the owner");
calogValueFree(&result);
calogValueFree(&arg);
// Release from the main thread: the registry-slot drop must run on jsCtx's heap.
calogFnRelease(callback);
}
static void testCrossContextFromScript(void) {
CalogValueT result;
int32_t status;
status = calogContextEval(jsCtx, "report(doubleIt(21))", &result);
CHECK(status == calogOkE, "JS script with a cross-context call ran without error");
CHECK(atomic_load(&reportedValue) == DOUBLE_EXPECTED, "cross-context doubleIt result flowed back into the script");
CHECK(atomic_load(&doubleItCtxId) == calogContextId(workerCtx), "doubleIt executed on the worker context's thread");
CHECK(atomic_load(&reportedCtxId) == calogContextId(jsCtx), "report executed on the JS context's own thread");
calogValueFree(&result);
calogContextClose(ctx);
}
static void testScriptError(void) {
CalogValueT result;
int32_t status;
CalogContextT *ctx;
int32_t before;
status = calogContextEval(jsCtx, "@@@ not valid js @@@", &result);
CHECK(status != calogOkE && result.type == calogStringE, "script error surfaced through result, not a crash");
calogValueFree(&result);
ctx = calogContextOpen(calog, &calogJsEngine);
before = atomic_load(&errorCount);
atomic_store(&scriptDone, false);
calogContextEval(ctx, "@@@ not valid js @@@");
pumpUntilDone();
CHECK(atomic_load(&errorCount) == before + 1, "a fire-and-forget script error reached the error handler");
CHECK(atomic_load(&errorCtxId) == calogContextId(ctx), "the error names the failing context");
calogContextClose(ctx);
}
static void testConcurrentContexts(void) {
CalogContextT *ctxs[3];
struct timespec ts = { 0, 500000 };
int32_t i;
atomic_store(&bumpCount, 0);
for (i = 0; i < 3; i++) {
ctxs[i] = calogContextOpen(calog, &calogJsEngine);
calogContextEval(ctxs[i], "bump(); bump(); bump();");
}
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 9; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&bumpCount) == 9, "three concurrent contexts all dispatched to the host thread");
for (i = 0; i < 3; i++) {
calogContextClose(ctxs[i]);
}
}
int main(void) {
const char *exposeNames[3];
CalogConfigT jsConfig;
broker = calogCreate();
if (broker == NULL) {
printf("broker create failed\n");
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
calogContextCreate(broker, NULL, NULL, &workerCtx);
exposeNames[0] = "doubleIt";
exposeNames[1] = "report";
exposeNames[2] = "setCb";
jsConfig.exposeNames = exposeNames;
jsConfig.exposeCount = 3;
calogContextCreate(broker, &calogJsEngine, &jsConfig, &jsCtx);
// setCb is owned by jsCtx, so register it once jsCtx exists (for its id), before
// calogContextStart exposes the names on the thread.
calogRegister(broker, "doubleIt", nativeDoubleIt, NULL, calogContextId(workerCtx));
calogRegister(broker, "report", nativeReport, NULL, 0);
calogRegister(broker, "setCb", nativeSetCb, NULL, calogContextId(jsCtx));
calogContextStart(workerCtx);
calogContextStart(jsCtx);
testCrossContextFromScript();
testHostAndInlineNatives();
testCrossThreadCallback();
testScriptError();
testCallbackAcrossContexts();
testConcurrentContexts();
calogDestroy(broker);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);

View file

@ -1,12 +1,10 @@
// testEngineLua.c -- a real Lua interpreter running on a CalogContextT thread.
//
// Proves the CalogEngineT wiring: a Lua context creates its interpreter on its own
// thread (calogLuaEngine.createInterpreter), exposes broker natives there, and runs
// scripts via calogContextEval. The script calls one thread-agnostic native (report,
// owner 0, run inline on the Lua thread) and one native owned by a DIFFERENT
// context (doubleIt, owned by ctx2) -- the exposed-native trampoline routes that
// call through the broker to ctx2's thread and back, with no callback in the
// script. Built under ASan+UBSan; the threading core is covered by testActor.
// testEngineLua.c -- Lua on a context thread, driven the way a host drives calog:
// register natives, open a context, fire-and-forget a script, and calogPump on the
// host thread to service the script's native calls. Verifies host-thread dispatch,
// the inline escape hatch, the sec-10 cross-thread callback, the error handler, and
// concurrent contexts. Built under ASan+UBSan.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
@ -14,31 +12,41 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define DOUBLE_EXPECTED 42
#define PURE_EXPECTED 6
#define PUMP_LIMIT 4000 // ~2s at 0.5ms/iter, a generous upper bound
static CalogT *broker = NULL;
static CalogContextT *luaCtx = NULL;
static CalogContextT *workerCtx = NULL;
static CalogT *calog = NULL;
static _Atomic int64_t reportedValue = 0;
static _Atomic uint32_t reportedCtxId = 0;
static _Atomic uint32_t doubleItCtxId = 0;
static _Atomic uint64_t reportedCtxId = 0xFFFFu;
static _Atomic uint64_t inlineCtxId = 0xFFFFu;
static _Atomic int32_t bumpCount = 0;
static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL;
static _Atomic int32_t matchCount = 0;
static _Atomic int32_t mismatchCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void testCallbackAcrossContexts(void);
static void testCrossContextFromScript(void);
static void testFailedInterpreter(void);
static void testPureScript(void);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testContextLoad(void);
static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void);
static void testScriptError(void);
static void testSingleThreadMultiRuntime(void);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
@ -50,26 +58,60 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
// Owned by workerCtx: routing must run this on workerCtx's thread, so record
// the id it actually ran on.
atomic_store(&doubleItCtxId, calogCurrentId());
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "doubleIt expects one integer");
atomic_fetch_add(&bumpCount, 1);
calogValueNil(result);
return calogOkE;
}
calogValueInt(result, args[0].as.i * 2);
static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
// userData is the runtime this native was registered under. Run from a script and
// serviced by calogPump, it must see calogCurrent() == that runtime -- which holds
// only if pump presents the pumping thread as the pumped runtime's host, so one
// thread can host several runtimes.
if (calogCurrent() == (CalogT *)userData) {
atomic_fetch_add(&matchCount, 1);
} else {
atomic_fetch_add(&mismatchCount, 1);
}
calogValueNil(result);
return calogOkE;
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&scriptDone, true);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "report expects one integer");
}
atomic_store(&reportedCtxId, calogCurrentId());
atomic_store(&reportedCtxId, calogCurrentId()); // should be 0 (host thread)
atomic_store(&reportedValue, args[0].as.i);
return calogOkE;
}
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&inlineCtxId, calogCurrentId()); // should be the script's context id
calogValueNil(result);
return calogOkE;
}
@ -81,137 +123,215 @@ static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *res
if (argCount != 1 || args[0].type != calogFnE) {
return calogFail(result, calogErrArgE, "setCb expects one function");
}
// Runs on luaCtx's thread; the closure is owned by luaCtx. Retain it so it
// outlives this call (the trampoline frees the args afterward).
calogFnRetain(args[0].as.fn);
atomic_store(&storedCb, args[0].as.fn);
return calogOkE;
}
static void testCallbackAcrossContexts(void) {
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)message;
(void)userData;
atomic_store(&errorCtxId, contextId);
atomic_fetch_add(&errorCount, 1);
}
// Pump the host thread until the running script signals done (or errors), bounded.
static void pumpUntilDone(void) {
struct timespec ts = { 0, 500000 }; // 0.5 ms
int errorsBefore;
int i;
errorsBefore = atomic_load(&errorCount);
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&scriptDone) || atomic_load(&errorCount) != errorsBefore) {
calogPump(calog); // one more sweep to drain trailing calls
return;
}
nanosleep(&ts, NULL);
}
}
static void testHostAndInlineNatives(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogLuaEngine);
CHECK(ctx != NULL, "opened a Lua context");
atomic_store(&scriptDone, false);
calogContextEval(ctx, "report(42); reportInline(1); done()");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 42, "host native received the argument");
CHECK(atomic_load(&reportedCtxId) == 0, "default native ran on the host thread (id 0)");
CHECK(atomic_load(&inlineCtxId) == calogContextId(ctx), "inline native ran on the script's own thread");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) {
CalogContextT *ctx;
CalogFnT *callback;
CalogValueT arg;
CalogValueT result;
int32_t status;
// A Lua closure is captured on luaCtx's thread via setCb (owned by luaCtx).
status = calogContextEval(luaCtx, "setCb(function(x) return x + 100 end)", &result);
CHECK(status == calogOkE, "captured a Lua closure as a CalogFnT on its owner thread");
calogValueFree(&result);
ctx = calogContextOpen(calog, &calogLuaEngine);
atomic_store(&scriptDone, false);
atomic_store(&storedCb, NULL);
calogContextEval(ctx, "setCb(function(x) return x + 100 end); done()");
pumpUntilDone();
callback = atomic_load(&storedCb);
CHECK(callback != NULL, "callback was stored");
CHECK(callback != NULL, "a Lua closure was captured as a CalogFnT");
// Invoke it from the MAIN thread: calogFnInvoke must marshal to luaCtx's thread
// (design.md sec 10) so the closure runs on its owner, never on the main thread.
// Invoke it from the host thread: it must marshal to the Lua context's thread
// (sec 10). calogFnInvoke blocks-and-pumps the host queue while it waits.
calogValueInt(&arg, 7);
status = calogFnInvoke(callback, &arg, 1, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 107, "cross-thread callable invoke routed to the owner context");
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 107, "cross-thread callable invoke routed to the owner");
calogValueFree(&result);
calogValueFree(&arg);
// Drop the last reference from the main thread: the luaL_unref must run on
// luaCtx's thread, not here (ASan/TSan would flag an off-thread touch).
calogFnRelease(callback);
}
static void testCrossContextFromScript(void) {
CalogValueT result;
int32_t status;
// doubleIt is owned by workerCtx; report is thread-agnostic. The script reads
// as plain synchronous code -- the cross-context hop is invisible to it.
status = calogContextEval(luaCtx, "report(doubleIt(21))", &result);
CHECK(status == calogOkE, "script with a cross-context call ran without error");
CHECK(atomic_load(&reportedValue) == DOUBLE_EXPECTED, "cross-context doubleIt result flowed back into the script");
CHECK(atomic_load(&doubleItCtxId) == calogContextId(workerCtx), "doubleIt executed on the worker context's thread");
CHECK(atomic_load(&reportedCtxId) == calogContextId(luaCtx), "report executed on the Lua context's own thread");
calogValueFree(&result);
}
static void testFailedInterpreter(void) {
CalogContextT *failCtx;
const char *badNames[1];
CalogConfigT badConfig;
CalogValueT result;
int32_t status;
// A config naming a native that was never registered makes createInterpreter
// fail on the context's thread (interp stays NULL). The eval must come back as
// an error rather than dereferencing the NULL interpreter.
badNames[0] = "noSuchNative";
badConfig.exposeNames = badNames;
badConfig.exposeCount = 1;
calogContextCreate(broker, &calogLuaEngine, &badConfig, &failCtx);
calogContextStart(failCtx);
status = calogContextEval(failCtx, "report(1)", &result);
CHECK(status != calogOkE, "eval on a context whose interpreter failed to init returns an error, not a crash");
calogValueFree(&result);
calogContextDestroy(failCtx);
}
static void testPureScript(void) {
CalogValueT result;
int32_t status;
status = calogContextEval(luaCtx, "report(1 + 2 + 3)", &result);
CHECK(status == calogOkE && atomic_load(&reportedValue) == PURE_EXPECTED, "a second eval on the live interpreter ran");
calogValueFree(&result);
calogContextClose(ctx);
}
static void testScriptError(void) {
CalogValueT result;
int32_t status;
CalogContextT *ctx;
int32_t before;
// A syntax error must come back as a failing status with the error in result
// (the single channel), and must not wedge the interpreter.
status = calogContextEval(luaCtx, "@@@ not valid lua @@@", &result);
CHECK(status != calogOkE && result.type == calogStringE, "script error surfaced through result, not a crash");
calogValueFree(&result);
ctx = calogContextOpen(calog, &calogLuaEngine);
before = atomic_load(&errorCount);
atomic_store(&scriptDone, false);
calogContextEval(ctx, "@@@ not valid lua @@@");
pumpUntilDone();
CHECK(atomic_load(&errorCount) == before + 1, "a fire-and-forget script error reached the error handler");
CHECK(atomic_load(&errorCtxId) == calogContextId(ctx), "the error names the failing context");
calogContextClose(ctx);
}
static void testConcurrentContexts(void) {
CalogContextT *ctxs[3];
struct timespec ts = { 0, 500000 };
int32_t i;
atomic_store(&bumpCount, 0);
for (i = 0; i < 3; i++) {
ctxs[i] = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctxs[i], "bump(); bump(); bump()");
}
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 9; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&bumpCount) == 9, "three concurrent contexts all dispatched to the host thread");
for (i = 0; i < 3; i++) {
calogContextClose(ctxs[i]);
}
}
static void testContextLoad(void) {
CalogContextT *ctx;
struct timespec ts = { 0, 500000 };
FILE *file;
int32_t i;
// Registration makes the Lua engine (extension "lua") a load candidate.
calogRegisterEngine(calog, &calogLuaEngine);
// A file whose extension matches a registered engine is found, loaded, and run.
file = fopen("calogLoadTest.lua", "wb");
CHECK(file != NULL, "wrote a temporary .lua script");
if (file != NULL) {
fputs("bump()", file);
fclose(file);
}
atomic_store(&bumpCount, 0);
ctx = calogContextLoad(calog, "calogLoadTest");
CHECK(ctx != NULL, "calogContextLoad opened a context for the matching .lua file");
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 1; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&bumpCount) == 1, "the loaded script ran on its new context");
if (ctx != NULL) {
calogContextClose(ctx);
}
remove("calogLoadTest.lua");
// No file with a registered extension exists -> NULL.
ctx = calogContextLoad(calog, "calogNoSuchScript");
CHECK(ctx == NULL, "calogContextLoad returns NULL when no file matches");
}
static void testSingleThreadMultiRuntime(void) {
CalogT *calogB;
CalogContextT *ctxA;
CalogContextT *ctxB;
struct timespec ts = { 0, 500000 };
int32_t i;
// This one thread now hosts TWO runtimes. Each runs a script that calls a native
// registered under a different runtime; pumping each in turn must make calogCurrent()
// resolve to the runtime being pumped -- not whichever was created last.
calogB = calogCreate();
CHECK(calogB != NULL, "created a second runtime on this same thread");
calogRegister(calog, "checkRuntime", nativeCheckRuntime, calog);
calogRegister(calogB, "checkRuntime", nativeCheckRuntime, calogB);
atomic_store(&matchCount, 0);
atomic_store(&mismatchCount, 0);
ctxA = calogContextOpen(calog, &calogLuaEngine);
ctxB = calogContextOpen(calogB, &calogLuaEngine);
calogContextEval(ctxA, "checkRuntime()");
calogContextEval(ctxB, "checkRuntime()");
for (i = 0; i < PUMP_LIMIT && (atomic_load(&matchCount) + atomic_load(&mismatchCount)) < 2; i++) {
calogPump(calog);
calogPump(calogB);
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&matchCount) == 2, "one thread pumped two runtimes; each native saw its own runtime");
CHECK(atomic_load(&mismatchCount) == 0, "no native resolved to the wrong runtime while sharing a host thread");
calogContextClose(ctxA);
calogContextClose(ctxB);
calogDestroy(calogB);
}
int main(void) {
const char *exposeNames[3];
CalogConfigT luaConfig;
broker = calogCreate();
if (broker == NULL) {
printf("broker create failed\n");
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
// Worker context hosts doubleIt; it has no interpreter (a pure native owner).
calogContextCreate(broker, NULL, NULL, &workerCtx);
exposeNames[0] = "doubleIt";
exposeNames[1] = "report";
exposeNames[2] = "setCb";
luaConfig.exposeNames = exposeNames;
luaConfig.exposeCount = 3;
calogContextCreate(broker, &calogLuaEngine, &luaConfig, &luaCtx);
// setCb is owned by luaCtx, so register it once luaCtx exists (for its id), but
// before calogContextStart -- createInterpreter exposes the names on the thread.
calogRegister(broker, "doubleIt", nativeDoubleIt, NULL, calogContextId(workerCtx));
calogRegister(broker, "report", nativeReport, NULL, 0);
calogRegister(broker, "setCb", nativeSetCb, NULL, calogContextId(luaCtx));
calogContextStart(workerCtx);
calogContextStart(luaCtx);
testCrossContextFromScript();
testPureScript();
testHostAndInlineNatives();
testCrossThreadCallback();
testScriptError();
testFailedInterpreter();
testCallbackAcrossContexts();
testConcurrentContexts();
testSingleThreadMultiRuntime();
testContextLoad();
calogDestroy(broker);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);

114
tests/testEngineMyBasic.c Normal file
View file

@ -0,0 +1,114 @@
// testEngineMyBasic.c -- my-basic on context threads under the actor model. Verifies a
// host native is serviced on the host thread, and that several my-basic contexts run
// concurrently without corruption -- my-basic keeps process-global state, so the engine
// serializes my-basic work with a lock; this is the test that would race without it.
// Built under ASan+UBSan (make test) and ThreadSanitizer (make tsanmb).
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define CONTEXT_COUNT 3
static CalogT *calog = NULL;
static _Atomic int32_t bumpCount = 0;
static _Atomic uint64_t bumpCtxId = 0xFFFFu;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int32_t bump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static void testConcurrentContexts(void);
static void testHostNative(void);
static int32_t bump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&bumpCtxId, calogCurrentId());
atomic_fetch_add(&bumpCount, 1);
calogValueNil(result);
return calogOkE;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static void testConcurrentContexts(void) {
CalogContextT *ctxs[CONTEXT_COUNT];
struct timespec ts = { 0, 500000 };
int32_t i;
// The scripts allocate (list creation) as they run so the parallel execution paths
// churn my-basic's now-atomic allocation counter concurrently -- the case that
// raced before the vendored patch (see make tsanmb).
atomic_store(&bumpCount, 0);
for (i = 0; i < CONTEXT_COUNT; i++) {
ctxs[i] = calogContextOpen(calog, &calogMyBasicEngine);
calogContextEval(ctxs[i], "a = list(1, 2, 3)\nb = list(4, 5, 6)\nbump()");
}
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < CONTEXT_COUNT; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&bumpCount) == CONTEXT_COUNT, "several concurrent my-basic contexts all reached the host native");
for (i = 0; i < CONTEXT_COUNT; i++) {
calogContextClose(ctxs[i]);
}
}
static void testHostNative(void) {
CalogContextT *ctx;
struct timespec ts = { 0, 500000 };
int32_t i;
ctx = calogContextOpen(calog, &calogMyBasicEngine);
CHECK(ctx != NULL, "opened a my-basic context");
atomic_store(&bumpCount, 0);
calogContextEval(ctx, "bump()");
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 1; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&bumpCount) == 1, "the my-basic script's native call ran");
CHECK(atomic_load(&bumpCtxId) == 0, "the native ran on the host thread (id 0)");
calogContextClose(ctx);
}
int main(void) {
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogRegister(calog, "bump", bump, NULL);
testHostNative();
testConcurrentContexts();
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

View file

@ -1,12 +1,9 @@
// testEngineSquirrel.c -- a real Squirrel VM running on a CalogContextT thread.
//
// The third engine, validating that the CalogEngineT vtable + canonical CalogValueT make a
// new engine drop-in: the same actor core (calogContextCreate/calogContextEval) drives a
// Squirrel VM exactly as it drives Lua. The script calls a thread-agnostic native
// (report), a native owned by a different context (doubleIt, routed cross-thread
// by the exposed-native trampoline), and exercises string and array marshalling
// (reportStr, sumArr). Built under ASan+UBSan; the Squirrel core is linked in
// un-sanitized but its heap use is still tracked across the boundary.
// testEngineSquirrel.c -- Squirrel on a context thread, driven the host way:
// register natives, open a context, fire-and-forget a script, and calogPump on the
// host thread. Verifies host-thread dispatch, the inline escape hatch, the sec-10
// cross-thread callback, the error handler, and concurrent contexts.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
@ -14,30 +11,35 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define DOUBLE_EXPECTED 42
#define SUM_EXPECTED 42
#define REPORTSTR_CAP 64
#define PUMP_LIMIT 4000
static CalogT *broker = NULL;
static CalogContextT *squirrelCtx = NULL;
static CalogContextT *workerCtx = NULL;
static CalogT *calog = NULL;
static _Atomic int64_t reportedValue = 0;
static _Atomic uint32_t reportedCtxId = 0;
static _Atomic uint32_t doubleItCtxId = 0;
static char reportedStr[REPORTSTR_CAP];
static _Atomic uint64_t reportedCtxId = 0xFFFFu;
static _Atomic uint64_t inlineCtxId = 0xFFFFu;
static _Atomic int32_t bumpCount = 0;
static _Atomic bool scriptDone = false;
static _Atomic int32_t errorCount = 0;
static _Atomic uint64_t errorCtxId = 0;
static CalogFnT *_Atomic storedCb = NULL;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportStr(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSumArr(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void testCrossContextFromScript(void);
static void testMarshalRoundTrip(void);
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(void);
static void testConcurrentContexts(void);
static void testCrossThreadCallback(void);
static void testHostAndInlineNatives(void);
static void testScriptError(void);
@ -50,133 +52,187 @@ static void checkImpl(bool condition, const char *message, const char *file, int
}
static int32_t nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&doubleItCtxId, calogCurrentId());
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "doubleIt expects one integer");
atomic_fetch_add(&bumpCount, 1);
calogValueNil(result);
return calogOkE;
}
calogValueInt(result, args[0].as.i * 2);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&scriptDone, true);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "report expects one integer");
}
atomic_store(&reportedCtxId, calogCurrentId());
atomic_store(&reportedValue, args[0].as.i);
return calogOkE;
}
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&inlineCtxId, calogCurrentId());
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReportStr(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "reportStr expects one string");
}
// Truncate defensively; the test strings are short. Synchronization with the
// reader is via the eval reply, which establishes happens-before.
snprintf(reportedStr, sizeof(reportedStr), "%s", args[0].as.s.bytes);
calogValueNil(result);
if (argCount != 1 || args[0].type != calogFnE) {
return calogFail(result, calogErrArgE, "setCb expects one function");
}
calogFnRetain(args[0].as.fn);
atomic_store(&storedCb, args[0].as.fn);
return calogOkE;
}
static int32_t nativeSumArr(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *aggregate;
int64_t sum;
int64_t index;
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)message;
(void)userData;
if (argCount != 1 || args[0].type != calogAggE) {
return calogFail(result, calogErrArgE, "sumArr expects one array");
}
aggregate = args[0].as.agg;
sum = 0;
for (index = 0; index < aggregate->arrayCount; index++) {
if (aggregate->array[index].type != calogIntE) {
return calogFail(result, calogErrTypeE, "sumArr expects integer elements");
}
sum += aggregate->array[index].as.i;
}
calogValueInt(result, sum);
return calogOkE;
atomic_store(&errorCtxId, contextId);
atomic_fetch_add(&errorCount, 1);
}
static void testCrossContextFromScript(void) {
static void pumpUntilDone(void) {
struct timespec ts = { 0, 500000 };
int errorsBefore;
int i;
errorsBefore = atomic_load(&errorCount);
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&scriptDone) || atomic_load(&errorCount) != errorsBefore) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
static void testHostAndInlineNatives(void) {
CalogContextT *ctx;
ctx = calogContextOpen(calog, &calogSquirrelEngine);
CHECK(ctx != NULL, "opened a Squirrel context");
atomic_store(&scriptDone, false);
calogContextEval(ctx, "report(42); reportInline(1); done();");
pumpUntilDone();
CHECK(atomic_load(&reportedValue) == 42, "host native received the argument");
CHECK(atomic_load(&reportedCtxId) == 0, "default native ran on the host thread (id 0)");
CHECK(atomic_load(&inlineCtxId) == calogContextId(ctx), "inline native ran on the script's own thread");
calogContextClose(ctx);
}
static void testCrossThreadCallback(void) {
CalogContextT *ctx;
CalogFnT *callback;
CalogValueT arg;
CalogValueT result;
int32_t status;
status = calogContextEval(squirrelCtx, "report(doubleIt(21))", &result);
CHECK(status == calogOkE, "squirrel script with a cross-context call ran without error");
CHECK(atomic_load(&reportedValue) == DOUBLE_EXPECTED, "cross-context doubleIt result flowed back into the script");
CHECK(atomic_load(&doubleItCtxId) == calogContextId(workerCtx), "doubleIt executed on the worker context's thread");
CHECK(atomic_load(&reportedCtxId) == calogContextId(squirrelCtx), "report executed on the Squirrel context's own thread");
ctx = calogContextOpen(calog, &calogSquirrelEngine);
atomic_store(&scriptDone, false);
atomic_store(&storedCb, NULL);
calogContextEval(ctx, "setCb(function(x) { return x + 100; }); done();");
pumpUntilDone();
callback = atomic_load(&storedCb);
CHECK(callback != NULL, "a Squirrel closure was captured as a CalogFnT");
calogValueInt(&arg, 7);
status = calogFnInvoke(callback, &arg, 1, &result);
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 107, "cross-thread callable invoke routed to the owner");
calogValueFree(&result);
}
calogValueFree(&arg);
calogFnRelease(callback);
static void testMarshalRoundTrip(void) {
CalogValueT result;
int32_t status;
status = calogContextEval(squirrelCtx, "reportStr(\"squirrel\")", &result);
CHECK(status == calogOkE && strcmp(reportedStr, "squirrel") == 0, "string marshalled from Squirrel into a native");
calogValueFree(&result);
status = calogContextEval(squirrelCtx, "report(sumArr([10, 20, 12]))", &result);
CHECK(status == calogOkE && atomic_load(&reportedValue) == SUM_EXPECTED, "array marshalled from Squirrel into the hybrid aggregate");
calogValueFree(&result);
calogContextClose(ctx);
}
static void testScriptError(void) {
CalogValueT result;
int32_t status;
CalogContextT *ctx;
int32_t before;
status = calogContextEval(squirrelCtx, "@@@ not valid squirrel @@@", &result);
CHECK(status != calogOkE && result.type == calogStringE, "script error surfaced through result, not a crash");
calogValueFree(&result);
ctx = calogContextOpen(calog, &calogSquirrelEngine);
before = atomic_load(&errorCount);
atomic_store(&scriptDone, false);
calogContextEval(ctx, "@@@ not valid squirrel @@@");
pumpUntilDone();
CHECK(atomic_load(&errorCount) == before + 1, "a fire-and-forget script error reached the error handler");
CHECK(atomic_load(&errorCtxId) == calogContextId(ctx), "the error names the failing context");
calogContextClose(ctx);
}
static void testConcurrentContexts(void) {
CalogContextT *ctxs[3];
struct timespec ts = { 0, 500000 };
int32_t i;
atomic_store(&bumpCount, 0);
for (i = 0; i < 3; i++) {
ctxs[i] = calogContextOpen(calog, &calogSquirrelEngine);
calogContextEval(ctxs[i], "bump(); bump(); bump();");
}
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 9; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&bumpCount) == 9, "three concurrent contexts all dispatched to the host thread");
for (i = 0; i < 3; i++) {
calogContextClose(ctxs[i]);
}
}
int main(void) {
const char *exposeNames[4];
CalogConfigT squirrelConfig;
broker = calogCreate();
if (broker == NULL) {
printf("broker create failed\n");
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "setCb", nativeSetCb, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogRegister(calog, "bump", nativeBump, NULL);
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
calogContextCreate(broker, NULL, NULL, &workerCtx);
calogRegister(broker, "doubleIt", nativeDoubleIt, NULL, calogContextId(workerCtx));
calogRegister(broker, "report", nativeReport, NULL, 0);
calogRegister(broker, "reportStr", nativeReportStr, NULL, 0);
calogRegister(broker, "sumArr", nativeSumArr, NULL, 0);
exposeNames[0] = "doubleIt";
exposeNames[1] = "report";
exposeNames[2] = "reportStr";
exposeNames[3] = "sumArr";
squirrelConfig.exposeNames = exposeNames;
squirrelConfig.exposeCount = 4;
calogContextCreate(broker, &calogSquirrelEngine, &squirrelConfig, &squirrelCtx);
calogContextStart(workerCtx);
calogContextStart(squirrelCtx);
testCrossContextFromScript();
testMarshalRoundTrip();
testHostAndInlineNatives();
testCrossThreadCallback();
testScriptError();
testConcurrentContexts();
calogDestroy(broker);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);

View file

@ -178,9 +178,9 @@ int main(void) {
printf("js context create failed\n");
return 1;
}
calogRegister(broker, "record", nativeRecord, NULL, 0);
calogRegister(broker, "applyTo5", nativeApplyTo5, NULL, 0);
calogRegister(broker, "sumArr", nativeSumArr, NULL, 0);
calogRegister(broker, "record", nativeRecord, NULL);
calogRegister(broker, "applyTo5", nativeApplyTo5, NULL);
calogRegister(broker, "sumArr", nativeSumArr, NULL);
calogJsExpose(jsctx, "record");
calogJsExpose(jsctx, "applyTo5");
calogJsExpose(jsctx, "sumArr");

128
tests/testLoad.c Normal file
View file

@ -0,0 +1,128 @@
// testLoad.c -- calogContextLoad across all four engines.
//
// Links every engine (built with all CALOG_WITH_* flags, set by the Makefile), so
// calogRegisterBuiltinEngines auto-registers Lua, JS, Squirrel, and my-basic, and each
// language is loaded by file extension. Also the first exercise of my-basic under the
// actor/context-thread model. Verifies: each extension loads and runs its script; the
// first registered engine wins when several files share a base name; a base that names
// no file returns NULL.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000 // ~2s at 0.5ms/iter
static CalogT *calog = NULL;
static _Atomic int64_t lastHit = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t hit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int64_t loadAndRun(const char *base);
static void writeScript(const char *path, const char *content);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t hit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount >= 1 && args[0].type == calogIntE) {
atomic_store(&lastHit, args[0].as.i);
}
return calogOkE;
}
static void writeScript(const char *path, const char *content) {
FILE *file;
file = fopen(path, "wb");
if (file != NULL) {
fputs(content, file);
fclose(file);
}
}
// Load the script for base, pump until its hit() lands (or timeout), close, and return
// the reported value -- 0 if it never ran, -1 if no file matched at all.
static int64_t loadAndRun(const char *base) {
CalogContextT *context;
struct timespec ts = { 0, 500000 };
int32_t i;
atomic_store(&lastHit, 0);
context = calogContextLoad(calog, base);
if (context == NULL) {
return -1;
}
for (i = 0; i < PUMP_LIMIT && atomic_load(&lastHit) == 0; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
calogContextClose(context);
return atomic_load(&lastHit);
}
int main(void) {
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogRegister(calog, "hit", hit, NULL);
calogRegisterBuiltinEngines(calog); // Lua, JS, Squirrel, my-basic (all linked)
writeScript("clTest.lua", "hit(11)");
CHECK(loadAndRun("clTest") == 11, "loaded and ran a .lua script by base name");
remove("clTest.lua");
writeScript("clTest.js", "hit(12)");
CHECK(loadAndRun("clTest") == 12, "loaded and ran a .js script by base name");
remove("clTest.js");
writeScript("clTest.nut", "hit(13)");
CHECK(loadAndRun("clTest") == 13, "loaded and ran a .nut Squirrel script by base name");
remove("clTest.nut");
writeScript("clTest.bas", "hit(14)");
CHECK(loadAndRun("clTest") == 14, "loaded and ran a .bas my-basic script by base name");
remove("clTest.bas");
// First match wins: Lua is registered before my-basic, so .lua beats .bas.
writeScript("clPrio.lua", "hit(21)");
writeScript("clPrio.bas", "hit(22)");
CHECK(loadAndRun("clPrio") == 21, "first registered engine wins when several files share a base");
remove("clPrio.lua");
remove("clPrio.bas");
// No file with any registered extension exists -> NULL.
CHECK(calogContextLoad(calog, "clNoSuchBase") == NULL, "calogContextLoad returns NULL when nothing matches");
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

View file

@ -61,7 +61,7 @@ static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *
(void)args;
(void)argCount;
(void)userData;
status = calogFnCreate(&callable, nativeAdd, NULL, NULL, 0, 0);
status = calogFnCreate(&callable, broker, nativeAdd, NULL, NULL, 0);
if (status != calogOkE) {
return calogFail(result, status, "getAdder out of memory");
}
@ -204,10 +204,10 @@ int main(void) {
printf("broker create failed\n");
return 1;
}
calogRegister(broker, "add", nativeAdd, NULL, 0);
calogRegister(broker, "record", nativeRecord, NULL, 0);
calogRegister(broker, "makeList", nativeMakeList, NULL, 0);
calogRegister(broker, "getAdder", nativeGetAdder, NULL, 0);
calogRegister(broker, "add", nativeAdd, NULL);
calogRegister(broker, "record", nativeRecord, NULL);
calogRegister(broker, "makeList", nativeMakeList, NULL);
calogRegister(broker, "getAdder", nativeGetAdder, NULL);
status = calogLuaCreate(&lua, broker, 1);
if (status != calogOkE) {

View file

@ -249,8 +249,8 @@ static int32_t runProgram(const char *source) {
if (status != calogOkE) {
return status;
}
calogRegister(broker, "callExport", nativeCallExport, basic, 0);
calogRegister(broker, "callEcho", nativeCallEcho, basic, 0);
calogRegister(broker, "callExport", nativeCallExport, basic);
calogRegister(broker, "callEcho", nativeCallEcho, basic);
calogMyBasicExpose(basic, "add");
calogMyBasicExpose(basic, "record");
calogMyBasicExpose(basic, "makeList");
@ -394,13 +394,13 @@ int main(void) {
return 1;
}
calogRegister(broker, "add", nativeAdd, NULL, 0);
calogRegister(broker, "record", nativeRecord, NULL, 0);
calogRegister(broker, "makeList", nativeMakeList, NULL, 0);
calogRegister(broker, "makeDict", nativeMakeDict, NULL, 0);
calogRegister(broker, "makeNested", nativeMakeNested, NULL, 0);
calogRegister(broker, "makeStr", nativeMakeStr, NULL, 0);
calogRegister(broker, "boom", nativeBoom, NULL, 0);
calogRegister(broker, "add", nativeAdd, NULL);
calogRegister(broker, "record", nativeRecord, NULL);
calogRegister(broker, "makeList", nativeMakeList, NULL);
calogRegister(broker, "makeDict", nativeMakeDict, NULL);
calogRegister(broker, "makeNested", nativeMakeNested, NULL);
calogRegister(broker, "makeStr", nativeMakeStr, NULL);
calogRegister(broker, "boom", nativeBoom, NULL);
testNativeFromBasic();
testStringMarshal();

View file

@ -127,8 +127,8 @@ int main(void) {
printf("broker create failed\n");
return 1;
}
calogRegister(broker, "add", nativeAdd, NULL, 0);
calogRegister(broker, "record", nativeRecord, NULL, 0);
calogRegister(broker, "add", nativeAdd, NULL);
calogRegister(broker, "record", nativeRecord, NULL);
status = calogLuaCreate(&lua, broker, LUA_CTX_ID);
if (status != calogOkE) {
@ -146,7 +146,7 @@ int main(void) {
printf("lua export failed\n");
return 1;
}
calogRegister(broker, "luaDouble", callableTrampoline, luaCallable, LUA_CTX_ID);
calogRegister(broker, "luaDouble", callableTrampoline, luaCallable);
testSharedNative();
testBasicCallsLua();

View file

@ -134,8 +134,8 @@ int main(void) {
printf("squirrel context create failed\n");
return 1;
}
calogRegister(broker, "record", nativeRecord, NULL, 0);
calogRegister(broker, "applyTo5", nativeApplyTo5, NULL, 0);
calogRegister(broker, "record", nativeRecord, NULL);
calogRegister(broker, "applyTo5", nativeApplyTo5, NULL);
calogSquirrelExpose(sqctx, "record");
calogSquirrelExpose(sqctx, "applyTo5");
calogValueNil(&recorded);

View file

@ -1328,7 +1328,11 @@ static void _resize_dynamic_buffer(_dynamic_buffer_t* buf, size_t es, size_t c);
#define _MB_READ_MEM_TAG_SIZE(t) (*((mb_mem_tag_t*)((char*)(t) - _MB_MEM_TAG_SIZE)))
#ifdef MB_ENABLE_ALLOC_STAT
static volatile size_t _mb_allocated = 0;
/* calog patch (see myBasic.c.orig): _Atomic instead of volatile so the global
* allocation counter is safe when independent interpreters run on separate threads;
* every += / -= / read below is then an atomic op. Lets calog run my-basic contexts in
* parallel with only lifecycle (mb_init/mb_dispose) serialized. */
static _Atomic size_t _mb_allocated = 0;
#else /* MB_ENABLE_ALLOC_STAT */
static const size_t _mb_allocated = (size_t)(~0);
#endif /* MB_ENABLE_ALLOC_STAT */

19506
vendor/mybasic/myBasic.c.orig vendored Normal file

File diff suppressed because it is too large Load diff