This commit is contained in:
2026-02-23 18:08:13 -06:00
parent a34566a0c1
commit 4da15d2a3e
16 changed files with 408 additions and 592 deletions

View File

@@ -1,5 +1,5 @@
#include "cell.h"
#include "quickjs-internal.h"
#include "pit_internal.h"
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(js, js2number(js,argv[0])))

View File

@@ -241,7 +241,7 @@ source/
**`cell_runtime.c`** is the single file that defines the native code contract. It should:
1. Include `quickjs-internal.h` for access to value representation and heap types
1. Include `pit_internal.h` for access to value representation and heap types
2. Export all `cell_rt_*` functions with C linkage (no `static`)
3. Keep each function thin — delegate to existing `JS_*` functions where possible
4. Handle GC safety: after any allocation (frame, string, array), callers' frames may have moved

View File

@@ -1,5 +1,5 @@
#define NOTA_IMPLEMENTATION
#include "quickjs-internal.h"
#include "pit_internal.h"
#include "cell.h"
static int nota_get_arr_len (JSContext *ctx, JSValue arr) {

View File

@@ -1,5 +1,5 @@
#include "cell.h"
#include "cell_internal.h"
#include "pit_internal.h"
#include <sys/stat.h>
#include <sys/types.h>
@@ -308,12 +308,9 @@ JSC_SCALL(os_system,
setting pause_flag = 2. Bump turn_gen so stale timer events are
ignored, and clear the pause flag so the VM doesn't raise
"interrupted" on the next backward branch. */
cell_rt *crt = JS_GetContextOpaque(js);
if (crt) {
atomic_fetch_add_explicit(&crt->turn_gen, 1, memory_order_relaxed);
JS_SetPauseFlag(js, 0);
crt->turn_start_ns = cell_ns();
}
atomic_fetch_add_explicit(&js->turn_gen, 1, memory_order_relaxed);
JS_SetPauseFlag(js, 0);
js->turn_start_ns = cell_ns();
ret = number2js(js, err);
)

View File

@@ -1,5 +1,5 @@
#define WOTA_IMPLEMENTATION
#include "quickjs-internal.h"
#include "pit_internal.h"
#include "cell.h"
typedef struct ObjectRef {

View File

@@ -8,8 +8,7 @@
#include "stb_ds.h"
#include "cell.h"
#include "quickjs-internal.h"
#include "cell_internal.h"
#include "pit_internal.h"
#define BOOTSTRAP_MCODE "boot/bootstrap.cm.mcode"
#define ENGINE_SRC "internal/engine.cm"
@@ -27,13 +26,13 @@
int run_c_test_suite(JSContext *ctx);
static int run_test_suite(size_t heap_size);
cell_rt *root_cell = NULL;
static JSContext *root_ctx = NULL;
static char *shop_path = NULL;
volatile JSContext *g_crash_ctx = NULL;
static char *core_path = NULL;
static int native_mode = 0;
static int warn_mode = 1;
static JSRuntime *g_runtime = NULL;
JSRuntime *g_runtime = NULL;
// Compute blake2b hash of data and return hex string (caller must free)
static char *compute_blake2_hex(const char *data, size_t size) {
@@ -303,44 +302,28 @@ const char* cell_get_core_path(void) {
return core_path;
}
void actor_disrupt(cell_rt *crt)
void actor_disrupt(JSContext *ctx)
{
crt->disrupt = 1;
JS_SetPauseFlag(crt->context, 2);
if (crt->state != ACTOR_RUNNING)
actor_free(crt);
ctx->disrupt = 1;
JS_SetPauseFlag(ctx, 2);
if (ctx->state != ACTOR_RUNNING)
actor_free(ctx);
}
JSValue js_core_internal_os_use(JSContext *js);
JSValue js_core_json_use(JSContext *js);
void script_startup(cell_rt *prt)
void script_startup(JSContext *js)
{
if (!g_runtime) {
g_runtime = JS_NewRuntime();
}
JSContext *js = JS_NewContext(g_runtime);
JS_SetContextOpaque(js, prt);
JS_SetGCScanExternal(js, actor_gc_scan);
prt->context = js;
/* Set per-actor heap memory limit */
JS_SetHeapMemoryLimit(js, ACTOR_MEMORY_LIMIT);
/* Register all GCRef fields so the Cheney GC can relocate them. */
JS_AddGCRef(js, &prt->idx_buffer_ref);
JS_AddGCRef(js, &prt->on_exception_ref);
JS_AddGCRef(js, &prt->message_handle_ref);
JS_AddGCRef(js, &prt->unneeded_ref);
JS_AddGCRef(js, &prt->actor_sym_ref);
prt->idx_buffer_ref.val = JS_NULL;
prt->on_exception_ref.val = JS_NULL;
prt->message_handle_ref.val = JS_NULL;
prt->unneeded_ref.val = JS_NULL;
prt->actor_sym_ref.val = JS_NULL;
cell_rt *crt = JS_GetContextOpaque(js);
JS_FreeValue(js, js_core_blob_use(js));
// Try engine fast-path: load engine.cm from source-hash cache
@@ -381,11 +364,11 @@ void script_startup(cell_rt *prt)
JSValue boot_env = JS_Stone(js, boot_env_ref.val);
JS_DeleteGCRef(js, &boot_env_ref);
crt->state = ACTOR_RUNNING;
js->state = ACTOR_RUNNING;
JSValue bv = JS_RunMachBin(js, (const uint8_t *)boot_bin, boot_bin_size, boot_env);
free(boot_bin);
uncaught_exception(js, bv);
crt->state = ACTOR_IDLE;
js->state = ACTOR_IDLE;
// Retry engine from cache
bin_data = try_engine_cache(&bin_size);
@@ -405,20 +388,20 @@ void script_startup(cell_rt *prt)
tmp = js_core_json_use(js);
JS_SetPropertyStr(js, env_ref.val, "json", tmp);
crt->actor_sym_ref.val = JS_NewObject(js);
JS_CellStone(js, crt->actor_sym_ref.val);
JS_SetActorSym(js, JS_DupValue(js, crt->actor_sym_ref.val));
JS_SetPropertyStr(js, env_ref.val, "actorsym", JS_DupValue(js, crt->actor_sym_ref.val));
js->actor_sym_ref.val = JS_NewObject(js);
JS_CellStone(js, js->actor_sym_ref.val);
JS_SetActorSym(js, JS_DupValue(js, js->actor_sym_ref.val));
JS_SetPropertyStr(js, env_ref.val, "actorsym", JS_DupValue(js, js->actor_sym_ref.val));
// Always set init (even if null)
if (crt->init_wota) {
if (js->init_wota) {
JSGCRef init_ref;
JS_PushGCRef(js, &init_ref);
init_ref.val = wota2value(js, crt->init_wota);
init_ref.val = wota2value(js, js->init_wota);
JS_SetPropertyStr(js, env_ref.val, "init", init_ref.val);
JS_PopGCRef(js, &init_ref);
free(crt->init_wota);
crt->init_wota = NULL;
free(js->init_wota);
js->init_wota = NULL;
} else {
JS_SetPropertyStr(js, env_ref.val, "init", JS_NULL);
}
@@ -438,12 +421,12 @@ void script_startup(cell_rt *prt)
JS_DeleteGCRef(js, &env_ref);
// Run engine from binary
crt->state = ACTOR_RUNNING;
js->state = ACTOR_RUNNING;
JSValue v = JS_RunMachBin(js, (const uint8_t *)bin_data, bin_size, hidden_env);
free(bin_data);
uncaught_exception(js, v);
crt->state = ACTOR_IDLE;
set_actor_state(crt);
js->state = ACTOR_IDLE;
set_actor_state(js);
}
static void signal_handler(int sig)
@@ -624,35 +607,23 @@ int cell_init(int argc, char **argv)
return 1;
}
/* Create a cell_rt for the CLI context so JS-C bridge functions work */
cell_rt *cli_rt = calloc(sizeof(*cli_rt), 1);
cli_rt->mutex = malloc(sizeof(pthread_mutex_t));
/* Set up mutexes on the CLI context */
ctx->mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutexattr_t mattr;
pthread_mutexattr_init(&mattr);
pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(cli_rt->mutex, &mattr);
cli_rt->msg_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(cli_rt->msg_mutex, &mattr);
pthread_mutex_init(ctx->mutex, &mattr);
ctx->msg_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(ctx->msg_mutex, &mattr);
pthread_mutexattr_destroy(&mattr);
cli_rt->context = ctx;
JS_SetContextOpaque(ctx, cli_rt);
JS_SetGCScanExternal(ctx, actor_gc_scan);
JS_AddGCRef(ctx, &cli_rt->idx_buffer_ref);
JS_AddGCRef(ctx, &cli_rt->on_exception_ref);
JS_AddGCRef(ctx, &cli_rt->message_handle_ref);
JS_AddGCRef(ctx, &cli_rt->unneeded_ref);
JS_AddGCRef(ctx, &cli_rt->actor_sym_ref);
cli_rt->idx_buffer_ref.val = JS_NULL;
cli_rt->on_exception_ref.val = JS_NULL;
cli_rt->message_handle_ref.val = JS_NULL;
cli_rt->unneeded_ref.val = JS_NULL;
cli_rt->actor_sym_ref.val = JS_NewObject(ctx);
JS_CellStone(ctx, cli_rt->actor_sym_ref.val);
JS_SetActorSym(ctx, JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
ctx->actor_sym_ref.val = JS_NewObject(ctx);
JS_CellStone(ctx, ctx->actor_sym_ref.val);
JS_SetActorSym(ctx, JS_DupValue(ctx, ctx->actor_sym_ref.val));
root_cell = cli_rt;
root_ctx = ctx;
JS_FreeValue(ctx, js_core_blob_use(ctx));
@@ -699,7 +670,7 @@ int cell_init(int argc, char **argv)
JS_SetPropertyStr(ctx, boot_env_ref.val, "core_path", btmp);
btmp = shop_path ? JS_NewString(ctx, shop_path) : JS_NULL;
JS_SetPropertyStr(ctx, boot_env_ref.val, "shop_path", btmp);
JS_SetPropertyStr(ctx, boot_env_ref.val, "actorsym", JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
JS_SetPropertyStr(ctx, boot_env_ref.val, "actorsym", JS_DupValue(ctx, ctx->actor_sym_ref.val));
btmp = js_core_json_use(ctx);
JS_SetPropertyStr(ctx, boot_env_ref.val, "json", btmp);
if (native_mode)
@@ -763,7 +734,7 @@ int cell_init(int argc, char **argv)
JS_SetPropertyStr(ctx, env_ref.val, "core_path", tmp);
tmp = shop_path ? JS_NewString(ctx, shop_path) : JS_NULL;
JS_SetPropertyStr(ctx, env_ref.val, "shop_path", tmp);
JS_SetPropertyStr(ctx, env_ref.val, "actorsym", JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
JS_SetPropertyStr(ctx, env_ref.val, "actorsym", JS_DupValue(ctx, ctx->actor_sym_ref.val));
tmp = js_core_json_use(ctx);
JS_SetPropertyStr(ctx, env_ref.val, "json", tmp);
if (native_mode || !warn_mode || eval_script) {
@@ -826,18 +797,11 @@ check_actors:
}
/* No actors spawned — clean up CLI context */
JS_DeleteGCRef(ctx, &cli_rt->idx_buffer_ref);
JS_DeleteGCRef(ctx, &cli_rt->on_exception_ref);
JS_DeleteGCRef(ctx, &cli_rt->message_handle_ref);
JS_DeleteGCRef(ctx, &cli_rt->unneeded_ref);
JS_DeleteGCRef(ctx, &cli_rt->actor_sym_ref);
pthread_mutex_destroy(cli_rt->mutex);
free(cli_rt->mutex);
pthread_mutex_destroy(cli_rt->msg_mutex);
free(cli_rt->msg_mutex);
free(cli_rt);
root_cell = NULL;
pthread_mutex_destroy(ctx->mutex);
free(ctx->mutex);
pthread_mutex_destroy(ctx->msg_mutex);
free(ctx->msg_mutex);
root_ctx = NULL;
JS_FreeContext(ctx);
JS_FreeRuntime(g_runtime);
@@ -890,18 +854,7 @@ cell_hook cell_trace_gethook(void)
void cell_rt_set_trace_hook(JSContext *ctx, cell_hook hook)
{
struct cell_rt *rt = JS_GetContextOpaque(ctx);
if (rt) rt->trace_hook = hook;
}
const char *cell_rt_name(struct cell_rt *rt)
{
return rt->name;
}
JSContext *cell_rt_context(struct cell_rt *rt)
{
return rt->context;
ctx->actor_trace_hook = hook;
}
int uncaught_exception(JSContext *js, JSValue v)
@@ -914,13 +867,12 @@ int uncaught_exception(JSContext *js, JSValue v)
by JS_ThrowError2 / print_backtrace. Just clear the flag. */
if (has_exc)
JS_GetException(js);
cell_rt *crt = JS_GetContextOpaque(js);
if (crt && !JS_IsNull(crt->on_exception_ref.val)) {
if (!JS_IsNull(js->on_exception_ref.val)) {
/* Disable interruption so actor_die can send messages
without being re-interrupted. */
JS_SetPauseFlag(js, 0);
JSValue err = JS_NewString(js, "interrupted");
JS_Call(js, crt->on_exception_ref.val, JS_NULL, 1, &err);
JS_Call(js, js->on_exception_ref.val, JS_NULL, 1, &err);
/* Clear any secondary exception from the callback. */
if (JS_HasException(js))
JS_GetException(js);

View File

@@ -1,28 +1,3 @@
/*
* cell.h — Consolidated public C API for ƿit modules
*
* QuickJS Javascript Engine
* Copyright (c) 2017-2021 Fabrice Bellard
* Copyright (c) 2017-2021 Charlie Gordon
*
* 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.
*/
#ifndef CELL_H
#define CELL_H
@@ -282,7 +257,7 @@ static inline JSValue MIST_TryNewImmediateASCII (const char *str, size_t len) {
return v;
}
/* Heap object type checks (non-inline — see mist_is_* in quickjs-internal.h
/* Heap object type checks (non-inline — see mist_is_* in pit_internal.h
for inline versions used by the VM dispatch loop) */
JS_BOOL JS_IsArray(JSValue v);
JS_BOOL JS_IsRecord(JSValue v);
@@ -881,14 +856,11 @@ void js_debug_sethook (JSContext *ctx, js_hook, int type, void *user);
#define CELL_HOOK_ENTER 1
#define CELL_HOOK_EXIT 2
struct cell_rt;
typedef void (*cell_hook)(struct cell_rt *rt, int type);
typedef void (*cell_hook)(JSContext *ctx, int type);
void cell_trace_sethook(cell_hook);
cell_hook cell_trace_gethook(void);
void cell_rt_set_trace_hook(JSContext *ctx, cell_hook hook);
size_t cell_ctx_heap_used(JSContext *ctx);
const char *cell_rt_name(struct cell_rt *rt);
JSContext *cell_rt_context(struct cell_rt *rt);
void js_debug_gethook(JSContext *ctx, js_hook *hook, int *type, void **user);
/* ============================================================

View File

@@ -1,118 +0,0 @@
#include <pthread.h>
#include <stdatomic.h>
/* Internal runtime accessors — not in public cell.h API */
void *JS_GetContextOpaque (JSContext *ctx);
void JS_SetContextOpaque (JSContext *ctx, void *opaque);
void JS_SetPauseFlag(JSContext *ctx, int value);
JSValue JS_GetStack (JSContext *ctx);
/* Letter type for unified message queue */
typedef enum {
LETTER_BLOB, /* Blob message */
LETTER_CALLBACK /* JSValue callback function */
} letter_type;
typedef struct letter {
letter_type type;
union {
blob *blob_data; /* For LETTER_BLOB */
JSValue callback; /* For LETTER_CALLBACK */
};
} letter;
#define ACTOR_IDLE 0 // Actor not doing anything
#define ACTOR_READY 1 // Actor ready for a turn
#define ACTOR_RUNNING 2 // Actor taking a turn
#define ACTOR_EXHAUSTED 3 // Actor waiting for GC
#define ACTOR_RECLAIMING 4 // Actor running GC
#define ACTOR_SLOW 5 // Actor going slowly; deprioritize
#define ACTOR_REFRESHED 6 // GC finished, ready to resume
// #define ACTOR_TRACE
#define ACTOR_FAST_TIMER_NS (10ULL * 1000000) // 10ms per turn
#define ACTOR_SLOW_TIMER_NS (60000ULL * 1000000) // 60s for slow actors
#define ACTOR_SLOW_STRIKES_MAX 3 // consecutive slow turns -> kill
#define ACTOR_MEMORY_LIMIT (16ULL * 1024 * 1024) // 16MB heap cap
typedef struct cell_rt {
JSContext *context;
/* JSValues on the GC heap — each paired with a JSGCRef so the
Cheney GC can relocate them during compaction. */
JSGCRef idx_buffer_ref;
JSGCRef on_exception_ref;
JSGCRef message_handle_ref;
JSGCRef unneeded_ref;
JSGCRef actor_sym_ref;
void *init_wota;
/* Protects JSContext usage */
pthread_mutex_t *mutex; /* for everything else */
pthread_mutex_t *msg_mutex; /* For message queue and timers queue */
char *id;
int idx_count;
/* The "mailbox" for incoming messages + a dedicated lock for it: */
letter *letters;
/* CHANGED FOR EVENTS: a separate lock for the actor->events queue */
struct { uint32_t key; JSValue value; } *timers;
int state;
uint32_t ar; // timer for unneeded
double ar_secs; // time for unneeded
int disrupt;
int is_quiescent; // tracked by scheduler for quiescence detection
int main_thread_only;
int affinity;
uint64_t turn_start_ns; // cell_ns() when turn began
_Atomic uint32_t turn_gen; // incremented each turn start
int slow_strikes; // consecutive slow-completed turns
int vm_suspended; // 1 if VM is paused mid-turn
const char *name; // human friendly name
cell_hook trace_hook;
} cell_rt;
/* Set by actor_turn/CLI before entering the VM, cleared after.
Read by signal_handler to print JS stack on crash. */
extern volatile JSContext *g_crash_ctx;
cell_rt *create_actor(void *wota);
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
void actor_disrupt(cell_rt *actor);
const char *send_message(const char *id, void *msg);
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds);
void script_startup(cell_rt *rt);
int uncaught_exception(JSContext *js, JSValue v);
int actor_exists(const char *id);
void set_actor_state(cell_rt *actor);
void enqueue_actor_priority(cell_rt *actor);
void actor_clock(cell_rt *actor, JSValue fn);
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds);
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id);
void exit_handler(void);
void actor_loop();
void actor_initialize(void);
void actor_free(cell_rt *actor);
void actor_gc_scan(JSContext *ctx, uint8_t *fb, uint8_t *fe,
uint8_t *tb, uint8_t **tf, uint8_t *te);
int scheduler_actor_count(void);
void scheduler_enable_quiescence(void);
JSValue JS_ResumeRegisterVM(JSContext *ctx);
uint64_t cell_ns();
void cell_sleep(double seconds);
int randombytes(void *buf, size_t n);

View File

@@ -1,29 +1,4 @@
/*
* QuickJS Javascript Engine
*
* Copyright (c) 2017-2025 Fabrice Bellard
* Copyright (c) 2017-2025 Charlie Gordon
*
* 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.
*/
#include "quickjs-internal.h"
#include "pit_internal.h"
#include <assert.h>
/* ============================================================

View File

@@ -1,30 +1,6 @@
#ifndef QUICKJS_INTERNAL_H
#define QUICKJS_INTERNAL_H
/*
* QuickJS Javascript Engine
*
* Copyright (c) 2017-2025 Fabrice Bellard
* Copyright (c) 2017-2025 Charlie Gordon
*
* 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.
*/
#include <assert.h>
#include <ctype.h>
#include <inttypes.h>
@@ -36,6 +12,9 @@
#include <stdatomic.h>
#include <sys/time.h>
#include <time.h>
#ifndef TARGET_PLAYDATE
#include <pthread.h>
#endif
#if defined(__APPLE__)
#include <malloc/malloc.h>
#elif defined(__linux__) || defined(__GLIBC__)
@@ -103,8 +82,6 @@ void JS_SetPoolSize (JSRuntime *rt, size_t initial, size_t cap);
JSContext *JS_NewContext (JSRuntime *rt);
JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size);
void JS_FreeContext (JSContext *s);
void *JS_GetContextOpaque (JSContext *ctx);
void JS_SetContextOpaque (JSContext *ctx, void *opaque);
typedef void (*JS_GCScanFn)(JSContext *ctx,
uint8_t *from_base, uint8_t *from_end,
@@ -864,6 +841,38 @@ typedef struct CCallRoot {
struct CCallRoot *prev; /* stack for nesting (C -> JS -> C -> ...) */
} CCallRoot;
/* ============================================================
Actor Types (merged from cell_internal.h)
============================================================ */
/* Letter type for unified message queue */
typedef enum {
LETTER_BLOB,
LETTER_CALLBACK
} letter_type;
typedef struct letter {
letter_type type;
union {
blob *blob_data;
JSValue callback;
};
} letter;
/* Actor state machine constants */
#define ACTOR_IDLE 0
#define ACTOR_READY 1
#define ACTOR_RUNNING 2
#define ACTOR_EXHAUSTED 3
#define ACTOR_RECLAIMING 4
#define ACTOR_SLOW 5
#define ACTOR_REFRESHED 6
#define ACTOR_FAST_TIMER_NS (10ULL * 1000000)
#define ACTOR_SLOW_TIMER_NS (60000ULL * 1000000)
#define ACTOR_SLOW_STRIKES_MAX 3
#define ACTOR_MEMORY_LIMIT (16ULL * 1024 * 1024)
struct JSContext {
JSRuntime *rt;
@@ -919,7 +928,6 @@ struct JSContext {
/* if NULL, RegExp compilation is not supported */
JSValue (*compile_regexp) (JSContext *ctx, JSValue pattern, JSValue flags);
void *user_opaque;
/* GC callback to scan external C-side roots (actor letters, timers) */
void (*gc_scan_external)(JSContext *ctx,
@@ -956,6 +964,44 @@ struct JSContext {
// todo: want this, but should be a simple increment/decrement counter while frames are pushed
size_t stack_depth;
size_t stack_limit;
/* === Actor fields (merged from cell_rt) === */
JSGCRef idx_buffer_ref;
JSGCRef on_exception_ref;
JSGCRef message_handle_ref;
JSGCRef unneeded_ref;
JSGCRef actor_sym_ref;
void *init_wota;
#ifndef TARGET_PLAYDATE
pthread_mutex_t *mutex;
pthread_mutex_t *msg_mutex;
#endif
char *id;
int idx_count;
letter *letters;
struct { uint32_t key; JSValue value; } *timers;
int state;
uint32_t ar;
double ar_secs;
int disrupt;
int is_quiescent;
int main_thread_only;
int affinity;
uint64_t turn_start_ns;
_Atomic uint32_t turn_gen;
int slow_strikes;
int vm_suspended;
const char *name;
cell_hook actor_trace_hook;
};
/* ============================================================
@@ -979,6 +1025,44 @@ static inline const char *JS_KeyGetStr (JSContext *ctx, char *buf, size_t buf_si
}
/* ============================================================
Actor / Scheduler Declarations (merged from cell_internal.h)
============================================================ */
/* Set by actor_turn/CLI before entering the VM, cleared after.
Read by signal_handler to print JS stack on crash. */
extern volatile JSContext *g_crash_ctx;
JSContext *create_actor(void *wota);
const char *register_actor(const char *id, JSContext *actor, int mainthread, double ar);
void actor_disrupt(JSContext *actor);
const char *send_message(const char *id, void *msg);
void actor_unneeded(JSContext *actor, JSValue fn, double seconds);
void script_startup(JSContext *ctx);
int uncaught_exception(JSContext *js, JSValue v);
int actor_exists(const char *id);
void set_actor_state(JSContext *actor);
void enqueue_actor_priority(JSContext *actor);
void actor_clock(JSContext *actor, JSValue fn);
uint32_t actor_delay(JSContext *actor, JSValue fn, double seconds);
JSValue actor_remove_timer(JSContext *actor, uint32_t timer_id);
void exit_handler(void);
void actor_loop(void);
void actor_initialize(void);
void actor_free(JSContext *actor);
void actor_gc_scan(JSContext *ctx, uint8_t *fb, uint8_t *fe,
uint8_t *tb, uint8_t **tf, uint8_t *te);
int scheduler_actor_count(void);
void scheduler_enable_quiescence(void);
JSValue JS_ResumeRegisterVM(JSContext *ctx);
uint64_t cell_ns(void);
void cell_sleep(double seconds);
int randombytes(void *buf, size_t n);
/* ============================================================
Constant Text Pool Functions
============================================================ */

View File

@@ -6,7 +6,7 @@
* string comparison, bitwise ops on floats, and boolean conversion.
*/
#include "quickjs-internal.h"
#include "pit_internal.h"
#include <math.h>
#include <pthread.h>
#include <stdlib.h>

View File

@@ -1,13 +1,11 @@
#include "cell.h"
#include "cell_internal.h"
#include "quickjs-internal.h"
#include "pit_internal.h"
#include <stdlib.h>
#include <string.h>
JSC_CCALL(os_createactor,
cell_rt *rt = JS_GetContextOpaque(js);
if (rt->disrupt)
if (js->disrupt)
return JS_RaiseDisrupt(js, "Can't start a new actor while disrupting.");
void *startup = value2wota(js, argv[0], JS_NULL, NULL);
@@ -57,14 +55,12 @@ JSC_CCALL(os_mailbox_push,
)
JSC_CCALL(os_register_actor,
cell_rt *rt = JS_GetContextOpaque(js);
const char *id = JS_ToCString(js, argv[0]);
double ar;
JS_ToFloat64(js, &ar, argv[3]);
const char *err = register_actor(id, rt, JS_ToBool(js, argv[2]), ar);
const char *err = register_actor(id, js, JS_ToBool(js, argv[2]), ar);
if (err) return JS_RaiseDisrupt(js, "Could not register actor: %s", err);
rt->message_handle_ref.val = argv[1];
rt->context = js;
js->message_handle_ref.val = argv[1];
JS_FreeCString(js, id);
)
@@ -77,56 +73,49 @@ JSC_CCALL(os_mailbox_exist,
)
JSC_CCALL(os_unneeded,
cell_rt *actor = JS_GetContextOpaque(js);
double secs;
JS_ToFloat64(js, &secs, argv[1]);
actor_unneeded(actor, argv[0], secs);
actor_unneeded(js, argv[0], secs);
)
JSC_CCALL(actor_disrupt,
cell_rt *crt = JS_GetContextOpaque(js);
actor_disrupt(crt);
actor_disrupt(js);
)
JSC_SCALL(actor_setname,
cell_rt *rt = JS_GetContextOpaque(js);
rt->name = strdup(str);
js->name = strdup(str);
)
JSC_CCALL(actor_on_exception,
cell_rt *rt = JS_GetContextOpaque(js);
rt->on_exception_ref.val = argv[0];
js->on_exception_ref.val = argv[0];
)
JSC_CCALL(actor_clock,
if (!JS_IsFunction(argv[0]))
return JS_RaiseDisrupt(js, "Argument must be a function.");
cell_rt *actor = JS_GetContextOpaque(js);
actor_clock(actor, argv[0]);
actor_clock(js, argv[0]);
)
JSC_CCALL(actor_delay,
if (!JS_IsFunction(argv[0]))
return JS_RaiseDisrupt(js, "Argument must be a function.");
cell_rt *actor = JS_GetContextOpaque(js);
double seconds;
JS_ToFloat64(js, &seconds, argv[1]);
if (seconds <= 0) {
actor_clock(actor, argv[0]);
actor_clock(js, argv[0]);
return JS_NULL;
}
uint32_t id = actor_delay(actor, argv[0], seconds);
uint32_t id = actor_delay(js, argv[0], seconds);
return JS_NewUint32(js, id);
)
JSC_CCALL(actor_removetimer,
cell_rt *actor = JS_GetContextOpaque(js);
uint32_t timer_id;
JS_ToUint32(js, &timer_id, argv[0]);
(void)actor_remove_timer(actor, timer_id);
(void)actor_remove_timer(js, timer_id);
)
/* Log callback bridge: called from JS_Log, calls ƿit log(channel, [msg, stack])

View File

@@ -1,30 +1,5 @@
/*
* QuickJS Javascript Engine
*
* Copyright (c) 2017-2025 Fabrice Bellard
* Copyright (c) 2017-2025 Charlie Gordon
*
* 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.
*/
#define BLOB_IMPLEMENTATION
#include "quickjs-internal.h"
#include "pit_internal.h"
#include <unistd.h>
// #define DUMP_BUDDY
@@ -87,7 +62,7 @@ static inline JS_BOOL JS_IsInteger (JSValue v) {
JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT;
/* === Public API wrappers (non-inline, for quickjs.h declarations) ===
These delegate to the static inline versions in quickjs-internal.h. */
These delegate to the static inline versions in pit_internal.h. */
JS_BOOL JS_IsStone(JSValue v) { return mist_is_stone(v); }
JS_BOOL JS_IsArray(JSValue v) { return mist_is_array(v); }
@@ -1943,10 +1918,8 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
/* Check memory limit — kill actor if heap exceeds cap */
if (ctx->heap_memory_limit > 0 && ctx->current_block_size > ctx->heap_memory_limit) {
#ifdef ACTOR_TRACE
void *crt = ctx->user_opaque;
if (crt)
fprintf(stderr, "[ACTOR_TRACE] heap %zu > limit %zu, OOM\n",
ctx->current_block_size, ctx->heap_memory_limit);
fprintf(stderr, "[ACTOR_TRACE] heap %zu > limit %zu, OOM\n",
ctx->current_block_size, ctx->heap_memory_limit);
#endif
return -1;
}
@@ -2080,6 +2053,18 @@ JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) {
ctx->log_callback_js = JS_NULL;
ctx->log_callback = NULL;
/* Register actor GCRef fields so the Cheney GC can relocate them. */
JS_AddGCRef(ctx, &ctx->idx_buffer_ref);
JS_AddGCRef(ctx, &ctx->on_exception_ref);
JS_AddGCRef(ctx, &ctx->message_handle_ref);
JS_AddGCRef(ctx, &ctx->unneeded_ref);
JS_AddGCRef(ctx, &ctx->actor_sym_ref);
ctx->idx_buffer_ref.val = JS_NULL;
ctx->on_exception_ref.val = JS_NULL;
ctx->message_handle_ref.val = JS_NULL;
ctx->unneeded_ref.val = JS_NULL;
ctx->actor_sym_ref.val = JS_NULL;
/* Initialize constant text pool (avoids overflow pages for common case) */
{
size_t ct_pool_size = 64 * 1024; /* 64KB initial CT pool */
@@ -2156,11 +2141,7 @@ JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size) {
return ctx;
}
void *JS_GetContextOpaque (JSContext *ctx) { return ctx->user_opaque; }
void JS_SetContextOpaque (JSContext *ctx, void *opaque) {
ctx->user_opaque = opaque;
}
void JS_SetGCScanExternal(JSContext *ctx, JS_GCScanFn fn) {
ctx->gc_scan_external = fn;
@@ -2190,6 +2171,11 @@ void JS_FreeContext (JSContext *ctx) {
cell_rt_free_native_state(ctx);
JS_DeleteGCRef(ctx, &ctx->suspended_frame_ref);
JS_DeleteGCRef(ctx, &ctx->idx_buffer_ref);
JS_DeleteGCRef(ctx, &ctx->on_exception_ref);
JS_DeleteGCRef(ctx, &ctx->message_handle_ref);
JS_DeleteGCRef(ctx, &ctx->unneeded_ref);
JS_DeleteGCRef(ctx, &ctx->actor_sym_ref);
for (i = 0; i < ctx->class_count; i++) {
}

View File

@@ -9,15 +9,14 @@
#include "stb_ds.h"
#include "cell.h"
#include "quickjs-internal.h"
#include "cell_internal.h"
#include "pit_internal.h"
#ifdef _WIN32
#include <windows.h>
#endif
typedef struct actor_node {
cell_rt *actor;
JSContext *actor;
struct actor_node *next;
} actor_node;
@@ -30,13 +29,13 @@ typedef enum {
typedef struct {
uint64_t execute_at_ns;
cell_rt *actor;
JSContext *actor;
uint32_t timer_id;
timer_type type;
uint32_t turn_gen; /* generation at registration time */
} timer_node;
static timer_node *timer_heap = NULL;
static timer_node *timer_heap = NULL;
// Priority queue indices
#define PQ_READY 0
@@ -92,7 +91,7 @@ static int has_any_work(actor_node *heads[]) {
}
static pthread_mutex_t *actors_mutex;
static struct { char *key; cell_rt *value; } *actors = NULL;
static struct { char *key; JSContext *value; } *actors = NULL;
#define lockless_shdel(NAME, KEY) pthread_mutex_lock(NAME##_mutex); shdel(NAME, KEY); pthread_mutex_unlock(NAME##_mutex);
#define lockless_shlen(NAME) ({ \
@@ -109,7 +108,7 @@ static struct { char *key; cell_rt *value; } *actors = NULL;
})
#define lockless_shget(NAME, KEY) ({ \
pthread_mutex_lock(NAME##_mutex); \
cell_rt *_actor = shget(NAME, KEY); \
JSContext *_actor = shget(NAME, KEY); \
pthread_mutex_unlock(NAME##_mutex); \
_actor; \
})
@@ -122,21 +121,21 @@ static struct { char *key; cell_rt *value; } *actors = NULL;
})
// Forward declarations
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval);
void actor_turn(cell_rt *actor);
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval);
void actor_turn(JSContext *actor);
void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type) {
void heap_push(uint64_t when, JSContext *actor, uint32_t timer_id, timer_type type) {
timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .type = type, .turn_gen = 0 };
if (type == TIMER_PAUSE || type == TIMER_KILL)
node.turn_gen = atomic_load_explicit(&actor->turn_gen, memory_order_relaxed);
arrput(timer_heap, node);
// Bubble up
int i = arrlen(timer_heap) - 1;
while (i > 0) {
int parent = (i - 1) / 2;
if (timer_heap[i].execute_at_ns >= timer_heap[parent].execute_at_ns) break;
timer_node tmp = timer_heap[i];
timer_heap[i] = timer_heap[parent];
timer_heap[parent] = tmp;
@@ -147,10 +146,10 @@ void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type
// Helper: Heap Pop
int heap_pop(timer_node *out) {
if (arrlen(timer_heap) == 0) return 0;
*out = timer_heap[0];
timer_node last = arrpop(timer_heap);
if (arrlen(timer_heap) > 0) {
timer_heap[0] = last;
// Bubble down
@@ -160,14 +159,14 @@ int heap_pop(timer_node *out) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int smallest = i;
if (left < n && timer_heap[left].execute_at_ns < timer_heap[smallest].execute_at_ns)
smallest = left;
if (right < n && timer_heap[right].execute_at_ns < timer_heap[smallest].execute_at_ns)
smallest = right;
if (smallest == i) break;
timer_node tmp = timer_heap[i];
timer_heap[i] = timer_heap[smallest];
timer_heap[smallest] = tmp;
@@ -180,7 +179,7 @@ int heap_pop(timer_node *out) {
void *timer_thread_func(void *arg) {
while (1) {
pthread_mutex_lock(&engine.lock);
if (engine.shutting_down) {
pthread_mutex_unlock(&engine.lock);
return NULL;
@@ -203,10 +202,10 @@ void *timer_thread_func(void *arg) {
uint32_t cur = atomic_load_explicit(&t.actor->turn_gen, memory_order_relaxed);
if (cur == t.turn_gen) {
if (t.type == TIMER_PAUSE) {
JS_SetPauseFlag(t.actor->context, 1);
JS_SetPauseFlag(t.actor, 1);
} else {
t.actor->disrupt = 1;
JS_SetPauseFlag(t.actor->context, 2);
JS_SetPauseFlag(t.actor, 2);
}
}
} else {
@@ -216,26 +215,26 @@ void *timer_thread_func(void *arg) {
JSValue cb = t.actor->timers[idx].value;
hmdel(t.actor->timers, t.timer_id);
actor_clock(t.actor, cb);
JS_FreeValue(t.actor->context, cb);
JS_FreeValue(t.actor, cb);
}
pthread_mutex_unlock(t.actor->msg_mutex);
}
continue;
continue;
} else {
// --- WAIT FOR DEADLINE ---
struct timespec ts;
uint64_t wait_ns = timer_heap[0].execute_at_ns - now;
// Convert relative wait time to absolute CLOCK_REALTIME
struct timespec now_real;
clock_gettime(CLOCK_REALTIME, &now_real);
uint64_t deadline_real_ns = (uint64_t)now_real.tv_sec * 1000000000ULL +
uint64_t deadline_real_ns = (uint64_t)now_real.tv_sec * 1000000000ULL +
(uint64_t)now_real.tv_nsec + wait_ns;
ts.tv_sec = deadline_real_ns / 1000000000ULL;
ts.tv_nsec = deadline_real_ns % 1000000000ULL;
pthread_cond_timedwait(&engine.timer_cond, &engine.lock, &ts);
}
}
@@ -244,7 +243,7 @@ void *timer_thread_func(void *arg) {
return NULL;
}
void enqueue_actor_priority(cell_rt *actor) {
void enqueue_actor_priority(JSContext *actor) {
actor_node *n = malloc(sizeof(actor_node));
n->actor = actor;
n->next = NULL;
@@ -301,10 +300,10 @@ void actor_initialize(void) {
engine.mq_head[i] = NULL;
engine.mq_tail[i] = NULL;
}
actors_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(actors_mutex, NULL);
// Start Timer Thread
pthread_create(&engine.timer_thread, NULL, timer_thread_func, NULL);
@@ -323,7 +322,7 @@ void actor_initialize(void) {
}
}
void actor_free(cell_rt *actor)
void actor_free(JSContext *actor)
{
if (actor->is_quiescent) {
actor->is_quiescent = 0;
@@ -364,42 +363,36 @@ void actor_free(cell_rt *actor)
pthread_mutex_lock(actor->msg_mutex);
pthread_mutex_lock(actor->mutex);
JSContext *js = actor->context;
JS_DeleteGCRef(js, &actor->idx_buffer_ref);
JS_DeleteGCRef(js, &actor->on_exception_ref);
JS_DeleteGCRef(js, &actor->message_handle_ref);
JS_DeleteGCRef(js, &actor->unneeded_ref);
JS_DeleteGCRef(js, &actor->actor_sym_ref);
for (int i = 0; i < hmlen(actor->timers); i++) {
JS_FreeValue(js, actor->timers[i].value);
JS_FreeValue(actor, actor->timers[i].value);
}
hmfree(actor->timers);
/* Free all letters in the queue */
for (int i = 0; i < arrlen(actor->letters); i++) {
if (actor->letters[i].type == LETTER_BLOB) {
blob_destroy(actor->letters[i].blob_data);
} else if (actor->letters[i].type == LETTER_CALLBACK) {
JS_FreeValue(js, actor->letters[i].callback);
JS_FreeValue(actor, actor->letters[i].callback);
}
}
arrfree(actor->letters);
JS_FreeContext(js);
free(actor->id);
pthread_mutex_unlock(actor->mutex);
pthread_mutex_destroy(actor->mutex);
free(actor->mutex);
pthread_mutex_unlock(actor->msg_mutex);
pthread_mutex_destroy(actor->msg_mutex);
free(actor->msg_mutex);
free(actor);
pthread_mutex_t *m = actor->mutex;
pthread_mutex_t *mm = actor->msg_mutex;
JS_FreeContext(actor);
pthread_mutex_unlock(m);
pthread_mutex_destroy(m);
free(m);
pthread_mutex_unlock(mm);
pthread_mutex_destroy(mm);
free(mm);
int actor_count = lockless_shlen(actors);
if (actor_count == 0) {
@@ -467,7 +460,7 @@ int actor_exists(const char *id)
return idx != -1;
}
void set_actor_state(cell_rt *actor)
void set_actor_state(JSContext *actor)
{
if (actor->disrupt) {
#ifdef SCHEDULER_DEBUG
@@ -548,7 +541,7 @@ void set_actor_state(cell_rt *actor)
pthread_mutex_unlock(actor->msg_mutex);
}
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval)
{
pthread_mutex_lock(actor->mutex);
// Check if this timer is still valid (match actor->ar)
@@ -556,22 +549,22 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
pthread_mutex_unlock(actor->mutex);
return 0;
}
actor->disrupt = 1;
if (!JS_IsNull(actor->unneeded_ref.val)) {
JSValue ret = JS_Call(actor->context, actor->unneeded_ref.val, JS_NULL, 0, NULL);
uncaught_exception(actor->context, ret);
JSValue ret = JS_Call(actor, actor->unneeded_ref.val, JS_NULL, 0, NULL);
uncaught_exception(actor, ret);
}
int should_free = (actor->state == ACTOR_IDLE);
pthread_mutex_unlock(actor->mutex);
if (should_free) actor_free(actor);
return 0;
}
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
void actor_unneeded(JSContext *actor, JSValue fn, double seconds)
{
if (actor->disrupt) return;
@@ -582,14 +575,14 @@ void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
actor->unneeded_ref.val = fn;
actor->ar_secs = seconds;
END:
if (actor->ar)
actor->ar = 0;
set_actor_state(actor);
}
cell_rt *get_actor(char *id)
JSContext *get_actor(char *id)
{
int idx = lockless_shgeti(actors, id);
if (idx == -1) {
@@ -622,17 +615,14 @@ void actor_loop()
}
}
cell_rt *create_actor(void *wota)
extern JSRuntime *g_runtime;
JSContext *create_actor(void *wota)
{
cell_rt *actor = calloc(sizeof(*actor), 1);
#ifdef HAVE_MIMALLOC
actor->heap = mi_heap_new();
#endif
JSContext *actor = JS_NewContext(g_runtime);
if (!actor) return NULL;
actor->init_wota = wota;
/* GCRef fields are registered after JSContext creation in script_startup.
For now, zero-init from calloc is sufficient (val = 0 = JS_MKVAL(JS_TAG_INT,0),
which is not a pointer so GC-safe). The actual JS_NULL assignment and
JS_AddGCRef happen in script_startup. */
arrsetcap(actor->letters, 5);
@@ -642,19 +632,22 @@ cell_rt *create_actor(void *wota)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(actor->mutex, &attr);
actor->msg_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(actor->msg_mutex, &attr); // msg_mutex can be recursive too to be safe
pthread_mutex_init(actor->msg_mutex, &attr);
pthread_mutexattr_destroy(&attr);
JS_SetGCScanExternal(actor, actor_gc_scan);
JS_SetHeapMemoryLimit(actor, ACTOR_MEMORY_LIMIT);
/* Lock actor->mutex while initializing JS runtime. */
pthread_mutex_lock(actor->mutex);
script_startup(actor);
set_actor_state(actor);
pthread_mutex_unlock(actor->mutex);
pthread_mutex_unlock(actor->mutex);
return actor;
}
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar)
const char *register_actor(const char *id, JSContext *actor, int mainthread, double ar)
{
actor->main_thread_only = mainthread;
actor->id = strdup(id);
@@ -676,29 +669,29 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
const char *send_message(const char *id, void *msg)
{
cell_rt *target = get_actor(id);
JSContext *target = get_actor(id);
if (!target) {
blob_destroy((blob *)msg);
return "Could not get actor from id.";
}
letter l;
l.type = LETTER_BLOB;
l.blob_data = (blob *)msg;
pthread_mutex_lock(target->msg_mutex);
arrput(target->letters, l);
pthread_mutex_unlock(target->msg_mutex);
if (target->ar)
target->ar = 0;
set_actor_state(target);
return NULL;
}
void actor_turn(cell_rt *actor)
void actor_turn(JSContext *actor)
{
pthread_mutex_lock(actor->mutex);
#ifdef ACTOR_TRACE
@@ -710,21 +703,21 @@ void actor_turn(cell_rt *actor)
actor->name ? actor->name : actor->id, prev_state);
#endif
if (!actor->trace_hook) {
if (!actor->actor_trace_hook) {
cell_hook gh = cell_trace_gethook();
if (gh) actor->trace_hook = gh;
if (gh) actor->actor_trace_hook = gh;
}
if (actor->trace_hook)
actor->trace_hook(actor, CELL_HOOK_ENTER);
if (actor->actor_trace_hook)
actor->actor_trace_hook(actor, CELL_HOOK_ENTER);
JSValue result;
if (actor->vm_suspended) {
/* RESUME path: continue suspended turn under kill timer only */
g_crash_ctx = actor->context;
g_crash_ctx = actor;
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
JS_SetPauseFlag(actor->context, 0);
JS_SetPauseFlag(actor, 0);
actor->turn_start_ns = cell_ns();
/* Register kill timer only for resume */
@@ -734,7 +727,7 @@ void actor_turn(cell_rt *actor)
pthread_cond_signal(&engine.timer_cond);
pthread_mutex_unlock(&engine.lock);
result = JS_ResumeRegisterVM(actor->context);
result = JS_ResumeRegisterVM(actor);
actor->vm_suspended = 0;
if (JS_IsSuspended(result)) {
@@ -743,7 +736,7 @@ void actor_turn(cell_rt *actor)
goto ENDTURN;
}
if (JS_IsException(result)) {
if (!uncaught_exception(actor->context, result))
if (!uncaught_exception(actor, result))
actor->disrupt = 1;
}
actor->slow_strikes++;
@@ -778,7 +771,7 @@ void actor_turn(cell_rt *actor)
pthread_mutex_unlock(actor->msg_mutex);
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
JS_SetPauseFlag(actor->context, 0);
JS_SetPauseFlag(actor, 0);
actor->turn_start_ns = cell_ns();
/* Register both pause and kill timers */
@@ -790,35 +783,35 @@ void actor_turn(cell_rt *actor)
pthread_cond_signal(&engine.timer_cond);
pthread_mutex_unlock(&engine.lock);
g_crash_ctx = actor->context;
g_crash_ctx = actor;
if (l.type == LETTER_BLOB) {
size_t size = blob_length(l.blob_data) / 8;
JSValue arg = js_new_blob_stoned_copy(actor->context,
JSValue arg = js_new_blob_stoned_copy(actor,
(void *)blob_data(l.blob_data), size);
blob_destroy(l.blob_data);
result = JS_Call(actor->context, actor->message_handle_ref.val,
result = JS_Call(actor, actor->message_handle_ref.val,
JS_NULL, 1, &arg);
if (JS_IsSuspended(result)) {
actor->vm_suspended = 1;
actor->state = ACTOR_SLOW;
JS_FreeValue(actor->context, arg);
JS_FreeValue(actor, arg);
goto ENDTURN_SLOW;
}
if (!uncaught_exception(actor->context, result))
if (!uncaught_exception(actor, result))
actor->disrupt = 1;
JS_FreeValue(actor->context, arg);
JS_FreeValue(actor, arg);
} else if (l.type == LETTER_CALLBACK) {
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
result = JS_Call(actor, l.callback, JS_NULL, 0, NULL);
if (JS_IsSuspended(result)) {
actor->vm_suspended = 1;
actor->state = ACTOR_SLOW;
JS_FreeValue(actor->context, l.callback);
JS_FreeValue(actor, l.callback);
goto ENDTURN_SLOW;
}
if (!uncaught_exception(actor->context, result))
if (!uncaught_exception(actor, result))
actor->disrupt = 1;
JS_FreeValue(actor->context, l.callback);
JS_FreeValue(actor, l.callback);
}
if (actor->disrupt) goto ENDTURN;
@@ -830,8 +823,8 @@ ENDTURN:
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
actor->state = ACTOR_IDLE;
if (actor->trace_hook)
actor->trace_hook(actor, CELL_HOOK_EXIT);
if (actor->actor_trace_hook)
actor->actor_trace_hook(actor, CELL_HOOK_EXIT);
if (actor->disrupt) {
#ifdef SCHEDULER_DEBUG
@@ -857,8 +850,8 @@ ENDTURN_SLOW:
fprintf(stderr, "[ACTOR_TRACE] %s: suspended mid-turn -> SLOW\n",
actor->name ? actor->name : actor->id);
#endif
if (actor->trace_hook)
actor->trace_hook(actor, CELL_HOOK_EXIT);
if (actor->actor_trace_hook)
actor->actor_trace_hook(actor, CELL_HOOK_EXIT);
enqueue_actor_priority(actor);
pthread_mutex_unlock(actor->mutex);
}
@@ -869,51 +862,50 @@ void actor_gc_scan(JSContext *ctx,
uint8_t *from_base, uint8_t *from_end,
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end)
{
cell_rt *actor = JS_GetContextOpaque(ctx);
if (!actor) return;
/* Lock msg_mutex to synchronize with the timer thread, which reads
timers and writes letters under the same lock. */
pthread_mutex_lock(actor->msg_mutex);
if (ctx->msg_mutex)
pthread_mutex_lock(ctx->msg_mutex);
for (int i = 0; i < arrlen(actor->letters); i++) {
if (actor->letters[i].type == LETTER_CALLBACK) {
actor->letters[i].callback = gc_copy_value(ctx,
actor->letters[i].callback,
for (int i = 0; i < arrlen(ctx->letters); i++) {
if (ctx->letters[i].type == LETTER_CALLBACK) {
ctx->letters[i].callback = gc_copy_value(ctx,
ctx->letters[i].callback,
from_base, from_end, to_base, to_free, to_end);
}
}
for (int i = 0; i < hmlen(actor->timers); i++) {
actor->timers[i].value = gc_copy_value(ctx,
actor->timers[i].value,
for (int i = 0; i < hmlen(ctx->timers); i++) {
ctx->timers[i].value = gc_copy_value(ctx,
ctx->timers[i].value,
from_base, from_end, to_base, to_free, to_end);
}
pthread_mutex_unlock(actor->msg_mutex);
if (ctx->msg_mutex)
pthread_mutex_unlock(ctx->msg_mutex);
}
void actor_clock(cell_rt *actor, JSValue fn)
void actor_clock(JSContext *actor, JSValue fn)
{
letter l;
l.type = LETTER_CALLBACK;
l.callback = JS_DupValue(actor->context, fn);
l.callback = JS_DupValue(actor, fn);
pthread_mutex_lock(actor->msg_mutex);
arrput(actor->letters, l);
pthread_mutex_unlock(actor->msg_mutex);
set_actor_state(actor);
}
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
uint32_t actor_delay(JSContext *actor, JSValue fn, double seconds)
{
pthread_mutex_lock(actor->msg_mutex);
pthread_mutex_lock(actor->msg_mutex);
static uint32_t global_timer_id = 1;
uint32_t id = global_timer_id++;
JSValue cb = JS_DupValue(actor->context, fn);
JSValue cb = JS_DupValue(actor, fn);
hmput(actor->timers, id, cb);
uint64_t now = cell_ns();
uint64_t execute_at = now + (uint64_t)(seconds * 1e9);
@@ -928,7 +920,7 @@ uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
return id;
}
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id)
JSValue actor_remove_timer(JSContext *actor, uint32_t timer_id)
{
JSValue cb = JS_NULL;
pthread_mutex_lock(actor->msg_mutex);

View File

@@ -1,19 +1,19 @@
#include "stb_ds.h"
#include "cell.h"
#include "cell_internal.h"
#include "pit_internal.h"
// --- Data Structures ---
// Simple linked list for the ready queue
typedef struct actor_node {
cell_rt *actor;
JSContext *actor;
struct actor_node *next;
} actor_node;
// Timer node for the min-heap
typedef struct {
uint64_t execute_at_ns;
cell_rt *actor;
JSContext *actor;
uint32_t timer_id;
int is_native; // 1 for native remove timer, 0 for JS timer
} timer_node;
@@ -24,27 +24,27 @@ static actor_node *ready_head = NULL;
static actor_node *ready_tail = NULL;
static timer_node *timer_heap = NULL; // stb_ds array
static struct { char *key; cell_rt *value; } *actors = NULL; // stb_ds hashmap
static struct { char *key; JSContext *value; } *actors = NULL; // stb_ds hashmap
static int shutting_down = 0;
// --- Forward Declarations ---
void actor_turn(cell_rt *actor);
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval);
void actor_turn(JSContext *actor);
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval);
// --- Heap Helpers ---
static void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, int is_native) {
static void heap_push(uint64_t when, JSContext *actor, uint32_t timer_id, int is_native) {
timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .is_native = is_native };
arrput(timer_heap, node);
// Bubble up
int i = arrlen(timer_heap) - 1;
while (i > 0) {
int parent = (i - 1) / 2;
if (timer_heap[i].execute_at_ns >= timer_heap[parent].execute_at_ns) break;
timer_node tmp = timer_heap[i];
timer_heap[i] = timer_heap[parent];
timer_heap[parent] = tmp;
@@ -54,10 +54,10 @@ static void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, int is_n
static int heap_pop(timer_node *out) {
if (arrlen(timer_heap) == 0) return 0;
*out = timer_heap[0];
timer_node last = arrpop(timer_heap);
if (arrlen(timer_heap) > 0) {
timer_heap[0] = last;
// Bubble down
@@ -67,14 +67,14 @@ static int heap_pop(timer_node *out) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int smallest = i;
if (left < n && timer_heap[left].execute_at_ns < timer_heap[smallest].execute_at_ns)
smallest = left;
if (right < n && timer_heap[right].execute_at_ns < timer_heap[smallest].execute_at_ns)
smallest = right;
if (smallest == i) break;
timer_node tmp = timer_heap[i];
timer_heap[i] = timer_heap[smallest];
timer_heap[smallest] = tmp;
@@ -90,10 +90,10 @@ void actor_initialize(void)
{
}
void actor_free(cell_rt *actor)
void actor_free(JSContext *actor)
{
shdel(actors, actor->id);
// Remove from ready queue if present (O(N))
if (ready_head) {
if (ready_head->actor == actor) {
@@ -115,43 +115,33 @@ void actor_free(cell_rt *actor)
}
}
}
JSContext *js = actor->context;
JS_DeleteGCRef(js, &actor->idx_buffer_ref);
JS_DeleteGCRef(js, &actor->on_exception_ref);
JS_DeleteGCRef(js, &actor->message_handle_ref);
JS_DeleteGCRef(js, &actor->unneeded_ref);
JS_DeleteGCRef(js, &actor->actor_sym_ref);
/* Free timer callbacks stored in actor */
for (int i = 0; i < hmlen(actor->timers); i++) {
JS_FreeValue(js, actor->timers[i].value);
JS_FreeValue(actor, actor->timers[i].value);
}
hmfree(actor->timers);
/* Free all letters in the queue */
for (int i = 0; i < arrlen(actor->letters); i++) {
if (actor->letters[i].type == LETTER_BLOB) {
blob_destroy(actor->letters[i].blob_data);
} else if (actor->letters[i].type == LETTER_CALLBACK) {
JS_FreeValue(js, actor->letters[i].callback);
JS_FreeValue(actor, actor->letters[i].callback);
}
}
arrfree(actor->letters);
JS_FreeContext(js);
free(actor->id);
free(actor);
JS_FreeContext(actor);
int actor_count = shlen(actors);
if (actor_count == 0) exit(0);
}
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
void actor_unneeded(JSContext *actor, JSValue fn, double seconds)
{
if (actor->disrupt) return;
@@ -162,15 +152,10 @@ void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
actor->unneeded_ref.val = fn;
actor->ar_secs = seconds;
END:
// If there was an existing unneeded timer, it will be handled/ignored in set_actor_state logic
// or we can explicitly invalidate it if we tracked the ID.
// For now, set_actor_state will schedule a new one if idle.
if (actor->ar) {
// In single threaded, we can't easily remove from heap O(N),
// but we can just let it fire and check ID match.
actor->ar = 0;
actor->ar = 0;
}
set_actor_state(actor);
}
@@ -195,15 +180,15 @@ int actor_exists(const char *id)
return idx != -1;
}
void set_actor_state(cell_rt *actor)
void set_actor_state(JSContext *actor)
{
if (actor->disrupt) {
actor_free(actor);
return;
}
// No mutex needed in single threaded
switch(actor->state) {
case ACTOR_RUNNING:
case ACTOR_READY:
@@ -212,59 +197,59 @@ void set_actor_state(cell_rt *actor)
actor->ar = 0;
}
break;
case ACTOR_IDLE:
if (arrlen(actor->letters)) {
actor->state = ACTOR_READY;
// Add to ready queue
actor_node *n = malloc(sizeof(actor_node));
n->actor = actor;
n->next = NULL;
if (ready_tail) {
ready_tail->next = n;
} else {
ready_head = n;
}
ready_tail = n;
} else if (!arrlen(actor->letters) && !hmlen(actor->timers)) {
// Schedule remove timer
static uint32_t global_timer_id = 1;
uint32_t id = global_timer_id++;
actor->ar = id;
uint64_t now = cell_ns();
uint64_t execute_at = now + (uint64_t)(actor->ar_secs * 1e9);
heap_push(execute_at, actor, id, 1); // 1 = native remove
}
break;
}
}
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
uint32_t actor_remove_cb(JSContext *actor, uint32_t id, uint32_t interval)
{
// Check if this timer is still valid (match actor->ar)
if (actor->ar != id && id != 0) {
if (actor->ar != id && id != 0) {
return 0;
}
actor->disrupt = 1;
if (!JS_IsNull(actor->unneeded_ref.val)) {
JSValue ret = JS_Call(actor->context, actor->unneeded_ref.val, JS_NULL, 0, NULL);
uncaught_exception(actor->context, ret);
JSValue ret = JS_Call(actor, actor->unneeded_ref.val, JS_NULL, 0, NULL);
uncaught_exception(actor, ret);
}
int should_free = (actor->state == ACTOR_IDLE);
if (should_free) actor_free(actor);
return 0;
}
cell_rt *get_actor(char *id)
JSContext *get_actor(char *id)
{
int idx = shgeti(actors, id);
if (idx == -1) {
@@ -282,19 +267,19 @@ void actor_loop()
actor_node *node = ready_head;
ready_head = node->next;
if (!ready_head) ready_tail = NULL;
actor_turn(node->actor);
free(node);
continue; // Loop again to check for more work or timers
}
// 2. Check Timers
uint64_t now = cell_ns();
if (arrlen(timer_heap) > 0) {
if (timer_heap[0].execute_at_ns <= now) {
timer_node t;
heap_pop(&t);
if (t.is_native) {
actor_remove_cb(t.actor, t.timer_id, 0);
} else {
@@ -304,7 +289,7 @@ void actor_loop()
JSValue cb = t.actor->timers[idx].value;
hmdel(t.actor->timers, t.timer_id);
actor_clock(t.actor, cb);
JS_FreeValue(t.actor->context, cb);
JS_FreeValue(t.actor, cb);
}
}
continue; // Loop again
@@ -320,30 +305,32 @@ void actor_loop()
}
}
cell_rt *create_actor(void *wota)
extern JSRuntime *g_runtime;
JSContext *create_actor(void *wota)
{
cell_rt *actor = calloc(sizeof(*actor), 1);
JSContext *actor = JS_NewContext(g_runtime);
if (!actor) return NULL;
actor->init_wota = wota;
/* GCRef fields are registered after JSContext creation in script_startup. */
arrsetcap(actor->letters, 5);
// No mutexes needed
actor->mutex = NULL;
actor->msg_mutex = NULL;
JS_SetGCScanExternal(actor, actor_gc_scan);
JS_SetHeapMemoryLimit(actor, ACTOR_MEMORY_LIMIT);
script_startup(actor);
set_actor_state(actor);
return actor;
}
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar)
const char *register_actor(const char *id, JSContext *actor, int mainthread, double ar)
{
actor->main_thread_only = mainthread;
actor->id = strdup(id);
actor->ar_secs = ar;
if (shgeti(actors, id) != -1) {
free(actor->id);
return "Actor with given ID already exists.";
@@ -355,82 +342,82 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
const char *send_message(const char *id, void *msg)
{
cell_rt *target = get_actor(id);
JSContext *target = get_actor(id);
if (!target) {
blob_destroy((blob *)msg);
return "Could not get actor from id.";
}
letter l;
l.type = LETTER_BLOB;
l.blob_data = (blob *)msg;
arrput(target->letters, l);
if (target->ar) {
// Invalidate unneeded timer
target->ar = 0;
}
set_actor_state(target);
return NULL;
}
void actor_turn(cell_rt *actor)
void actor_turn(JSContext *actor)
{
actor->state = ACTOR_RUNNING;
actor->state = ACTOR_RUNNING;
JSValue result;
TAKETURN:
if (!arrlen(actor->letters)) {
goto ENDTURN;
}
letter l = actor->letters[0];
arrdel(actor->letters, 0);
if (l.type == LETTER_BLOB) {
arrdel(actor->letters, 0);
if (l.type == LETTER_BLOB) {
// Create a JS blob from the C blob
size_t size = blob_length(l.blob_data) / 8; // Convert bits to bytes
JSValue arg = js_new_blob_stoned_copy(actor->context, (void *)blob_data(l.blob_data), size);
JSValue arg = js_new_blob_stoned_copy(actor, (void *)blob_data(l.blob_data), size);
blob_destroy(l.blob_data);
result = JS_Call(actor->context, actor->message_handle_ref.val, JS_NULL, 1, &arg);
uncaught_exception(actor->context, result);
JS_FreeValue(actor->context, arg);
result = JS_Call(actor, actor->message_handle_ref.val, JS_NULL, 1, &arg);
uncaught_exception(actor, result);
JS_FreeValue(actor, arg);
} else if (l.type == LETTER_CALLBACK) {
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
uncaught_exception(actor->context, result);
JS_FreeValue(actor->context, l.callback);
result = JS_Call(actor, l.callback, JS_NULL, 0, NULL);
uncaught_exception(actor, result);
JS_FreeValue(actor, l.callback);
}
if (actor->disrupt) goto ENDTURN;
ENDTURN:
actor->state = ACTOR_IDLE;
actor->state = ACTOR_IDLE;
set_actor_state(actor);
}
void actor_clock(cell_rt *actor, JSValue fn)
void actor_clock(JSContext *actor, JSValue fn)
{
letter l;
l.type = LETTER_CALLBACK;
l.callback = JS_DupValue(actor->context, fn);
l.callback = JS_DupValue(actor, fn);
arrput(actor->letters, l);
set_actor_state(actor);
}
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
uint32_t actor_delay(JSContext *actor, JSValue fn, double seconds)
{
static uint32_t global_timer_id = 1;
uint32_t id = global_timer_id++;
JSValue cb = JS_DupValue(actor->context, fn);
JSValue cb = JS_DupValue(actor, fn);
hmput(actor->timers, id, cb);
uint64_t now = cell_ns();
uint64_t execute_at = now + (uint64_t)(seconds * 1e9);
@@ -439,7 +426,7 @@ uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
return id;
}
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id)
JSValue actor_remove_timer(JSContext *actor, uint32_t timer_id)
{
JSValue cb = JS_NULL;
int id = hmgeti(actor->timers, timer_id);

View File

@@ -5,7 +5,7 @@
*/
#include "cell.h"
#include "quickjs-internal.h"
#include "pit_internal.h"
#include <stdio.h>
#include <string.h>
#include <math.h>