Resolve security audit findings
ober
ba9a96fd29e7ba944f02010da2ad1fe022a1e4ef
--- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install packages run: | sudo apt-get update --- a/.github/workflows/security-baseline.yml +++ b/.github/workflows/security-baseline.yml @@ -13,7 +13,7 @@ jobs: baseline: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Required release files run: | --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.so *.dylib +*.o *.wpo .jerbuild-hashes .jerboa/* --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ JERBOA_VERSION ?= v0.2.3 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin JERBUILD ?= $(shell if [ -x "$(CURDIR)/../jerboa/dist/jerbuild" ]; then printf '%s\n' "$(CURDIR)/../jerboa/dist/jerbuild"; elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; else printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; fi) +JERBUILD_ABS = $(abspath $(JERBUILD)) JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null) LIBDIRS = lib:$(JH)/lib @@ -10,13 +11,14 @@ RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence QT_VENDOR_DIR ?= $(CURDIR)/../qt-vendor QT_FALLBACK_DIR ?= $(CURDIR)/../jerboa-emacs -QT_SHIM_INCLUDE_DIR ?= $(firstword $(wildcard $(QT_VENDOR_DIR)/vendor) $(wildcard $(QT_FALLBACK_DIR)/support/vendor-overrides) $(wildcard $(QT_FALLBACK_DIR)/vendor)) -QT_SHIM_LIB_DIR ?= $(firstword $(wildcard $(QT_VENDOR_DIR)/vendor) $(wildcard $(QT_FALLBACK_DIR))) +QT_SHIM_INCLUDE_DIR ?= $(realpath $(firstword $(wildcard $(QT_VENDOR_DIR)/vendor) $(wildcard $(QT_FALLBACK_DIR)/support/vendor-overrides) $(wildcard $(QT_FALLBACK_DIR)/vendor))) +QT_SHIM_LIB_DIR ?= $(realpath $(firstword $(wildcard $(QT_VENDOR_DIR)/vendor) $(wildcard $(QT_FALLBACK_DIR)))) QT_CFLAGS := $(shell pkg-config --cflags Qt6Widgets 2>/dev/null) QT_LIBS := $(shell pkg-config --libs Qt6Widgets 2>/dev/null || echo "-lQt6Widgets") CC ?= cc +CXX ?= c++ WARN_CFLAGS ?= -Wall -Wextra -Wformat=2 -Wshadow -Wpointer-arith -Wcast-align -Wwrite-strings HARDEN_CFLAGS ?= -O2 -fPIC -fstack-protector-strong -D_FORTIFY_SOURCE=2 CFLAGS ?= $(HARDEN_CFLAGS) $(WARN_CFLAGS) @@ -25,20 +27,28 @@ UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) SHARED_FLAG = -dynamiclib SHLIB_EXT = .dylib - LD_VAR = DYLD_LIBRARY_PATH - HARDEN_LDFLAGS ?= -Wl,-no_uuid + # Modern dyld rejects dlopen() of a dylib without LC_UUID. Deterministic + # evidence hashes the rebuilt artifact instead of stripping this required + # load command. + HARDEN_LDFLAGS ?= -Wl,-undefined,dynamic_lookup + ASAN_RUN_OPTIONS ?= halt_on_error=1 else SHARED_FLAG = -shared SHLIB_EXT = .so - LD_VAR = LD_LIBRARY_PATH HARDEN_LDFLAGS ?= -Wl,-z,relro,-z,now + ASAN_RUN_OPTIONS ?= detect_leaks=1:halt_on_error=1 endif SHIM := jerboa_qt_shim$(SHLIB_EXT) +SHIM_OBJS := jerboa_qt_shim.o callback_queue.o jerboa_qt_lifecycle.o +CALLBACK_QUEUE_TEST := .jerboa/callback-queue-test +CALLBACK_QUEUE_ASAN_TEST := .jerboa/callback-queue-asan-test +CALLBACK_QUEUE_TSAN_TEST := .jerboa/callback-queue-tsan-test .PHONY: all build native transpile security test test-pure test-native example-smoke clean shim vendor-check \ ensure-jerboa-tools audit qt-shim-provenance-check qt-shim-provenance-enforce \ - qt-advisory-check qt-advisory-enforce native-boundary-corpus sbom reproducibility-report target-evidence verify release-evidence + qt-advisory-check qt-advisory-enforce native-boundary-corpus sbom reproducibility-report target-evidence verify release-evidence \ + test-callback-queue test-callback-queue-asan test-callback-queue-tsan test-loader-policy all: build @@ -64,8 +74,17 @@ native: shim security: @sh scripts/security-check.sh -$(SHIM): jerboa_qt_shim.c Makefile - $(CC) $(CFLAGS) $(SHARED_FLAG) -I$(QT_SHIM_INCLUDE_DIR) -o $@ $< \ +jerboa_qt_shim.o: jerboa_qt_shim.c callback_queue.h Makefile + $(CC) $(CFLAGS) -pthread -I$(QT_SHIM_INCLUDE_DIR) -I. -c -o $@ $< + +callback_queue.o: callback_queue.c callback_queue.h Makefile + $(CC) $(CFLAGS) -pthread -I. -c -o $@ $< + +jerboa_qt_lifecycle.o: jerboa_qt_lifecycle.cpp callback_queue.h Makefile + $(CXX) -std=c++17 $(CFLAGS) $(QT_CFLAGS) -pthread -I. -c -o $@ $< + +$(SHIM): $(SHIM_OBJS) Makefile + $(CXX) $(CFLAGS) $(SHARED_FLAG) -o $@ $(SHIM_OBJS) \ -L$(QT_SHIM_LIB_DIR) -lqt_shim -Wl,-rpath,$(QT_SHIM_LIB_DIR) $(QT_LIBS) $(HARDEN_LDFLAGS) transpile: ensure-jerboa-tools @@ -78,24 +97,53 @@ test: test-pure test-pure: transpile $(JERBUILD) exec --libdirs "$(LIBDIRS)" tests/pure.ss +test-loader-policy: transpile shim + @REPO_ROOT="$(CURDIR)" \ + JERBUILD="$(JERBUILD_ABS)" \ + LIBDIRS="$(CURDIR)/lib:$(JH)/lib" \ + QT_SHIM_LIB_DIR="$(QT_SHIM_LIB_DIR)" \ + SHLIB_EXT="$(SHLIB_EXT)" \ + sh scripts/loader-policy-test.sh + +test-callback-queue: $(CALLBACK_QUEUE_TEST) + $(CALLBACK_QUEUE_TEST) + +$(CALLBACK_QUEUE_TEST): callback_queue.c callback_queue.h tests/callback_queue_test.c + @mkdir -p .jerboa + $(CC) -std=c11 -O2 $(WARN_CFLAGS) -pthread -I. \ + callback_queue.c tests/callback_queue_test.c -o $@ + +test-callback-queue-asan: callback_queue.c callback_queue.h tests/callback_queue_test.c + @mkdir -p .jerboa + $(CC) -std=c11 -O1 $(WARN_CFLAGS) -pthread -I. \ + -fsanitize=address,undefined -fno-omit-frame-pointer \ + callback_queue.c tests/callback_queue_test.c -o $(CALLBACK_QUEUE_ASAN_TEST) + ASAN_OPTIONS=$(ASAN_RUN_OPTIONS) UBSAN_OPTIONS=halt_on_error=1 \ + $(CALLBACK_QUEUE_ASAN_TEST) + +test-callback-queue-tsan: callback_queue.c callback_queue.h tests/callback_queue_test.c + @mkdir -p .jerboa + $(CC) -std=c11 -O1 $(WARN_CFLAGS) -pthread -I. \ + -fsanitize=thread -fno-omit-frame-pointer \ + callback_queue.c tests/callback_queue_test.c -o $(CALLBACK_QUEUE_TSAN_TEST) + TSAN_OPTIONS=halt_on_error=1 $(CALLBACK_QUEUE_TSAN_TEST) + test-native: transpile shim - JERBOA_QT_LIB=$(CURDIR) \ + JERBOA_QT_DEV_NATIVE=1 JERBOA_QT_LIB=$(CURDIR) \ JERBOA_QT_SHIM_DIR=$(QT_SHIM_LIB_DIR) \ - $(LD_VAR)=$(CURDIR):$(QT_SHIM_LIB_DIR) \ QT_QPA_PLATFORM=offscreen \ $(JERBUILD) exec --libdirs "$(LIBDIRS)" qt-test.ss native-boundary-corpus: transpile shim support/native-boundary-corpus.ss - JERBOA_QT_LIB=$(CURDIR) \ + JERBOA_QT_DEV_NATIVE=1 JERBOA_QT_LIB=$(CURDIR) \ JERBOA_QT_SHIM_DIR=$(QT_SHIM_LIB_DIR) \ - $(LD_VAR)=$(CURDIR):$(QT_SHIM_LIB_DIR) \ QT_QPA_PLATFORM=offscreen \ QT_LOGGING_RULES=qt.qpa.fonts=false \ $(JERBUILD) exec --libdirs "$(LIBDIRS)" support/native-boundary-corpus.ss example-smoke: transpile shim @rm -f /tmp/jerboa-qt-example-hello.out /tmp/jerboa-qt-example-hello.err - @QT_QPA_PLATFORM=offscreen JERBOA_QT_SHIM_DIR="$(QT_SHIM_LIB_DIR)" \ + @QT_QPA_PLATFORM=offscreen JERBOA_QT_DEV_NATIVE=1 JERBOA_QT_SHIM_DIR="$(QT_SHIM_LIB_DIR)" \ examples/hello.ss >/tmp/jerboa-qt-example-hello.out 2>/tmp/jerboa-qt-example-hello.err & \ pid=$$!; \ sleep 3; \ @@ -218,5 +266,6 @@ release-evidence: security clean: rm -f jerboa_qt_shim.so jerboa_qt_shim.dylib qt_chez_shim.so qt_chez_shim.dylib + rm -f $(SHIM_OBJS) $(CALLBACK_QUEUE_TEST) $(CALLBACK_QUEUE_ASAN_TEST) $(CALLBACK_QUEUE_TSAN_TEST) rm -rf lib dist .jerboa/bin -rmdir .jerboa 2>/dev/null || true --- a/README.md +++ b/README.md @@ -83,7 +83,9 @@ Native tests run headless using Qt's offscreen platform plugin (`QT_QPA_PLATFORM `docs/ffi-boundary.md`, `docs/shim-provenance.md`, `docs/qt-advisory-review.md`, and `docs/threat-model.md` are complete. -The Scheme modules are designed to import without loading native Qt libraries. Native libraries are resolved lazily on first FFI use, Scheme strings with embedded NUL bytes are rejected before native calls, and callback trampolines are registered when `qt-app-create` is called. +The Scheme modules are designed to import without loading native Qt libraries. Native libraries are resolved lazily on first FFI use, but dynamic loading requires canonical absolute `JERBOA_QT_SHIM_DIR` and `JERBOA_QT_LIB` directories whose final library files are regular non-symlinks. Static builds are detected from pre-registered representative symbols. Scheme strings with embedded NUL bytes are rejected before native calls. + +Qt's native thread never enters Chez. Signal values are copied into a bounded native queue and `qt-app-exec!` or `qt-pump-events!` drains that queue on the thread that created the application. Callback registration, removal, and execution from any other Jerboa thread are rejected. `make release-evidence` records pure/native tests, native-boundary corpus evidence, native linkage, Qt advisory status, external shim provenance, @@ -151,9 +153,9 @@ Or invoke manually without the shebang: ```sh JH="$(jerbuild --jerboa-home)" +JERBOA_QT_DEV_NATIVE=1 \ JERBOA_QT_LIB="$PWD" \ JERBOA_QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" \ -DYLD_LIBRARY_PATH="$PWD:${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}:${DYLD_LIBRARY_PATH:-}" \ QT_QPA_PLATFORM=offscreen \ jerbuild exec --libdirs "lib:$JH/lib" examples/hello.ss ``` @@ -1594,28 +1596,30 @@ RAII-style macros that guarantee cleanup even on exceptions. ## Architecture -The binding uses a three-layer architecture: +The binding uses a four-layer architecture: 1. **C++ shim** (`vendor/qt_shim.h`, `vendor/qt_shim.cpp` in qt-vendor) -- thin `extern "C"` wrappers around Qt6 C++ classes. All Qt objects are opaque `void*` handles. Compiled as `libqt_shim.so`. -2. **Chez callback bridge** (`jerboa_qt_shim.c`) -- stores Chez-provided `foreign-callable` function pointers and provides C wrapper functions that pass them to the C++ shim's signal connection APIs. Unlike Gambit's compile-time `c-define` trampolines, Chez creates callback pointers at runtime. +2. **Native callback bridge** (`jerboa_qt_shim.c`, `callback_queue.c`, and `jerboa_qt_lifecycle.cpp`) -- Qt callbacks copy bounded scalar/string events into a pthread-safe queue. It never stores or calls a Scheme function pointer. QObject destruction queues priority cleanup events, including parent-driven child teardown. 3. **FFI layer** (`jerboa-qt/ffi.ss`) -- Jerboa FFI bindings for each C function. Imports are pure; `libqt_shim.so` and `jerboa_qt_shim.so` are loaded lazily on first native use, and C entries are resolved lazily with `c-lambda`. -4. **High-level API** (`jerboa-qt/qt.ss`) -- Idiomatic Jerboa wrappers with boolean conversions, the `with-qt-app` macro, and all the `qt-on-*!` signal registration functions. Callback trampolines are registered when `qt-app-create` is called. +4. **High-level API** (`jerboa-qt/qt.ss`) -- Idiomatic Jerboa wrappers with boolean conversions, the `with-qt-app` macro, and all the `qt-on-*!` signal registration functions. It owns the generation-tagged callback registry on the `qt-app-create` thread. ### Callback Pattern -Qt signals are connected to Scheme handlers through a callback trampoline system: +Qt signals are transferred to Scheme handlers through an owner-thread queue: ``` -Qt signal -> C++ lambda -> chez_qt_*() wrapper -> foreign-callable trampoline - -> callback table lookup -> user's Scheme closure +Qt signal -> C++ lambda -> bounded native queue -> owner-thread drain + -> synchronized callback table lookup -> user's Scheme closure ``` Four callback types cover all signals: `void`, `string(text)`, `int(value)`, `bool(checked)`. -All `qt-on-*!` signal registration functions return a callback ID (integer). Use `unregister-qt-handler!` to remove a specific callback, or `qt-disconnect-all!` to disconnect all signals from a QObject. +All `qt-on-*!` signal registration functions return a generation-tagged callback ID (integer). Use `unregister-qt-handler!` to remove a specific callback, or `qt-disconnect-all!` to disconnect all signals from a QObject. Both must run on the application owner thread. `qt-timer-single-shot!` removes its closure before invocation. Explicit destructors and native `QObject::destroyed` events remove registrations exactly once; queued events for removed IDs are ignored. + +`qt-app-exec!` drains automatically. Code that pumps Qt manually must call `(qt-pump-events! app)`, which processes Qt events and drains at most one bounded queue batch. `qt-callback-registry-size`, `qt-callback-pending-count`, and `qt-callback-dropped-count` are available for lifecycle/overload monitoring. ### Memory Management --- a/SECURITY.md +++ b/SECURITY.md @@ -6,10 +6,11 @@ and production-readiness tracker gates are complete for this repository. ## Current Posture - Importing `(jerboa-qt ffi)` and `(jerboa-qt qt)` must not load native Qt libraries. -- Native libraries and C entries are resolved lazily through `(jerboa ffi)`. +- Native libraries and C entries are resolved lazily through `(jerboa ffi)`. Dynamic loading accepts only configured absolute directories and regular non-symlink library files; no CWD or bare-name search is used. - Scheme string arguments are rejected if they contain embedded NUL bytes. -- Callback trampolines are registered lazily when `qt-app-create` is called. -- Public high-level APIs do not expose direct event-queue pumping; prefer `qt-app-exec!` with an optional tick callback. +- Native Qt callbacks never enter Chez. They copy into a bounded pthread-safe queue that is drained only by the Scheme thread that called `qt-app-create`. +- Callback registry mutation is synchronized and owner-thread restricted. Generation-tagged IDs, QObject destruction watches, explicit subtree invalidation, and one-shot removal prevent stale closures from outliving native objects. +- Prefer `qt-app-exec!`; manual loops must use `qt-pump-events!` so both Qt and callback queues are drained. - `make qt-shim-provenance-check` records the external `libqt_shim` source, provider Git commit, local callback shim, and native binary hashes. - `make qt-advisory-check` records whether the local Qt line satisfies the @@ -36,7 +37,7 @@ Applications built on this library can handle GUI text, editor buffers, clipboar `make qt-shim-provenance-enforce`. - Enforce the reviewed Qt advisory floor with `make qt-advisory-enforce`. - Run hosted native tests on Linux and macOS with Qt offscreen. -- Run sanitizer builds for `jerboa_qt_shim.c` and the external Qt shim. +- Retain passing ASan/UBSan queue evidence and run whole-shim/external Qt ThreadSanitizer builds on a supported hosted target. - Add regression coverage for null returned strings, NUL inputs, QObject ownership, callback disconnects, QProcess, file dialogs, clipboard, and event-loop reentrancy. - Attach marker-complete target proof through `JQT_TARGET_PROOF_FILE` covering hosted Linux/macOS native CI, Qt advisory status, external shim provenance, new file mode 100644 --- /dev/null +++ b/callback_queue.c @@ -0,0 +1,188 @@ +#include "callback_queue.h" + +#include <pthread.h> +#include <stdlib.h> +#include <string.h> + +/* Callback traffic is deliberately bounded. A stalled Scheme owner must not + * let an untrusted/high-rate GUI event source grow process memory without + * limit. */ +#define JERBOA_QT_CALLBACK_QUEUE_CAPACITY 8192U +#define JERBOA_QT_CALLBACK_STRING_MAX (1024U * 1024U) + +struct callback_event { + int type; + long callback_id; + int value; + char *string_value; +}; + +static pthread_mutex_t callback_mutex = PTHREAD_MUTEX_INITIALIZER; +static struct callback_event callback_queue[JERBOA_QT_CALLBACK_QUEUE_CAPACITY]; +static size_t callback_head; +static size_t callback_count; +static unsigned long callback_dropped; + +/* Only the registered Scheme owner thread accesses dispatch_event. */ +static struct callback_event dispatch_event; + +static size_t bounded_string_length(const char *value) { + size_t length = 0; + + if (value == NULL) { + return 0; + } + while (length <= JERBOA_QT_CALLBACK_STRING_MAX && value[length] != '\0') { + ++length; + } + return length; +} + +static void enqueue_event(int type, long callback_id, int value, + const char *string_value) { + char *copy = NULL; + + if (type == JERBOA_QT_CALLBACK_STRING) { + const size_t length = bounded_string_length(string_value); + if (string_value == NULL || length > JERBOA_QT_CALLBACK_STRING_MAX) { + pthread_mutex_lock(&callback_mutex); + ++callback_dropped; + pthread_mutex_unlock(&callback_mutex); + return; + } + copy = (char *)malloc(length + 1U); + if (copy == NULL) { + pthread_mutex_lock(&callback_mutex); + ++callback_dropped; + pthread_mutex_unlock(&callback_mutex); + return; + } + memcpy(copy, string_value, length + 1U); + } + + pthread_mutex_lock(&callback_mutex); + if (callback_count == JERBOA_QT_CALLBACK_QUEUE_CAPACITY) { + ++callback_dropped; + pthread_mutex_unlock(&callback_mutex); + free(copy); + return; + } + + { + const size_t tail = + (callback_head + callback_count) % JERBOA_QT_CALLBACK_QUEUE_CAPACITY; + callback_queue[tail].type = type; + callback_queue[tail].callback_id = callback_id; + callback_queue[tail].value = value; + callback_queue[tail].string_value = copy; + ++callback_count; + } + pthread_mutex_unlock(&callback_mutex); +} + +void jerboa_qt_enqueue_void_event(long callback_id) { + enqueue_event(JERBOA_QT_CALLBACK_VOID, callback_id, 0, NULL); +} + +void jerboa_qt_enqueue_string_event(long callback_id, const char *value) { + enqueue_event(JERBOA_QT_CALLBACK_STRING, callback_id, 0, value); +} + +void jerboa_qt_enqueue_int_event(long callback_id, int value) { + enqueue_event(JERBOA_QT_CALLBACK_INT, callback_id, value, NULL); +} + +void jerboa_qt_enqueue_bool_event(long callback_id, int value) { + enqueue_event(JERBOA_QT_CALLBACK_BOOL, callback_id, value, NULL); +} + +void jerboa_qt_enqueue_destroyed_event(long callback_id) { + pthread_mutex_lock(&callback_mutex); + if (callback_count == JERBOA_QT_CALLBACK_QUEUE_CAPACITY) { + const size_t tail = + (callback_head + callback_count - 1U) % + JERBOA_QT_CALLBACK_QUEUE_CAPACITY; + free(callback_queue[tail].string_value); + memset(&callback_queue[tail], 0, sizeof(callback_queue[tail])); + --callback_count; + ++callback_dropped; + } + + /* Destruction invalidates callbacks that may already be queued for the + * same ID, so cleanup events have priority over ordinary FIFO traffic. */ + callback_head = + (callback_head + JERBOA_QT_CALLBACK_QUEUE_CAPACITY - 1U) % + JERBOA_QT_CALLBACK_QUEUE_CAPACITY; + callback_queue[callback_head].type = JERBOA_QT_CALLBACK_DESTROYED; + callback_queue[callback_head].callback_id = callback_id; + callback_queue[callback_head].value = 0; + callback_queue[callback_head].string_value = NULL; + ++callback_count; + pthread_mutex_unlock(&callback_mutex); +} + +int jerboa_qt_next_callback_event(void) { + free(dispatch_event.string_value); + memset(&dispatch_event, 0, sizeof(dispatch_event)); + + pthread_mutex_lock(&callback_mutex); + if (callback_count == 0U) { + pthread_mutex_unlock(&callback_mutex); + return JERBOA_QT_CALLBACK_NONE; + } + + dispatch_event = callback_queue[callback_head]; + memset(&callback_queue[callback_head], 0, + sizeof(callback_queue[callback_head])); + callback_head = (callback_head + 1U) % JERBOA_QT_CALLBACK_QUEUE_CAPACITY; + --callback_count; + pthread_mutex_unlock(&callback_mutex); + return dispatch_event.type; +} + +long jerboa_qt_callback_event_id(void) { + return dispatch_event.callback_id; +} + +int jerboa_qt_callback_event_value(void) { + return dispatch_event.value; +} + +const char *jerboa_qt_callback_event_string(void) { + return dispatch_event.string_value == NULL ? "" : dispatch_event.string_value; +} + +unsigned long jerboa_qt_callback_pending_count(void) { + unsigned long result; + + pthread_mutex_lock(&callback_mutex); + result = (unsigned long)callback_count; + pthread_mutex_unlock(&callback_mutex); + return result; +} + +unsigned long jerboa_qt_callback_dropped_count(void) { + unsigned long result; + + pthread_mutex_lock(&callback_mutex); + result = callback_dropped; + pthread_mutex_unlock(&callback_mutex); + return result; +} + +void jerboa_qt_clear_callback_events(void) { + size_t i; + + pthread_mutex_lock(&callback_mutex); + for (i = 0; i < JERBOA_QT_CALLBACK_QUEUE_CAPACITY; ++i) { + free(callback_queue[i].string_value); + memset(&callback_queue[i], 0, sizeof(callback_queue[i])); + } + callback_head = 0U; + callback_count = 0U; + callback_dropped = 0UL; + pthread_mutex_unlock(&callback_mutex); + + free(dispatch_event.string_value); + memset(&dispatch_event, 0, sizeof(dispatch_event)); +} new file mode 100644 --- /dev/null +++ b/callback_queue.h @@ -0,0 +1,42 @@ +#ifndef JERBOA_QT_CALLBACK_QUEUE_H +#define JERBOA_QT_CALLBACK_QUEUE_H + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +enum jerboa_qt_callback_event_type { + JERBOA_QT_CALLBACK_NONE = 0, + JERBOA_QT_CALLBACK_VOID = 1, + JERBOA_QT_CALLBACK_STRING = 2, + JERBOA_QT_CALLBACK_INT = 3, + JERBOA_QT_CALLBACK_BOOL = 4, + JERBOA_QT_CALLBACK_DESTROYED = 5 +}; + +/* These functions are safe to call from the native Qt thread. They never + * enter Chez or retain a pointer supplied by Qt. */ +void jerboa_qt_enqueue_void_event(long callback_id); +void jerboa_qt_enqueue_string_event(long callback_id, const char *value); +void jerboa_qt_enqueue_int_event(long callback_id, int value); +void jerboa_qt_enqueue_bool_event(long callback_id, int value); +void jerboa_qt_enqueue_destroyed_event(long callback_id); + +/* Exactly one Scheme-owned thread may consume events. Producers may run on + * any pthread. Accessors describe the event returned by the most recent + * successful jerboa_qt_next_callback_event call. */ +int jerboa_qt_next_callback_event(void); +long jerboa_qt_callback_event_id(void); +int jerboa_qt_callback_event_value(void); +const char *jerboa_qt_callback_event_string(void); +unsigned long jerboa_qt_callback_pending_count(void); +unsigned long jerboa_qt_callback_dropped_count(void); +void jerboa_qt_clear_callback_events(void); + +#ifdef __cplusplus +} +#endif + +#endif --- a/docs/ffi-boundary.md +++ b/docs/ffi-boundary.md @@ -5,14 +5,16 @@ - `libqt_shim`: the external C++ Qt shim that owns the Qt wrappers. - `jerboa_qt_shim`: the local Chez callback bridge for Qt signal callbacks. -The Scheme modules can now be imported without loading native Qt libraries. Native libraries are loaded lazily on the first FFI call through `(jerboa ffi)` `load-shared-object*`, then individual C entries are resolved lazily with `c-lambda`. +The Scheme modules can be imported without loading native Qt libraries. Native libraries are loaded lazily on the first FFI call through `(jerboa ffi)` `load-shared-object*`, then individual C entries are resolved lazily with `c-lambda`. ## Current Controls - Importing `(jerboa-qt ffi)` or `(jerboa-qt qt)` is pure and must not require Qt, X11, Wayland, or a display server. - Scheme string arguments are rejected if they contain embedded NUL bytes before native resolution or native calls. -- Callback trampolines are registered lazily when `qt-app-create` is called. -- `JERBOA_QT_SHIM_DIR` selects the external shim directory. `JERBOA_QT_LIB` selects the local callback shim directory. +- Dynamic loading requires canonical absolute `JERBOA_QT_SHIM_DIR` and `JERBOA_QT_LIB` directories. The selected final library files and every path component must resolve without symlink or `..` substitution. There is no current-directory or bare-name fallback. Static builds are recognized only when representative external and local symbols are already registered. +- The Qt pthread never enters Chez. Native callbacks copy into an 8192-event queue with a 1 MiB per-string limit; overflow is counted and drops the newest ordinary event. Destruction cleanup has priority so stale queued callbacks cannot execute after logical teardown. +- `qt-app-create` establishes one Scheme owner thread. Registry mutation, event draining, and handler execution from other Jerboa threads fail closed. +- Callback IDs combine an application generation and sequence. Every registration is associated with each owning QObject, native destruction is watched, and explicit destructors invalidate QObject subtrees before disconnect/delete. - `make test` runs pure checks. `make test-native` is the gate for hosted native Qt behavior. - `make qt-shim-provenance-check` records external shim source, binary, and provider Git metadata. @@ -28,7 +30,7 @@ The Scheme modules can now be imported without loading native Qt libraries. Nati Qt owns parented child widgets. Do not manually destroy a child after its parent has been destroyed. Prefer one explicit destroy call at the root widget or use `with-qt-app` for application lifetime. -Callback IDs returned by `qt-on-*!` are Scheme-side references. Use `unregister-qt-handler!` or `qt-disconnect-all!` when disconnecting long-lived objects. +Callback IDs returned by `qt-on-*!` are Scheme-side references. Use `unregister-qt-handler!` or `qt-disconnect-all!` when disconnecting long-lived objects. Both are idempotent at the logical registry boundary, and native late events for absent IDs are discarded. One-shot timers unregister before invoking user code. ## Known Gaps @@ -37,8 +39,8 @@ Callback IDs returned by `qt-on-*!` are Scheme-side references. Use `unregister- - The Qt advisory floor must stay current. The local release evidence currently records Qt 6.11.1, which passes the CVE-2026-6210 floor for the 6.11 line. - QScintilla support is optional and must be tested against the exact native shim used by the release. -- Direct event-queue pumping remains restricted to the low-level FFI boundary. High-level callers should use `qt-app-exec!` with an optional tick callback instead. +- High-level manual pumping must use `qt-pump-events!`; direct low-level `ffi-qt-app-process-events` does not drain the callback queue. +- The queue is intentionally lossy at its fixed capacity. Applications should monitor `qt-callback-dropped-count` and treat growth as overload rather than assuming every high-rate UI event is delivered. - QProcess, clipboard, file dialog, and filesystem model APIs cross process, desktop, and filesystem boundaries and require application-level policy. -- Production memory/render-time quotas and sanitizer evidence are still - required beyond the bounded local native-boundary corpus. +- The local queue has deterministic concurrency, ASan/UBSan, and lifecycle coverage. Full Qt/Chez ThreadSanitizer proof still requires a supported Linux/macOS Qt runtime built with TSan; external GUI review remains a production gate. --- a/examples/autocomplete.ss +++ b/examples/autocomplete.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; autocomplete.ss — QSettings, QCompleter, QToolTip demo --- a/examples/counter.ss +++ b/examples/counter.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; Counter example: button click increments a label --- a/examples/dashboard.ss +++ b/examples/dashboard.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; dashboard.ss — Dashboard demo showcasing tabs, lists, tables, sliders --- a/examples/datainput.ss +++ b/examples/datainput.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; datainput.ss — Data input widgets demo --- a/examples/diagram.ss +++ b/examples/diagram.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# (import (jerboa-qt qt)) --- a/examples/dialogs.ss +++ b/examples/dialogs.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; dialogs.ss — Showcase of all dialog types. --- a/examples/dockable.ss +++ b/examples/dockable.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# (import (jerboa-qt qt)) --- a/examples/dragdrop.ss +++ b/examples/dragdrop.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# (import (jerboa-qt qt)) --- a/examples/editor.ss +++ b/examples/editor.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; editor.ss — Simple text editor with menus, toolbar, keyboard shortcuts --- a/examples/filebrowser.ss +++ b/examples/filebrowser.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; File Browser — demonstrates QTreeWidget, QGridLayout, QTimer, clipboard --- a/examples/filemanager.ss +++ b/examples/filemanager.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; filemanager.ss — File system browser using QFileSystemModel + QTreeView --- a/examples/form.ss +++ b/examples/form.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; Form example: exercises Phase 2 widgets --- a/examples/hello.ss +++ b/examples/hello.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; Minimal jerboa-qt example: window with a label --- a/examples/keyboard.ss +++ b/examples/keyboard.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; keyboard.ss — Key event viewer and shortcut demo. --- a/examples/mdi.ss +++ b/examples/mdi.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# (import (jerboa-qt qt)) --- a/examples/modelviewer.ss +++ b/examples/modelviewer.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; modelviewer.ss — Model/View demo --- a/examples/painter.ss +++ b/examples/painter.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; painter.ss — QPainter demo: draw shapes, text, and composited images --- a/examples/planner.ss +++ b/examples/planner.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; planner.ss — QFormLayout, QCalendarWidget, QTextBrowser, --- a/examples/polished.ss +++ b/examples/polished.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; polished.ss — Practical Polish demo --- a/examples/richtext.ss +++ b/examples/richtext.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; richtext.ss — HTML viewer with font/color customization --- a/examples/settings.ss +++ b/examples/settings.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; settings.ss — Radio buttons, button groups, group boxes --- a/examples/styled.ss +++ b/examples/styled.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; styled.ss — Dark-themed split-pane app with scroll area, key events --- a/examples/terminal.ss +++ b/examples/terminal.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; terminal.ss — Simple terminal demonstrating QProcess with QPlainTextEdit --- a/examples/trayapp.ss +++ b/examples/trayapp.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# (import (jerboa-qt qt)) --- a/examples/widgets.ss +++ b/examples/widgets.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# ;;; widgets.ss — Niche widgets demo: QDial, QLCDNumber, QToolBox, QUndoStack --- a/examples/wizard.ss +++ b/examples/wizard.ss @@ -9,8 +9,7 @@ fi QT_SHIM_DIR="${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}" export JERBOA_QT_LIB="$JERBOA_QT_DIR" export JERBOA_QT_SHIM_DIR="$QT_SHIM_DIR" -export DYLD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}" -export LD_LIBRARY_PATH="$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}" +export JERBOA_QT_DEV_NATIVE=1 exec jerbuild exec --libdirs "$JERBOA_QT_DIR/lib:$JH/lib" "$0" "$@" |# (import (jerboa-qt qt)) new file mode 100644 --- /dev/null +++ b/jerboa_qt_lifecycle.cpp @@ -0,0 +1,142 @@ +#include "callback_queue.h" + +#include <QMetaObject> +#include <QObject> +#include <QThread> + +#include <algorithm> +#include <mutex> +#include <unordered_map> +#include <vector> + +extern "C" void Sdeactivate_thread(void); +extern "C" int Sactivate_thread(void); + +static std::mutex watch_mutex; +struct watch_state { + std::vector<long> ids; + QMetaObject::Connection connection; +}; +static std::unordered_map<QObject *, watch_state> watched_objects; + +static std::vector<long> take_watched_ids(QObject *object) { + std::lock_guard<std::mutex> guard(watch_mutex); + auto found = watched_objects.find(object); + if (found == watched_objects.end()) { + return {}; + } + std::vector<long> result = std::move(found->second.ids); + watched_objects.erase(found); + return result; +} + +static void enqueue_destroyed_ids(QObject *object) { + for (long id : take_watched_ids(object)) { + jerboa_qt_enqueue_destroyed_event(id); + } +} + +/* QObject::connect is thread-safe. The destroyed handler runs on the + * object's Qt thread and only enqueues an integer; Scheme state is never + * touched from that native thread. */ +extern "C" void jerboa_qt_watch_destroyed(void *object, long callback_id) { + if (object == nullptr) { + return; + } + + auto *qobject = static_cast<QObject *>(object); + bool install_connection = false; + { + std::lock_guard<std::mutex> guard(watch_mutex); + auto &state = watched_objects[qobject]; + auto &ids = state.ids; + install_connection = ids.empty(); + if (std::find(ids.begin(), ids.end(), callback_id) == ids.end()) { + ids.push_back(callback_id); + } + } + if (install_connection) { + auto connection = QObject::connect( + qobject, &QObject::destroyed, + [qobject]() { enqueue_destroyed_ids(qobject); }); + std::lock_guard<std::mutex> guard(watch_mutex); + auto found = watched_objects.find(qobject); + if (found != watched_objects.end()) { + found->second.connection = connection; + } + } +} + +extern "C" void jerboa_qt_unwatch_destroyed(void *object, long callback_id) { + if (object == nullptr) { + return; + } + + QMetaObject::Connection connection; + bool disconnect = false; + { + std::lock_guard<std::mutex> guard(watch_mutex); + auto found = watched_objects.find(static_cast<QObject *>(object)); + if (found == watched_objects.end()) { + return; + } + auto &ids = found->second.ids; + ids.erase(std::remove(ids.begin(), ids.end(), callback_id), ids.end()); + if (ids.empty()) { + connection = found->second.connection; + watched_objects.erase(found); + disconnect = true; + } + } + if (disconnect) { + QObject::disconnect(connection); + } +} + +extern "C" void jerboa_qt_clear_destroyed_watches(void) { + std::vector<QMetaObject::Connection> connections; + { + std::lock_guard<std::mutex> guard(watch_mutex); + for (const auto &entry : watched_objects) { + connections.push_back(entry.second.connection); + } + watched_objects.clear(); + } + for (const auto &connection : connections) { + QObject::disconnect(connection); + } +} + +/* Explicit parent deletion must invalidate child registrations immediately, + * before qt_disconnect_all can remove destroyed-signal watchers. Traverse + * QObject ownership on the object's Qt thread, but only enqueue integer IDs; + * no Scheme object or callback crosses this boundary. */ +extern "C" void jerboa_qt_notify_destroying_tree(void *object) { + if (object == nullptr) { + return; + } + + auto *root = static_cast<QObject *>(object); + std::vector<QObject *> objects; + auto collect = [&objects, root]() { + objects.push_back(root); + const auto children = root->findChildren<QObject *>(); + objects.insert(objects.end(), children.begin(), children.end()); + }; + + if (root->thread() == QThread::currentThread()) { + collect(); + } else { + Sdeactivate_thread(); + const bool invoked = QMetaObject::invokeMethod( + root, collect, Qt::BlockingQueuedConnection); + (void)Sactivate_thread(); + if (!invoked) { + return; + } + } + + for (QObject *current : objects) { + enqueue_destroyed_ids(current); + } +} --- a/jerboa_qt_shim.c +++ b/jerboa_qt_shim.c @@ -1,31 +1,32 @@ -/* jerboa_qt_shim.c — Callback bridge for Chez Scheme FFI +/* jerboa_qt_shim.c — Native callback queue bridge for Chez Scheme FFI * - * Chez Scheme's foreign-callable creates function pointers at runtime, - * unlike Gambit's c-define which creates static trampolines at compile time. - * - * This shim stores the Chez-provided callback function pointers and - * provides C functions that qt_shim.cpp can call through them. + * This shim never calls Chez from Qt's native thread. Signal wrappers pass + * bounded native-queue producers to qt_shim; Jerboa drains those immutable + * events later on its registered owner thread. * * Pattern: - * 1. Chez creates foreign-callable → gets a C function pointer - * 2. Chez calls jerboa_qt_set_*_callback() to register that pointer - * 3. Signal connection wrappers use the stored pointer as the trampoline + * The jerboa_qt_set_* entry points remain as ABI-compatible no-ops for older + * callers. No caller-supplied function pointer is retained or invoked. */ +#include "callback_queue.h" #include "qt_shim.h" #include <stddef.h> -/* ---- Stored callback function pointers (set from Chez at init time) ---- */ -static qt_callback_void chez_void_callback = NULL; -static qt_callback_string chez_string_callback = NULL; -static qt_callback_int chez_int_callback = NULL; -static qt_callback_bool chez_bool_callback = NULL; +/* Loader ABI canary: "JQT1". */ +unsigned int jerboa_qt_abi_version(void) { return 0x4a515431u; } +