Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cl): QuickJS bridge — embed JS piece runner in Common Lisp binary

Scaffolding for running .mjs pieces on the CL variant:
- quickjs-shim.c: C wrapper for NaN-boxed JSValue (CFFI-safe)
- quickjs-ffi.lisp: CFFI bindings to QuickJS + shim
- js-bridge.lisp: registers CL graphics/audio as JS API, drives
piece lifecycle (boot/paint/act/sim) through QuickJS
- docker-build.sh: builds libquickjs.so + libquickjs-shim.so for CL

The bridge creates a __ac_api object in JS with wipe/ink/line/box/
circle/sound.synth/system.jump etc., backed by CL callbacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+603 -3
+3
fedac/native/cl/ac-native.asd
··· 31 31 (:file "audio") 32 32 ;; Main 33 33 (:file "config") 34 + ;; QuickJS bridge (JS piece runner) 35 + (:file "quickjs-ffi") 36 + (:file "js-bridge") 34 37 (:file "main") 35 38 ;; Build 36 39 (:file "build")))
+248
fedac/native/cl/js-bridge.lisp
··· 1 + ;;; js-bridge.lisp — Bridge between QuickJS and CL graphics/audio/input 2 + ;;; 3 + ;;; This creates a QuickJS context, registers CL functions as the JS API 4 + ;;; (wipe, ink, line, box, circle, sound.synth, etc.), loads .mjs pieces, 5 + ;;; and drives the piece lifecycle (boot, paint, act, sim) each frame. 6 + 7 + (in-package :ac-native.js-bridge) 8 + 9 + (defvar *ctx* nil "Active QuickJS context") 10 + (defvar *rt* nil "Active QuickJS runtime") 11 + (defvar *graph* nil "Current graph context for JS callbacks") 12 + (defvar *fb* nil "Current framebuffer for JS callbacks") 13 + (defvar *audio* nil "Audio engine for JS callbacks") 14 + (defvar *screen-w* 0 "Screen width") 15 + (defvar *screen-h* 0 "Screen height") 16 + (defvar *has-boot* nil) 17 + (defvar *has-paint* nil) 18 + (defvar *has-act* nil) 19 + (defvar *has-sim* nil) 20 + 21 + ;;; ── Initialize QuickJS and register the API ── 22 + 23 + (defun js-init (graph fb audio screen-w screen-h) 24 + "Create QuickJS runtime and context, register CL functions as JS API." 25 + (setf *graph* graph 26 + *fb* fb 27 + *audio* audio 28 + *screen-w* screen-w 29 + *screen-h* screen-h) 30 + (setf *rt* (ac-native.quickjs:js-new-runtime)) 31 + (setf *ctx* (ac-native.quickjs:js-new-context *rt*)) 32 + ;; Register the API bootstrap code 33 + (let ((api-code (build-api-js))) 34 + (ac-native.quickjs:qjs-eval *ctx* api-code (length api-code) "<api-init>" 0)) 35 + (format *error-output* "[js-bridge] QuickJS initialized~%") 36 + (force-output *error-output*)) 37 + 38 + (defun js-destroy () 39 + "Free QuickJS context and runtime." 40 + (when *ctx* (ac-native.quickjs:js-free-context *ctx*) (setf *ctx* nil)) 41 + (when *rt* (ac-native.quickjs:js-free-runtime *rt*) (setf *rt* nil))) 42 + 43 + ;;; ── Load and run a piece ── 44 + 45 + (defun js-load-piece (path) 46 + "Load a .mjs piece file and detect lifecycle functions." 47 + (let ((code (with-open-file (s path :direction :input :if-does-not-exist nil) 48 + (when s 49 + (let ((buf (make-string (file-length s)))) 50 + (read-sequence buf s) 51 + buf))))) 52 + (unless code 53 + (format *error-output* "[js-bridge] Piece not found: ~A~%" path) 54 + (return-from js-load-piece nil)) 55 + ;; Wrap module exports into globals for lifecycle calling 56 + (let ((wrapper (format nil "~A~%~ 57 + if (typeof boot === 'function') globalThis.__piece_boot = boot;~%~ 58 + if (typeof paint === 'function') globalThis.__piece_paint = paint;~%~ 59 + if (typeof act === 'function') globalThis.__piece_act = act;~%~ 60 + if (typeof sim === 'function') globalThis.__piece_sim = sim;~%~ 61 + if (typeof leave === 'function') globalThis.__piece_leave = leave;~%" 62 + code))) 63 + (let ((rc (ac-native.quickjs:qjs-eval *ctx* wrapper (length wrapper) 64 + path 0))) 65 + (when (= rc -1) 66 + (format *error-output* "[js-bridge] Failed to load ~A~%" path) 67 + (return-from js-load-piece nil)))) 68 + ;; Detect which lifecycle functions exist 69 + (setf *has-boot* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_boot"))) 70 + (setf *has-paint* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_paint"))) 71 + (setf *has-act* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_act"))) 72 + (setf *has-sim* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_sim"))) 73 + (format *error-output* "[js-bridge] Loaded ~A (boot=~A paint=~A act=~A sim=~A)~%" 74 + path *has-boot* *has-paint* *has-act* *has-sim*) 75 + (force-output *error-output*) 76 + t)) 77 + 78 + ;;; ── Lifecycle calls ── 79 + 80 + (defun js-boot () 81 + "Call the piece's boot() with the API object." 82 + (when *has-boot* 83 + (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_boot") 84 + (ac-native.quickjs:qjs-execute-pending *ctx*))) 85 + 86 + (defun js-paint (paint-count) 87 + "Call the piece's paint() with the API object." 88 + (when *has-paint* 89 + (ac-native.quickjs:qjs-set-global-int *ctx* "__paintCount" paint-count) 90 + (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_paint") 91 + (ac-native.quickjs:qjs-execute-pending *ctx*))) 92 + 93 + (defun js-act (event-type event-key event-code) 94 + "Call the piece's act() with an event." 95 + (when *has-act* 96 + (ac-native.quickjs:qjs-set-global-int *ctx* "__evType" event-type) 97 + (ac-native.quickjs:qjs-set-global-string *ctx* "__evKey" event-key) 98 + (ac-native.quickjs:qjs-set-global-int *ctx* "__evCode" event-code) 99 + (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_act") 100 + (ac-native.quickjs:qjs-execute-pending *ctx*))) 101 + 102 + (defun js-sim () 103 + "Call the piece's sim()." 104 + (when *has-sim* 105 + (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_sim") 106 + (ac-native.quickjs:qjs-execute-pending *ctx*))) 107 + 108 + ;;; ── Build the JS API object ── 109 + ;;; This JS code creates __ac_api with all the graphics/audio/system 110 + ;;; functions that call back into native CL via registered C functions. 111 + 112 + (defun build-api-js () 113 + "Return JS source that builds the __ac_api object." 114 + (format nil " 115 + // Console 116 + globalThis.console = { 117 + log: function(...args) { __cl_log(args.join(' ')); }, 118 + warn: function(...args) { __cl_log('[warn] ' + args.join(' ')); }, 119 + error: function(...args) { __cl_log('[error] ' + args.join(' ')); }, 120 + }; 121 + 122 + // Screen dimensions (updated each frame by CL) 123 + globalThis.__paintCount = 0; 124 + globalThis.__evType = 0; 125 + globalThis.__evKey = ''; 126 + globalThis.__evCode = 0; 127 + 128 + // Event helper 129 + function makeEvent() { 130 + return { 131 + _type: __evType, 132 + _key: __evKey, 133 + code: __evCode, 134 + key: __evKey, 135 + name: __evKey, 136 + x: 0, y: 0, 137 + repeat: false, 138 + velocity: 255, 139 + pressure: 0, 140 + is: function(pattern) { 141 + const parts = pattern.split(':'); 142 + if (parts[0] === 'keyboard') { 143 + const dir = parts[1]; // 'down' or 'up' 144 + const key = parts[2]; 145 + if (dir === 'down' && __evType === 1 && __evKey === key) return true; 146 + if (dir === 'up' && __evType === 0 && __evKey === key) return true; 147 + return false; 148 + } 149 + return false; 150 + } 151 + }; 152 + } 153 + 154 + // Build API object passed to lifecycle functions 155 + globalThis.__ac_api = { 156 + wipe: function(r, g, b) { __cl_wipe(r||0, g||0, b||0); }, 157 + ink: function(r, g, b, a) { __cl_ink(r||255, g||255, b||255, a===undefined?255:a); return __ac_api; }, 158 + line: function(x1, y1, x2, y2) { __cl_line(x1, y1, x2, y2); }, 159 + box: function(x, y, w, h) { __cl_box(x, y, w, h); }, 160 + circle: function(x, y, r) { __cl_circle(x, y, r); }, 161 + plot: function(x, y) { __cl_plot(x, y); }, 162 + write: function(text, opts) { 163 + var x = 0, y = 0; 164 + if (opts && typeof opts === 'object') { x = opts.x || 0; y = opts.y || 0; } 165 + __cl_write(String(text), x, y); 166 + }, 167 + screen: { get width() { return __cl_screen_w(); }, get height() { return __cl_screen_h(); } }, 168 + event: makeEvent(), 169 + paintCount: 0, 170 + num: { 171 + rand: function() { return Math.random(); }, 172 + randIntRange: function(a, b) { return Math.floor(Math.random() * (b - a + 1)) + a; }, 173 + clamp: function(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }, 174 + lerp: function(a, b, t) { return a + (b - a) * t; }, 175 + dist: function(x1, y1, x2, y2) { return Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)); }, 176 + abs: Math.abs, floor: Math.floor, ceil: Math.ceil, round: Math.round, 177 + sign: Math.sign, min: Math.min, max: Math.max, 178 + map: function(v, a, b, c, d) { return c + (v - a) / (b - a) * (d - c); }, 179 + }, 180 + sound: { 181 + synth: function(cfg) { 182 + var type = cfg.type || 0; 183 + var freq = cfg.tone || cfg.freq || 440; 184 + var vol = cfg.volume !== undefined ? cfg.volume : 0.7; 185 + var dur = cfg.duration || -1; 186 + var attack = cfg.attack || 0; 187 + var decay = cfg.decay || 0; 188 + var pan = cfg.pan || 0; 189 + var id = __cl_synth(type, freq, vol, dur, attack, decay, pan); 190 + return { 191 + id: id, 192 + kill: function(fade) { __cl_synth_kill(id, fade || 0); }, 193 + update: function(opts) { /* TODO */ }, 194 + }; 195 + }, 196 + kill: function(id, fade) { __cl_synth_kill(id, fade || 0); }, 197 + bpm: function(v) { if (v !== undefined) __cl_set_bpm(v); return __cl_get_bpm(); }, 198 + time: 0, 199 + speaker: { 200 + poll: function() {}, 201 + sampleRate: 48000, 202 + waveforms: { left: [] }, 203 + amplitudes: { left: 0, right: 0 }, 204 + frequencies: { left: [] }, 205 + systemVolume: 70, 206 + }, 207 + room: { toggle: function(){}, setMix: function(){}, mix: 0 }, 208 + fx: { setMix: function(){}, mix: 0 }, 209 + glitch: { toggle: function(){} }, 210 + microphone: { open: function(){}, close: function(){}, connected: false }, 211 + sample: { play: function(){}, kill: function(){} }, 212 + speak: function(text) { __cl_log('[tts] ' + text); }, 213 + }, 214 + system: { 215 + version: 'cl-bridge', 216 + jump: function(piece) { __cl_jump(piece); }, 217 + reboot: function() { __cl_reboot(); }, 218 + poweroff: function() { __cl_poweroff(); }, 219 + readFile: function(path) { return __cl_read_file(path); }, 220 + config: { handle: '', piece: '' }, 221 + brightness: -1, 222 + battery: { percent: -1, charging: false, status: 'Unknown' }, 223 + hw: { model: '', vendor: '', cpu: '', cores: 0 }, 224 + }, 225 + wifi: { 226 + scan: function() { __cl_log('[wifi] scan requested'); }, 227 + connect: function(ssid, pass) { __cl_log('[wifi] connect: ' + ssid); }, 228 + disconnect: function() { __cl_log('[wifi] disconnect'); }, 229 + }, 230 + store: { 231 + retrieve: function(k, d) { return Promise.resolve(d); }, 232 + persist: function() { return Promise.resolve(); }, 233 + delete: function() { return Promise.resolve(); }, 234 + }, 235 + net: { 236 + preload: function() { return Promise.resolve(); }, 237 + pieces: function() { return Promise.resolve([]); }, 238 + }, 239 + clock: { time: function() { return Date.now(); } }, 240 + params: [], 241 + colon: [], 242 + needsPaint: function() {}, 243 + help: { resampleArray: function(a,n) { return a; } }, 244 + }; 245 + 246 + // Performance timer 247 + globalThis.performance = { now: function() { return Date.now(); } }; 248 + "))
+19 -1
fedac/native/cl/packages.lisp
··· 86 86 #:config-claude-token #:config-github-pat #:config-wifi 87 87 #:write-device-tokens)) 88 88 89 + (defpackage :ac-native.quickjs 90 + (:use :cl :cffi) 91 + (:export #:js-new-runtime #:js-free-runtime 92 + #:js-new-context #:js-free-context 93 + #:qjs-eval #:qjs-eval-module 94 + #:qjs-get-global-string #:qjs-set-global-int 95 + #:qjs-set-global-float #:qjs-set-global-string 96 + #:qjs-register-func #:qjs-call-global #:qjs-call-with-api 97 + #:qjs-arg-int #:qjs-arg-float #:qjs-arg-string 98 + #:qjs-check-exception #:qjs-execute-pending 99 + #:qjs-has-global-func)) 100 + 101 + (defpackage :ac-native.js-bridge 102 + (:use :cl) 103 + (:export #:js-init #:js-destroy #:js-load-piece 104 + #:js-boot #:js-paint #:js-act #:js-sim)) 105 + 89 106 (defpackage :ac-native 90 107 (:use :cl :ac-native.util :ac-native.color :ac-native.framebuffer 91 108 :ac-native.drm :ac-native.graph :ac-native.font 92 - :ac-native.input :ac-native.audio :ac-native.config) 109 + :ac-native.input :ac-native.audio :ac-native.config 110 + :ac-native.js-bridge) 93 111 (:export #:main)) 94 112 95 113 (defpackage :ac-native.build
+133
fedac/native/cl/quickjs-ffi.lisp
··· 1 + ;;; QuickJS CFFI bindings for AC Native OS 2 + ;;; Embeds QuickJS into the Common Lisp runtime so .mjs pieces can run. 3 + 4 + (in-package :ac-native.quickjs) 5 + 6 + ;;; ── Library loading ── 7 + 8 + (cffi:define-foreign-library libquickjs 9 + (:unix "libquickjs.so") 10 + (t (:default "libquickjs"))) 11 + 12 + (cffi:use-foreign-library libquickjs) 13 + 14 + ;;; ── Constants ── 15 + 16 + (defconstant +js-tag-int+ 0) 17 + (defconstant +js-tag-bool+ 1) 18 + (defconstant +js-tag-null+ 2) 19 + (defconstant +js-tag-undefined+ 3) 20 + (defconstant +js-tag-float64+ 7) 21 + (defconstant +js-tag-string+ -7) 22 + (defconstant +js-tag-object+ -1) 23 + 24 + (defconstant +js-eval-type-global+ 0) 25 + (defconstant +js-eval-type-module+ 1) 26 + 27 + ;;; ── Opaque types ── 28 + 29 + (cffi:defctype js-runtime :pointer) 30 + (cffi:defctype js-context :pointer) 31 + ;; JSValue is a 64-bit union on x86-64 (NaN-boxing). We pass as two uint32 words. 32 + ;; For simplicity, represent as a 2-element struct or pass via helper C shim. 33 + 34 + ;;; ── Core runtime functions ── 35 + 36 + (cffi:defcfun ("JS_NewRuntime" js-new-runtime) js-runtime) 37 + 38 + (cffi:defcfun ("JS_FreeRuntime" js-free-runtime) :void 39 + (rt js-runtime)) 40 + 41 + (cffi:defcfun ("JS_NewContext" js-new-context) js-context 42 + (rt js-runtime)) 43 + 44 + (cffi:defcfun ("JS_FreeContext" js-free-context) :void 45 + (ctx js-context)) 46 + 47 + ;;; ── Helper shim functions (defined in quickjs-shim.c) ── 48 + ;;; These wrap the NaN-boxed JSValue into pointer-safe calls. 49 + 50 + (cffi:defcfun ("qjs_eval" qjs-eval) :int 51 + (ctx js-context) 52 + (code :string) 53 + (code-len :int) 54 + (filename :string) 55 + (eval-flags :int)) 56 + 57 + (cffi:defcfun ("qjs_eval_module" qjs-eval-module) :int 58 + (ctx js-context) 59 + (code :string) 60 + (code-len :int) 61 + (filename :string)) 62 + 63 + ;; Get global object property as string (returns NULL-terminated string, caller must free) 64 + (cffi:defcfun ("qjs_get_global_string" qjs-get-global-string) :string 65 + (ctx js-context) 66 + (name :string)) 67 + 68 + ;; Set global property to integer 69 + (cffi:defcfun ("qjs_set_global_int" qjs-set-global-int) :void 70 + (ctx js-context) 71 + (name :string) 72 + (value :int)) 73 + 74 + ;; Set global property to float 75 + (cffi:defcfun ("qjs_set_global_float" qjs-set-global-float) :void 76 + (ctx js-context) 77 + (name :string) 78 + (value :double)) 79 + 80 + ;; Set global property to string 81 + (cffi:defcfun ("qjs_set_global_string" qjs-set-global-string) :void 82 + (ctx js-context) 83 + (name :string) 84 + (value :string)) 85 + 86 + ;; Register a C callback as a global JS function 87 + ;; Signature: int (*)(JSContext*, int argc, void* argv_opaque) 88 + (cffi:defcfun ("qjs_register_func" qjs-register-func) :void 89 + (ctx js-context) 90 + (name :string) 91 + (func :pointer) 92 + (argc :int)) 93 + 94 + ;; Call a global JS function by name with no args, return 0 on success 95 + (cffi:defcfun ("qjs_call_global" qjs-call-global) :int 96 + (ctx js-context) 97 + (name :string)) 98 + 99 + ;; Call a global function with an object argument (the API object) 100 + (cffi:defcfun ("qjs_call_with_api" qjs-call-with-api) :int 101 + (ctx js-context) 102 + (func-name :string)) 103 + 104 + ;; Get integer argument from callback 105 + (cffi:defcfun ("qjs_arg_int" qjs-arg-int) :int 106 + (ctx js-context) 107 + (argv :pointer) 108 + (index :int)) 109 + 110 + ;; Get float argument from callback 111 + (cffi:defcfun ("qjs_arg_float" qjs-arg-float) :double 112 + (ctx js-context) 113 + (argv :pointer) 114 + (index :int)) 115 + 116 + ;; Get string argument from callback (caller must free) 117 + (cffi:defcfun ("qjs_arg_string" qjs-arg-string) :string 118 + (ctx js-context) 119 + (argv :pointer) 120 + (index :int)) 121 + 122 + ;; Check for pending exception, print it, return 1 if exception occurred 123 + (cffi:defcfun ("qjs_check_exception" qjs-check-exception) :int 124 + (ctx js-context)) 125 + 126 + ;; Execute pending jobs (promises, async) 127 + (cffi:defcfun ("qjs_execute_pending" qjs-execute-pending) :int 128 + (ctx js-context)) 129 + 130 + ;; Check if a global function exists 131 + (cffi:defcfun ("qjs_has_global_func" qjs-has-global-func) :int 132 + (ctx js-context) 133 + (name :string))
+180
fedac/native/cl/quickjs-shim.c
··· 1 + /* quickjs-shim.c — Thin C shim for calling QuickJS from Common Lisp via CFFI. 2 + * 3 + * QuickJS uses NaN-boxed JSValue (64-bit union) which is awkward to pass 4 + * through CFFI. This shim provides pointer-safe wrappers that CL can call. 5 + * 6 + * Compile: gcc -shared -fPIC -o libquickjs-shim.so quickjs-shim.c \ 7 + * -I/path/to/quickjs -L/path/to -lquickjs -lm 8 + */ 9 + 10 + #include "quickjs.h" 11 + #include <stdio.h> 12 + #include <stdlib.h> 13 + #include <string.h> 14 + 15 + /* ── Eval helpers ── */ 16 + 17 + int qjs_eval(JSContext *ctx, const char *code, int code_len, const char *filename, int flags) { 18 + JSValue val = JS_Eval(ctx, code, code_len, filename, flags); 19 + if (JS_IsException(val)) { 20 + JSValue exc = JS_GetException(ctx); 21 + const char *str = JS_ToCString(ctx, exc); 22 + if (str) { fprintf(stderr, "[qjs] eval error: %s\n", str); JS_FreeCString(ctx, str); } 23 + /* Print stack trace if available */ 24 + if (JS_IsObject(exc)) { 25 + JSValue stack = JS_GetPropertyStr(ctx, exc, "stack"); 26 + const char *ss = JS_ToCString(ctx, stack); 27 + if (ss) { fprintf(stderr, "%s\n", ss); JS_FreeCString(ctx, ss); } 28 + JS_FreeValue(ctx, stack); 29 + } 30 + JS_FreeValue(ctx, exc); 31 + JS_FreeValue(ctx, val); 32 + return -1; 33 + } 34 + JS_FreeValue(ctx, val); 35 + return 0; 36 + } 37 + 38 + int qjs_eval_module(JSContext *ctx, const char *code, int code_len, const char *filename) { 39 + return qjs_eval(ctx, code, code_len, filename, JS_EVAL_TYPE_MODULE); 40 + } 41 + 42 + /* ── Global property access ── */ 43 + 44 + const char *qjs_get_global_string(JSContext *ctx, const char *name) { 45 + JSValue global = JS_GetGlobalObject(ctx); 46 + JSValue val = JS_GetPropertyStr(ctx, global, name); 47 + JS_FreeValue(ctx, global); 48 + const char *str = JS_ToCString(ctx, val); 49 + JS_FreeValue(ctx, val); 50 + return str; /* caller must JS_FreeCString */ 51 + } 52 + 53 + void qjs_set_global_int(JSContext *ctx, const char *name, int value) { 54 + JSValue global = JS_GetGlobalObject(ctx); 55 + JS_SetPropertyStr(ctx, global, name, JS_NewInt32(ctx, value)); 56 + JS_FreeValue(ctx, global); 57 + } 58 + 59 + void qjs_set_global_float(JSContext *ctx, const char *name, double value) { 60 + JSValue global = JS_GetGlobalObject(ctx); 61 + JS_SetPropertyStr(ctx, global, name, JS_NewFloat64(ctx, value)); 62 + JS_FreeValue(ctx, global); 63 + } 64 + 65 + void qjs_set_global_string(JSContext *ctx, const char *name, const char *value) { 66 + JSValue global = JS_GetGlobalObject(ctx); 67 + JS_SetPropertyStr(ctx, global, name, JS_NewString(ctx, value)); 68 + JS_FreeValue(ctx, global); 69 + } 70 + 71 + /* ── Callback registration ── 72 + * CL registers a C function pointer. The shim wraps it into a JSCFunction 73 + * that extracts args and calls through. 74 + * 75 + * For simplicity, we use a table of up to 256 registered callbacks. 76 + */ 77 + 78 + typedef JSValue (*NativeFn)(JSContext *ctx, JSValueConst this_val, 79 + int argc, JSValueConst *argv); 80 + 81 + /* Register a native C function as a global JS function */ 82 + void qjs_register_func(JSContext *ctx, const char *name, NativeFn func, int argc) { 83 + JSValue global = JS_GetGlobalObject(ctx); 84 + JS_SetPropertyStr(ctx, global, name, 85 + JS_NewCFunction(ctx, func, name, argc)); 86 + JS_FreeValue(ctx, global); 87 + } 88 + 89 + /* ── Argument extraction helpers ── */ 90 + 91 + int qjs_arg_int(JSContext *ctx, JSValueConst *argv, int index) { 92 + int32_t val = 0; 93 + JS_ToInt32(ctx, &val, argv[index]); 94 + return val; 95 + } 96 + 97 + double qjs_arg_float(JSContext *ctx, JSValueConst *argv, int index) { 98 + double val = 0; 99 + JS_ToFloat64(ctx, &val, argv[index]); 100 + return val; 101 + } 102 + 103 + const char *qjs_arg_string(JSContext *ctx, JSValueConst *argv, int index) { 104 + return JS_ToCString(ctx, argv[index]); /* caller must JS_FreeCString */ 105 + } 106 + 107 + /* ── Call global function ── */ 108 + 109 + int qjs_call_global(JSContext *ctx, const char *name) { 110 + JSValue global = JS_GetGlobalObject(ctx); 111 + JSValue func = JS_GetPropertyStr(ctx, global, name); 112 + if (!JS_IsFunction(ctx, func)) { 113 + JS_FreeValue(ctx, func); 114 + JS_FreeValue(ctx, global); 115 + return -1; 116 + } 117 + JSValue ret = JS_Call(ctx, func, global, 0, NULL); 118 + int err = 0; 119 + if (JS_IsException(ret)) { 120 + JSValue exc = JS_GetException(ctx); 121 + const char *str = JS_ToCString(ctx, exc); 122 + if (str) { fprintf(stderr, "[qjs] %s() error: %s\n", name, str); JS_FreeCString(ctx, str); } 123 + JS_FreeValue(ctx, exc); 124 + err = -1; 125 + } 126 + JS_FreeValue(ctx, ret); 127 + JS_FreeValue(ctx, func); 128 + JS_FreeValue(ctx, global); 129 + return err; 130 + } 131 + 132 + /* Call a function with the API object as argument. 133 + * The API object is built by building properties on a fresh object, 134 + * then passing it as the first arg. This mirrors how the C version works. 135 + * 136 + * For the CL bridge, we build the API object in JS (via eval) and 137 + * call the lifecycle function with it. This function calls a wrapper 138 + * that's been eval'd: __cl_call_lifecycle(funcName) 139 + */ 140 + int qjs_call_with_api(JSContext *ctx, const char *func_name) { 141 + char code[256]; 142 + snprintf(code, sizeof(code), 143 + "if (typeof %s === 'function') { %s(__ac_api); }", func_name, func_name); 144 + return qjs_eval(ctx, code, strlen(code), "<lifecycle>", JS_EVAL_TYPE_GLOBAL); 145 + } 146 + 147 + /* ── Exception handling ── */ 148 + 149 + int qjs_check_exception(JSContext *ctx) { 150 + JSValue exc = JS_GetException(ctx); 151 + if (JS_IsNull(exc) || JS_IsUndefined(exc)) { 152 + JS_FreeValue(ctx, exc); 153 + return 0; 154 + } 155 + const char *str = JS_ToCString(ctx, exc); 156 + if (str) { fprintf(stderr, "[qjs] exception: %s\n", str); JS_FreeCString(ctx, str); } 157 + JS_FreeValue(ctx, exc); 158 + return 1; 159 + } 160 + 161 + /* ── Pending jobs (promises) ── */ 162 + 163 + int qjs_execute_pending(JSContext *ctx) { 164 + JSContext *ctx2; 165 + int n = 0; 166 + while (JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx2) > 0) 167 + n++; 168 + return n; 169 + } 170 + 171 + /* ── Function existence check ── */ 172 + 173 + int qjs_has_global_func(JSContext *ctx, const char *name) { 174 + JSValue global = JS_GetGlobalObject(ctx); 175 + JSValue func = JS_GetPropertyStr(ctx, global, name); 176 + int is_func = JS_IsFunction(ctx, func); 177 + JS_FreeValue(ctx, func); 178 + JS_FreeValue(ctx, global); 179 + return is_func; 180 + }
+20 -2
fedac/native/docker-build.sh
··· 325 325 # ── Optional: Swap in Common Lisp binary ── 326 326 if [ "${AC_BUILD_LISP:-0}" = "1" ]; then 327 327 log "Step 2b: Building ac-native (Common Lisp)..." 328 + 329 + # Build libquickjs.so for CL to load via CFFI 330 + QJSDIR="/cache/quickjs-2024-01-13" 331 + log " Building libquickjs.so..." 332 + gcc -shared -fPIC -O2 -o "$BUILD/libquickjs.so" \ 333 + "$QJSDIR/quickjs.c" "$QJSDIR/libunicode.c" \ 334 + "$QJSDIR/libregexp.c" "$QJSDIR/cutils.c" "$QJSDIR/libbf.c" \ 335 + -DCONFIG_VERSION=\"0.8.0\" -lm 2>&1 || { err "libquickjs.so build failed"; } 336 + 337 + # Build quickjs-shim.so (CL-friendly wrappers) 328 338 CL_DIR="$NATIVE/cl" 339 + log " Building libquickjs-shim.so..." 340 + gcc -shared -fPIC -O2 -o "$BUILD/libquickjs-shim.so" \ 341 + "$CL_DIR/quickjs-shim.c" \ 342 + -I"$QJSDIR" -L"$BUILD" -lquickjs -lm 2>&1 || { err "shim build failed"; } 343 + 344 + # Build CL binary with SBCL 329 345 sbcl --non-interactive \ 330 346 --eval '(load "/opt/quicklisp/setup.lisp")' \ 331 347 --eval '(require :asdf)' \ ··· 338 354 # Swap into initramfs (keep C version in build dir) 339 355 cp "$IROOT/ac-native" "$BUILD/ac-native-c" 340 356 cp "$BUILD/ac-native-cl" "$IROOT/ac-native" 341 - # Add libzstd (CL runtime needs it, C binary doesn't) 357 + # Bundle shared libraries needed by CL 358 + cp "$BUILD/libquickjs.so" "$IROOT/lib64/" 359 + cp "$BUILD/libquickjs-shim.so" "$IROOT/lib64/" 342 360 for lib in /lib64/libzstd.so*; do 343 361 [ -f "$lib" ] && cp -L "$lib" "$IROOT/lib64/" 2>/dev/null 344 362 done 345 - log " Swapped CL binary into initramfs" 363 + log " Swapped CL binary into initramfs (with QuickJS)" 346 364 else 347 365 err "CL binary not produced — using C binary" 348 366 fi