API more or less ready for use.
This commit is contained in:
parent
6fb3d0367c
commit
50fad18341
36 changed files with 21987 additions and 1030 deletions
49
LICENSE.md
Normal file
49
LICENSE.md
Normal 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`.
|
||||
38
Makefile
38
Makefile
|
|
@ -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
374
README.md
Normal 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
117
design.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -22,21 +27,25 @@ 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 };
|
||||
CalogT *calog;
|
||||
CalogContextT *ctx;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
64
src/broker.c
64
src/broker.c
|
|
@ -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;
|
||||
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.
|
||||
slot->fn = fn;
|
||||
slot->userData = userData;
|
||||
slot->ownerCtxId = ownerCtxId;
|
||||
// Re-registration replaces the existing binding in place: allocation free,
|
||||
// so it never grows and never fails.
|
||||
slot->fn = fn;
|
||||
slot->userData = userData;
|
||||
slot->runInline = runInline;
|
||||
return calogOkE;
|
||||
}
|
||||
if ((broker->entryCount + 1) * BROKER_PERCENT_SCALE >= broker->slotCount * BROKER_LOAD_PERCENT) {
|
||||
|
|
@ -162,20 +177,15 @@ int32_t calogRegister(CalogT *broker, const char *name, CalogNativeFnT fn, void
|
|||
if (nameCopy == NULL) {
|
||||
return calogErrOomE;
|
||||
}
|
||||
slot->name = nameCopy;
|
||||
slot->fn = fn;
|
||||
slot->userData = userData;
|
||||
slot->ownerCtxId = ownerCtxId;
|
||||
slot->name = nameCopy;
|
||||
slot->fn = fn;
|
||||
slot->userData = userData;
|
||||
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;
|
||||
|
|
|
|||
134
src/calog.h
134
src/calog.h
|
|
@ -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,39 +110,40 @@ 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);
|
||||
const char *name;
|
||||
// NULL-terminated list of file extensions this engine handles, without the leading
|
||||
// dot (e.g. {"lua", NULL}). calogContextLoad uses it to pick an engine by filename.
|
||||
const char *const *extensions;
|
||||
int32_t (*createInterpreter)(CalogContextT *context, void **interpOut);
|
||||
void (*destroyInterpreter)(void *interp);
|
||||
int32_t (*runSource)(void *interp, const char *source, CalogValueT *result);
|
||||
} CalogEngineT;
|
||||
|
||||
// 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);
|
||||
CalogT *calogCreate(void);
|
||||
void calogDestroy(CalogT *calog);
|
||||
void calogPump(CalogT *calog); // run pending script->native calls on THIS thread
|
||||
void calogSetErrorHandler(CalogT *calog, CalogErrorFnT fn, void *userData);
|
||||
|
||||
// ---- writing natives ----
|
||||
int32_t calogFail(CalogValueT *result, int32_t status, const char *message);
|
||||
const char *calogTypeName(CalogTypeE type);
|
||||
// ---- natives ----
|
||||
int32_t calogRegister(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the host thread
|
||||
int32_t calogRegisterInline(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the calling script thread
|
||||
int32_t calogCall(CalogT *calog, const char *name, CalogValueT *args, int32_t argCount, CalogValueT *result);
|
||||
int32_t calogFail(CalogValueT *result, int32_t status, const char *message);
|
||||
const char *calogTypeName(CalogTypeE type);
|
||||
|
||||
// ---- values ----
|
||||
void calogValueNil(CalogValueT *value);
|
||||
|
|
@ -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)
|
||||
CalogT *calogCurrent(void); // runtime of the calling context (NULL 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
808
src/context.c
808
src/context.c
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -8,44 +8,42 @@
|
|||
#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;
|
||||
int32_t status;
|
||||
|
||||
*interpOut = NULL;
|
||||
status = calogJsCreate(&jc, calogContextBroker(context), calogContextId(context));
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -9,44 +9,42 @@
|
|||
#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;
|
||||
int32_t status;
|
||||
|
||||
*interpOut = NULL;
|
||||
status = calogLuaCreate(&lc, calogContextBroker(context), calogContextId(context));
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
77
src/mybasic/mybasicEngine.c
Normal file
77
src/mybasic/mybasicEngine.c
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -8,44 +8,42 @@
|
|||
#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;
|
||||
int32_t status;
|
||||
|
||||
*interpOut = NULL;
|
||||
status = calogSquirrelCreate(&sc, calogContextBroker(context), calogContextId(context));
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
53
src/value.c
53
src/value.c
|
|
@ -19,13 +19,13 @@
|
|||
#define CALLABLE_INITIAL_REFCOUNT 1
|
||||
|
||||
struct CalogFnT {
|
||||
CalogNativeFnT fn;
|
||||
void *userData;
|
||||
CalogReleaseFnT release;
|
||||
uint32_t ownerCtxId;
|
||||
uint32_t ownerGen;
|
||||
_Atomic int32_t refCount;
|
||||
_Atomic bool alive;
|
||||
CalogNativeFnT fn;
|
||||
void *userData;
|
||||
CalogReleaseFnT release;
|
||||
CalogT *runtime; // owning runtime; its hooks route invoke/release
|
||||
uint64_t ownerCtxId; // the owning context's 64-bit id (0 = host)
|
||||
_Atomic int32_t refCount;
|
||||
_Atomic bool alive;
|
||||
};
|
||||
|
||||
static int32_t aggregateCopyDepth(CalogAggT **out, const CalogAggT *src, int32_t depth);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 CalogContextT *ctx1 = NULL;
|
||||
static CalogContextT *ctx2 = NULL;
|
||||
static CalogContextT *ctx3 = NULL;
|
||||
static int32_t testsRun = 0;
|
||||
static int32_t testsFailed = 0;
|
||||
static CalogT *calog = NULL;
|
||||
static CalogContextT *ctx1 = NULL;
|
||||
static CalogContextT *ctx2 = 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 index;
|
||||
int32_t *failures;
|
||||
int32_t index;
|
||||
|
||||
driverArg = (DriverArgT *)arg;
|
||||
for (index = 0; index < driverArg->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++;
|
||||
failures = (int32_t *)arg;
|
||||
for (index = 0; index < STRESS_ITERATIONS; index++) {
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
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;
|
||||
CalogValueT inner;
|
||||
int32_t status;
|
||||
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;
|
||||
CalogValueT inner;
|
||||
int32_t status;
|
||||
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,129 +131,168 @@ static int32_t nativeWhoAmI(CalogValueT *args, int32_t argCount, CalogValueT *re
|
|||
}
|
||||
|
||||
|
||||
static void testConcurrentStress(void) {
|
||||
pthread_t threads[DRIVER_COUNT];
|
||||
DriverArgT driverArgs[DRIVER_COUNT];
|
||||
int32_t index;
|
||||
int32_t totalFailures;
|
||||
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];
|
||||
int32_t oks[DRIVER_COUNT];
|
||||
int32_t index;
|
||||
|
||||
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) {
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
static void testConcurrentStress(void) {
|
||||
pthread_t threads[DRIVER_COUNT];
|
||||
int32_t failures[DRIVER_COUNT];
|
||||
int32_t index;
|
||||
int32_t total;
|
||||
|
||||
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);
|
||||
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 testErrorChannel(void) {
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
static void testCrossThreadInvoke(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;
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
static void testReentrant(void) {
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
broker = calogCreate();
|
||||
if (broker == NULL) {
|
||||
printf("broker create failed\n");
|
||||
calog = calogCreate();
|
||||
if (calog == NULL) {
|
||||
printf("calog create failed\n");
|
||||
return 1;
|
||||
}
|
||||
ctx1 = calogContextOpen(calog, NULL);
|
||||
ctx2 = calogContextOpen(calog, NULL);
|
||||
|
||||
calogContextCreate(broker, NULL, NULL, &ctx1);
|
||||
calogContextCreate(broker, NULL, NULL, &ctx2);
|
||||
calogContextCreate(broker, NULL, NULL, &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));
|
||||
|
||||
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));
|
||||
|
||||
calogContextStart(ctx1);
|
||||
calogContextStart(ctx2);
|
||||
calogContextStart(ctx3);
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 CalogFnT *_Atomic storedCb = NULL;
|
||||
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");
|
||||
}
|
||||
calogValueInt(result, args[0].as.i * 2);
|
||||
atomic_fetch_add(&bumpCount, 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(&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) {
|
||||
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);
|
||||
|
||||
callback = atomic_load(&storedCb);
|
||||
CHECK(callback != NULL, "callback was stored");
|
||||
|
||||
// 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);
|
||||
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");
|
||||
calogValueFree(&result);
|
||||
calogValueFree(&arg);
|
||||
|
||||
// Release from the main thread: the registry-slot drop must run on jsCtx's heap.
|
||||
calogFnRelease(callback);
|
||||
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 testCrossContextFromScript(void) {
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
static void pumpUntilDone(void) {
|
||||
struct timespec ts = { 0, 500000 };
|
||||
int errorsBefore;
|
||||
int i;
|
||||
|
||||
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");
|
||||
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;
|
||||
|
||||
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, "a JS 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);
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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 _Atomic int64_t reportedValue = 0;
|
||||
static _Atomic uint32_t reportedCtxId = 0;
|
||||
static _Atomic uint32_t doubleItCtxId = 0;
|
||||
static CalogFnT *_Atomic storedCb = NULL;
|
||||
static int32_t testsRun = 0;
|
||||
static int32_t testsFailed = 0;
|
||||
static CalogT *calog = NULL;
|
||||
static _Atomic int64_t reportedValue = 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;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
calogValueInt(result, args[0].as.i * 2);
|
||||
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) {
|
||||
CalogFnT *callback;
|
||||
CalogValueT arg;
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||
(void)message;
|
||||
(void)userData;
|
||||
atomic_store(&errorCtxId, contextId);
|
||||
atomic_fetch_add(&errorCount, 1);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
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
114
tests/testEngineMyBasic.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
calogValueInt(result, args[0].as.i * 2);
|
||||
atomic_fetch_add(&bumpCount, 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(&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");
|
||||
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;
|
||||
}
|
||||
sum += aggregate->array[index].as.i;
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
calogValueInt(result, sum);
|
||||
return calogOkE;
|
||||
}
|
||||
|
||||
|
||||
static void testCrossContextFromScript(void) {
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
static void testHostAndInlineNatives(void) {
|
||||
CalogContextT *ctx;
|
||||
|
||||
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");
|
||||
calogValueFree(&result);
|
||||
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 testMarshalRoundTrip(void) {
|
||||
CalogValueT result;
|
||||
int32_t status;
|
||||
static void testCrossThreadCallback(void) {
|
||||
CalogContextT *ctx;
|
||||
CalogFnT *callback;
|
||||
CalogValueT arg;
|
||||
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);
|
||||
ctx = calogContextOpen(calog, &calogSquirrelEngine);
|
||||
atomic_store(&scriptDone, false);
|
||||
atomic_store(&storedCb, NULL);
|
||||
calogContextEval(ctx, "setCb(function(x) { return x + 100; }); done();");
|
||||
pumpUntilDone();
|
||||
|
||||
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");
|
||||
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);
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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
128
tests/testLoad.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
6
vendor/mybasic/myBasic.c
vendored
6
vendor/mybasic/myBasic.c
vendored
|
|
@ -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
19506
vendor/mybasic/myBasic.c.orig
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue