summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/guide.md27
-rw-r--r--include/yasl/app.h3
-rw-r--r--include/yasl/loop.h63
-rw-r--r--src/common/loop.h5
-rw-r--r--src/platform/cocoa/window.m102
-rw-r--r--src/platform/posix/loop.c219
-rw-r--r--tests/common/test_loop.c248
-rw-r--r--tests/meson.build8
8 files changed, 651 insertions, 24 deletions
diff --git a/docs/guide.md b/docs/guide.md
index ab30c3d..888e4ab 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -334,10 +334,29 @@ The loop never closes application file descriptors: delete the watch before
closing its descriptor. Ready callbacks may add or remove watches. Do not call
`yasl_loop_step()` or `yasl_loop_run()` recursively.
-Cocoa dispatch waits for native events and checks the loop nonblockingly
-afterward. An application FD alone does not currently wake an indefinite Cocoa
-dispatch; use a bounded timeout when integrating such sources. `loop_kick()`
-signals the loop pipe but is not a portable cross-thread event-posting API.
+Every backend's dispatch wakes when a loop descriptor becomes ready. Cocoa
+waits for native events and mirrors the loop's descriptors in a kqueue while it
+waits. If kqueue rejects a descriptor, Cocoa bounds the wait to 10 ms instead.
+
+Two calls work from any thread. `yasl_loop_kick()` only wakes the loop, and it
+is also safe in a signal handler. `yasl_loop_post()` queues a function to run
+on the owning thread during a later step or dispatch. Use it to hand results
+from worker threads to the loop:
+
+```c
+static void results_ready(void *ud)
+{
+ worker_drain(ud); /* Runs on the loop thread. */
+}
+/* On the worker thread; false means allocation failed. */
+yasl_loop_post(loop, results_ready, worker);
+```
+
+`yasl_loop_watch_signals()` delivers POSIX signals to a callback on the loop
+thread. It installs a process-wide handler for each signal, with `SA_RESTART`,
+and restores the previous action on unwatch. YASL leaves thread signal masks
+alone: a watched signal must stay unblocked in at least one thread. Only one
+loop can watch a given signal at a time.
## Native interoperability
diff --git a/include/yasl/app.h b/include/yasl/app.h
index bf6de9c..b7e1c13 100644
--- a/include/yasl/app.h
+++ b/include/yasl/app.h
@@ -98,7 +98,8 @@ YASL_API void yasl_surface_request_frame(struct yasl_surface *s);
* @param code Application-defined message code (yasl_user_event.code).
* @param payload Arbitrary pointer passed through to the event.
* @param size Optional payload size.
- * @return true if queued successfully, false if queue is full or memory failed.
+ * @return true if queued; false if memory failed or d is not run by yasl_run().
+ * Applications that run their own loop use yasl_loop_post() instead.
*/
YASL_API bool yasl_post_event(struct yasl_display *d, struct yasl_surface *s,
uint32_t code, void *payload, size_t size);
diff --git a/include/yasl/loop.h b/include/yasl/loop.h
index 9faa8f7..a8e9974 100644
--- a/include/yasl/loop.h
+++ b/include/yasl/loop.h
@@ -3,7 +3,8 @@
* Optional event loop, file watches, and timers.
*
* Use the loop's owning thread for all calls and callbacks, except
- * yasl_loop_now_ns(). A display borrows a supplied loop. For window applications,
+ * yasl_loop_now_ns(), yasl_loop_kick(), and yasl_loop_post(), which any thread
+ * may call. A display borrows a supplied loop. For window applications,
* drive yasl_display_dispatch(), drain input, then yasl_display_flush(); the
* standalone loop_run() function does not replace this cycle. Timer intervals are
* nanoseconds; timeout_ms arguments are milliseconds. Do not recursively step/run
@@ -29,10 +30,11 @@ extern "C" {
*/
YASL_API struct yasl_loop *yasl_loop_create(void);
/**
- * Release a loop, its watches, and its timers.
+ * Release a loop, its watches, its timers, and its signal watch.
* @param l Loop, or NULL for no action.
* Close displays borrowing this loop first. Must not run during a loop callback
- * or an active step/run. Watched file descriptors are not closed.
+ * or an active step/run. Watched file descriptors are not closed. Posted
+ * functions that have not run are dropped; no thread may still post or kick.
*/
YASL_API void yasl_loop_destroy(struct yasl_loop *l);
/**
@@ -59,13 +61,62 @@ YASL_API int yasl_loop_step(struct yasl_loop *l, int timeout_ms);
*/
YASL_API void yasl_loop_quit(struct yasl_loop *l);
/**
- * Signal the loop's wake pipe without dispatching callbacks.
+ * Wake a loop that is waiting, without running any callback.
* @param l Loop, or NULL for no action.
- * Multiple signals may coalesce. This does not schedule a redraw or enqueue an
- * event, and it does not provide a cross-thread application messaging contract.
+ * May be called from any thread and from a signal handler. The loop must stay
+ * alive until the call returns. A waiting yasl_loop_step(), yasl_loop_run(), or
+ * yasl_display_dispatch() on this loop returns early; a loop that is not
+ * waiting returns early from its next wait instead. Kicks coalesce. This does
+ * not schedule a redraw or enqueue an event; use yasl_loop_post() to run code
+ * on the owning thread.
*/
YASL_API void yasl_loop_kick(struct yasl_loop *l);
/**
+ * Run a function on the loop's owning thread.
+ * @param l Loop.
+ * @param fn Required function, called once with ud during a later step.
+ * @param ud Borrowed userdata until fn runs.
+ * @return true if queued; false for NULL arguments, a loop without a wake
+ * pipe, or allocation failure.
+ * May be called from any thread, but not from a signal handler. Wakes the loop
+ * like yasl_loop_kick(). Functions run in posting order, during a
+ * yasl_loop_step() or display dispatch on the owning thread. They may post
+ * again; a function posted while posts are running waits for a later step.
+ * Stop posting before yasl_loop_destroy(), which drops functions that have
+ * not run.
+ */
+YASL_API bool yasl_loop_post(struct yasl_loop *l, void (*fn)(void *ud), void *ud);
+/**
+ * Deliver POSIX signals to a callback on the loop's owning thread.
+ * @param l Loop.
+ * @param signals Signal numbers to watch; the array is read during the call.
+ * @param n Number of entries, at least one.
+ * @param cb Required callback, given ud and one signal number per delivery.
+ * @param ud Borrowed userdata until the watch is removed.
+ * @return true if every signal is watched; false with errno set and no
+ * dispositions changed. EINVAL means a NULL or invalid argument, including
+ * SIGKILL and SIGSTOP. EBUSY means another loop watches one of the signals.
+ * Replaces this loop's previous watch, if any. Installs one process-wide
+ * handler per signal with SA_RESTART and an empty handler mask, and saves the
+ * previous action. The handler only records the signal and wakes the loop,
+ * so it may run on any thread. YASL does not change any thread's signal mask:
+ * a signal blocked in every thread stays pending and is not delivered. Caught
+ * signals reset to their default action across exec, so spawned programs
+ * do not inherit the handler. Repeated deliveries of one signal before the
+ * callback runs coalesce into one callback. SIGCHLD reports stopped and
+ * continued children as well as exits. Callbacks run during yasl_loop_step()
+ * or display dispatch, and may remove the watch.
+ */
+YASL_API bool yasl_loop_watch_signals(struct yasl_loop *l, const int *signals, int n,
+ void (*cb)(void *ud, int signo), void *ud);
+/**
+ * Stop watching signals and restore their previous actions.
+ * @param l Loop, or NULL for no action.
+ * Does nothing if the loop watches no signals. Signals that arrived but have
+ * not reached the callback are dropped. yasl_loop_destroy() also unwatches.
+ */
+YASL_API void yasl_loop_unwatch_signals(struct yasl_loop *l);
+/**
* Read the monotonic clock used by YASL timers and timing results.
* @return Nanoseconds from CLOCK_MONOTONIC, unrelated to wall-clock time.
* May be called from any thread.
diff --git a/src/common/loop.h b/src/common/loop.h
index 663417a..60c5b64 100644
--- a/src/common/loop.h
+++ b/src/common/loop.h
@@ -3,7 +3,12 @@
#include "yasl/loop.h"
+struct pollfd;
+
int yasl_loop_next_timeout_ms(const struct yasl_loop *l);
+/* The loop's current poll set, valid until the next watch change. Backends
+ * that wait on native events use it to wake on loop readiness. */
+int yasl_loop_poll_set(const struct yasl_loop *l, const struct pollfd **fds);
/* Whether id is armed: added, not deleted, and not retired by its callback. */
bool yasl_loop_timer_armed(const struct yasl_loop *l, yasl_timer_id id);
diff --git a/src/platform/cocoa/window.m b/src/platform/cocoa/window.m
index f1f0f03..811a7c6 100644
--- a/src/platform/cocoa/window.m
+++ b/src/platform/cocoa/window.m
@@ -13,11 +13,18 @@
#include <assert.h>
#include <math.h>
+#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/event.h>
+#include <unistd.h>
#define LINK_IDLE_FRAMES 8
+/* Application-defined event subtype that only wakes a waiting dispatch. */
+#define LOOP_WAKE_SUBTYPE 0x5953
+/* Wait bound while some loop descriptor cannot be watched with kqueue. */
+#define LOOP_FALLBACK_MS 10
#define SCROLL_POINTS_PER_LINE 10.0
enum {
@@ -1119,6 +1126,54 @@ static void pump_window(struct cocoa_surface *w)
@autoreleasepool { render_and_commit(w); }
}
+static void loop_ready(CFFileDescriptorRef fd, CFOptionFlags flags, void *info)
+{
+ (void)fd;
+ (void)flags;
+ (void)info;
+ NSEvent *wake = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSZeroPoint
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:nil
+ subtype:LOOP_WAKE_SUBTYPE
+ data1:0
+ data2:0];
+ [NSApp postEvent:wake atStart:YES];
+}
+
+/* A kqueue mirroring the loop's poll set, or -1. complete is false when some
+ * descriptor could not be registered. */
+static int loop_kqueue(struct yasl_loop *loop, bool *complete)
+{
+ *complete = true;
+ int kq = kqueue();
+ if (kq < 0) {
+ *complete = false;
+ return -1;
+ }
+ const struct pollfd *fds;
+ int n = yasl_loop_poll_set(loop, &fds);
+ for (int i = 0; i < n; i++) {
+ struct kevent change;
+ if (fds[i].events & POLLIN) {
+ EV_SET(&change, fds[i].fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
+ if (kevent(kq, &change, 1, NULL, 0, NULL) < 0) *complete = false;
+ }
+ if (fds[i].events & POLLOUT) {
+ EV_SET(&change, fds[i].fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
+ if (kevent(kq, &change, 1, NULL, 0, NULL) < 0) *complete = false;
+ }
+ }
+ return kq;
+}
+
+static bool is_loop_wake(NSEvent *ev)
+{
+ return ev.type == NSEventTypeApplicationDefined && ev.subtype == LOOP_WAKE_SUBTYPE;
+}
+
int cocoa_display_dispatch_timeout(struct cocoa_display *d, int timeout_ms)
{
assert([NSThread isMainThread]);
@@ -1143,6 +1198,30 @@ int cocoa_display_dispatch_timeout(struct cocoa_display *d, int timeout_ms)
}
}
+ /* AppKit waits on Mach ports, not on loop descriptors. While waiting,
+ * a run loop source on a kqueue of the loop's descriptors posts a wake
+ * event, so kicks, posts, signals, and watches end the wait. */
+ CFFileDescriptorRef wake_fd = NULL;
+ CFRunLoopSourceRef wake_source = NULL;
+ if (timeout_ms != 0 && d->loop) {
+ bool complete;
+ int kq = loop_kqueue(d->loop, &complete);
+ if (kq >= 0) {
+ wake_fd = CFFileDescriptorCreate(kCFAllocatorDefault, kq, true, loop_ready, NULL);
+ if (!wake_fd) close(kq);
+ }
+ if (wake_fd) {
+ CFFileDescriptorEnableCallBacks(wake_fd, kCFFileDescriptorReadCallBack);
+ wake_source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, wake_fd, 0);
+ }
+ if (wake_source)
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), wake_source, kCFRunLoopDefaultMode);
+ else
+ complete = false;
+ if (!complete && (timeout_ms < 0 || timeout_ms > LOOP_FALLBACK_MS))
+ timeout_ms = LOOP_FALLBACK_MS;
+ }
+
NSDate *limitDate;
if (timeout_ms == 0)
limitDate = [NSDate distantPast];
@@ -1156,19 +1235,24 @@ int cocoa_display_dispatch_timeout(struct cocoa_display *d, int timeout_ms)
untilDate:limitDate
inMode:NSDefaultRunLoopMode
dequeue:YES];
- if (ev) {
- count++;
- [NSApp sendEvent:ev];
- [NSApp updateWindows];
-
- while ((ev = [NSApp nextEventMatchingMask:NSEventMaskAny
- untilDate:[NSDate distantPast]
- inMode:NSDefaultRunLoopMode
- dequeue:YES])) {
+ if (wake_source) {
+ CFRunLoopRemoveSource(CFRunLoopGetCurrent(), wake_source, kCFRunLoopDefaultMode);
+ CFRelease(wake_source);
+ }
+ if (wake_fd) {
+ CFFileDescriptorInvalidate(wake_fd);
+ CFRelease(wake_fd);
+ }
+ while (ev) {
+ if (!is_loop_wake(ev)) {
count++;
[NSApp sendEvent:ev];
[NSApp updateWindows];
}
+ ev = [NSApp nextEventMatchingMask:NSEventMaskAny
+ untilDate:[NSDate distantPast]
+ inMode:NSDefaultRunLoopMode
+ dequeue:YES];
}
}
diff --git a/src/platform/posix/loop.c b/src/platform/posix/loop.c
index 88454d8..3c3d730 100644
--- a/src/platform/posix/loop.c
+++ b/src/platform/posix/loop.c
@@ -1,10 +1,14 @@
#define _GNU_SOURCE
+#define _DARWIN_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "common/loop.h"
+#include "platform/sync.h"
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
+#include <signal.h>
+#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
@@ -31,6 +35,21 @@ struct yasl_timer_source {
bool deleted;
};
+struct yasl_post {
+ void (*fn)(void *ud);
+ void *ud;
+ struct yasl_post *next;
+};
+
+/* Signals this loop watches; the previous actions are restored on unwatch. */
+struct yasl_signal_watch {
+ void (*cb)(void *ud, int signo);
+ void *ud;
+ int count;
+ int signals[NSIG];
+ struct sigaction old[NSIG];
+};
+
struct yasl_loop {
struct yasl_fd_source *fds;
struct pollfd *pollfds;
@@ -47,8 +66,21 @@ struct yasl_loop {
int timer_capacity;
uint32_t next_timer_id;
int timer_firing_depth;
+
+ yasl_mutex post_mutex;
+ struct yasl_post *post_head;
+ struct yasl_post *post_tail;
+
+ struct yasl_signal_watch *signals;
};
+/* Handlers run on any thread, so they only touch lock-free atomics: they mark
+ * the signal pending and write to the owning loop's wake pipe. The pipe is
+ * stored as fd + 1 so that the zero initializer means no pipe. */
+static atomic_int signal_wake_fd[NSIG];
+static atomic_bool signal_pending[NSIG];
+static struct yasl_loop *signal_owner[NSIG];
+
uint64_t yasl_loop_now_ns(void)
{
struct timespec ts;
@@ -56,13 +88,41 @@ uint64_t yasl_loop_now_ns(void)
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
+static void run_posts(struct yasl_loop *l)
+{
+ yasl_mutex_lock(&l->post_mutex);
+ struct yasl_post *p = l->post_head;
+ l->post_head = l->post_tail = NULL;
+ yasl_mutex_unlock(&l->post_mutex);
+ while (p) {
+ struct yasl_post *next = p->next;
+ p->fn(p->ud);
+ free(p);
+ p = next;
+ }
+}
+
+static void run_signals(struct yasl_loop *l)
+{
+ /* A callback may unwatch, so re-check the watch before each delivery. */
+ for (int signo = 1; signo < NSIG; signo++) {
+ struct yasl_signal_watch *w = l->signals;
+ if (!w) return;
+ if (signal_owner[signo] != l || !atomic_exchange(&signal_pending[signo], false))
+ continue;
+ w->cb(w->ud, signo);
+ }
+}
+
static int loop_wake_cb(int fd, short revents, void *ud)
{
(void)revents;
- (void)ud;
char buf[128];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0 || (n < 0 && errno == EINTR)) {}
+ struct yasl_loop *l = ud;
+ run_posts(l);
+ run_signals(l);
return 0;
}
@@ -81,6 +141,13 @@ struct yasl_loop *yasl_loop_create(void)
return NULL;
}
+ if (yasl_mutex_init(&l->post_mutex) != 0) {
+ free(l->fds);
+ free(l->pollfds);
+ free(l);
+ return NULL;
+ }
+
l->next_id = 1;
l->next_timer_id = 1;
l->wake_rfd = -1;
@@ -114,6 +181,13 @@ struct yasl_loop *yasl_loop_create(void)
void yasl_loop_destroy(struct yasl_loop *l)
{
if (!l) return;
+ yasl_loop_unwatch_signals(l);
+ for (struct yasl_post *p = l->post_head; p;) {
+ struct yasl_post *next = p->next;
+ free(p);
+ p = next;
+ }
+ yasl_mutex_destroy(&l->post_mutex);
if (l->wake_watch > 0)
yasl_loop_del_fd(l, l->wake_watch);
if (l->wake_rfd >= 0)
@@ -184,14 +258,141 @@ int yasl_loop_set_fd_events(struct yasl_loop *l, int id, short events)
return -1;
}
-void yasl_loop_kick(struct yasl_loop *l)
+static void wake_write(int fd)
{
- if (!l || l->wake_wfd < 0) return;
+ /* A full pipe already wakes the loop, so EAGAIN needs no retry. */
+ int saved = errno;
char b = 1;
ssize_t n;
do {
- n = write(l->wake_wfd, &b, 1);
+ n = write(fd, &b, 1);
} while (n < 0 && errno == EINTR);
+ errno = saved;
+}
+
+void yasl_loop_kick(struct yasl_loop *l)
+{
+ if (!l || l->wake_wfd < 0) return;
+ wake_write(l->wake_wfd);
+}
+
+bool yasl_loop_post(struct yasl_loop *l, void (*fn)(void *ud), void *ud)
+{
+ if (!l || !fn || l->wake_wfd < 0) return false;
+ struct yasl_post *p = malloc(sizeof(*p));
+ if (!p) return false;
+ *p = (struct yasl_post){.fn = fn, .ud = ud};
+ yasl_mutex_lock(&l->post_mutex);
+ if (l->post_tail)
+ l->post_tail->next = p;
+ else
+ l->post_head = p;
+ l->post_tail = p;
+ yasl_mutex_unlock(&l->post_mutex);
+ wake_write(l->wake_wfd);
+ return true;
+}
+
+static void signal_handler(int signo)
+{
+ atomic_store(&signal_pending[signo], true);
+ int fd = atomic_load(&signal_wake_fd[signo]) - 1;
+ if (fd >= 0) wake_write(fd);
+}
+
+static bool signal_valid(int signo)
+{
+ return signo > 0 && signo < NSIG && signo != SIGKILL && signo != SIGSTOP;
+}
+
+bool yasl_loop_watch_signals(struct yasl_loop *l, const int *signals, int n,
+ void (*cb)(void *ud, int signo), void *ud)
+{
+ if (!l || !signals || n <= 0 || !cb || l->wake_wfd < 0) {
+ errno = EINVAL;
+ return false;
+ }
+ bool wanted[NSIG] = {0};
+ for (int i = 0; i < n; i++) {
+ if (!signal_valid(signals[i])) {
+ errno = EINVAL;
+ return false;
+ }
+ if (signal_owner[signals[i]] && signal_owner[signals[i]] != l) {
+ errno = EBUSY;
+ return false;
+ }
+ wanted[signals[i]] = true;
+ }
+ struct yasl_signal_watch *w = calloc(1, sizeof(*w));
+ if (!w) return false;
+ w->cb = cb;
+ w->ud = ud;
+
+ /* Keep the old watch's saved actions for signals that stay watched, so
+ * that unwatch restores what the application had before any watch. */
+ struct yasl_signal_watch *old = l->signals;
+ struct sigaction sa = {.sa_handler = signal_handler, .sa_flags = SA_RESTART};
+ sigemptyset(&sa.sa_mask);
+ for (int signo = 1; signo < NSIG; signo++) {
+ if (!wanted[signo]) continue;
+ int prev = -1;
+ for (int i = 0; old && i < old->count; i++)
+ if (old->signals[i] == signo) prev = i;
+ if (prev >= 0) {
+ w->old[w->count] = old->old[prev];
+ } else {
+ atomic_store(&signal_pending[signo], false);
+ atomic_store(&signal_wake_fd[signo], l->wake_wfd + 1);
+ if (sigaction(signo, &sa, &w->old[w->count]) != 0) {
+ int err = errno;
+ atomic_store(&signal_wake_fd[signo], 0);
+ for (int i = 0; i < w->count; i++) {
+ int done = w->signals[i];
+ bool kept = false;
+ for (int j = 0; old && j < old->count; j++)
+ if (old->signals[j] == done) kept = true;
+ if (kept) continue;
+ sigaction(done, &w->old[i], NULL);
+ atomic_store(&signal_wake_fd[done], 0);
+ }
+ free(w);
+ errno = err;
+ return false;
+ }
+ }
+ w->signals[w->count++] = signo;
+ }
+
+ /* Restore signals the old watch had that the new one drops. */
+ for (int i = 0; old && i < old->count; i++) {
+ int signo = old->signals[i];
+ if (wanted[signo]) continue;
+ sigaction(signo, &old->old[i], NULL);
+ atomic_store(&signal_wake_fd[signo], 0);
+ atomic_store(&signal_pending[signo], false);
+ signal_owner[signo] = NULL;
+ }
+ for (int i = 0; i < w->count; i++)
+ signal_owner[w->signals[i]] = l;
+ free(old);
+ l->signals = w;
+ return true;
+}
+
+void yasl_loop_unwatch_signals(struct yasl_loop *l)
+{
+ if (!l || !l->signals) return;
+ struct yasl_signal_watch *w = l->signals;
+ l->signals = NULL;
+ for (int i = 0; i < w->count; i++) {
+ int signo = w->signals[i];
+ sigaction(signo, &w->old[i], NULL);
+ atomic_store(&signal_wake_fd[signo], 0);
+ atomic_store(&signal_pending[signo], false);
+ signal_owner[signo] = NULL;
+ }
+ free(w);
}
static void timer_cleanup_deleted(struct yasl_loop *l)
@@ -215,6 +416,16 @@ bool yasl_loop_timer_armed(const struct yasl_loop *l, yasl_timer_id id)
return false;
}
+int yasl_loop_poll_set(const struct yasl_loop *l, const struct pollfd **fds)
+{
+ if (!l) {
+ *fds = NULL;
+ return 0;
+ }
+ *fds = l->pollfds;
+ return l->count;
+}
+
int yasl_loop_next_timeout_ms(const struct yasl_loop *l)
{
if (!l || l->timer_count <= 0) return -1;
diff --git a/tests/common/test_loop.c b/tests/common/test_loop.c
new file mode 100644
index 0000000..f982e5e
--- /dev/null
+++ b/tests/common/test_loop.c
@@ -0,0 +1,248 @@
+#define _GNU_SOURCE
+#define _DARWIN_C_SOURCE
+#include "yasl/loop.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+static void sleep_ms(int ms)
+{
+ struct timespec ts = {.tv_sec = ms / 1000, .tv_nsec = (long)(ms % 1000) * 1000000L};
+ nanosleep(&ts, NULL);
+}
+
+/* Fail instead of hanging when a wake never arrives. */
+static void on_alarm(int signo)
+{
+ (void)signo;
+ static const char msg[] = "loop test timed out\n";
+ ssize_t ignored = write(STDERR_FILENO, msg, sizeof(msg) - 1);
+ (void)ignored;
+ _exit(1);
+}
+
+struct post_log {
+ struct yasl_loop *loop;
+ char order[8];
+ int count;
+};
+
+static void post_a(void *ud);
+
+static void post_c(void *ud)
+{
+ struct post_log *log = ud;
+ log->order[log->count++] = 'c';
+}
+
+static void post_a(void *ud)
+{
+ struct post_log *log = ud;
+ log->order[log->count++] = 'a';
+ assert(yasl_loop_post(log->loop, post_c, log));
+}
+
+static void post_b(void *ud)
+{
+ struct post_log *log = ud;
+ log->order[log->count++] = 'b';
+}
+
+static void test_post_order(void)
+{
+ struct yasl_loop *l = yasl_loop_create();
+ assert(l);
+ assert(!yasl_loop_post(NULL, post_b, NULL));
+ assert(!yasl_loop_post(l, NULL, NULL));
+
+ struct post_log log = {.loop = l};
+ assert(yasl_loop_post(l, post_a, &log));
+ assert(yasl_loop_post(l, post_b, &log));
+ assert(yasl_loop_step(l, 0) >= 1);
+ /* A post made while posts run waits for a later step. */
+ assert(log.count == 2 && memcmp(log.order, "ab", 2) == 0);
+ assert(yasl_loop_step(l, 0) >= 1);
+ assert(log.count == 3 && log.order[2] == 'c');
+
+ /* Destroy drops posts that never ran. */
+ assert(yasl_loop_post(l, post_b, &log));
+ yasl_loop_destroy(l);
+ assert(log.count == 3);
+}
+
+struct thread_ctx {
+ struct yasl_loop *loop;
+ bool kick;
+ int signo;
+ _Atomic bool ran;
+};
+
+static void mark_ran(void *ud)
+{
+ struct thread_ctx *ctx = ud;
+ ctx->ran = true;
+}
+
+static void *wake_thread(void *ud)
+{
+ struct thread_ctx *ctx = ud;
+ sleep_ms(30);
+ if (ctx->signo)
+ kill(getpid(), ctx->signo);
+ else if (ctx->kick)
+ yasl_loop_kick(ctx->loop);
+ else
+ assert(yasl_loop_post(ctx->loop, mark_ran, ctx));
+ return NULL;
+}
+
+static void test_cross_thread(void)
+{
+ struct yasl_loop *l = yasl_loop_create();
+ assert(l);
+
+ struct thread_ctx post = {.loop = l};
+ pthread_t t;
+ assert(pthread_create(&t, NULL, wake_thread, &post) == 0);
+ while (!post.ran)
+ assert(yasl_loop_step(l, -1) >= 0);
+ pthread_join(t, NULL);
+
+ /* A kick ends an indefinite wait without running anything. */
+ struct thread_ctx kick = {.loop = l, .kick = true};
+ assert(pthread_create(&t, NULL, wake_thread, &kick) == 0);
+ assert(yasl_loop_step(l, -1) >= 1);
+ pthread_join(t, NULL);
+
+ yasl_loop_destroy(l);
+}
+
+struct signal_log {
+ struct yasl_loop *loop;
+ int seen[NSIG];
+ int total;
+ bool unwatch;
+};
+
+static void on_signal(void *ud, int signo)
+{
+ struct signal_log *log = ud;
+ assert(signo > 0 && signo < NSIG);
+ log->seen[signo]++;
+ log->total++;
+ if (log->unwatch)
+ yasl_loop_unwatch_signals(log->loop);
+}
+
+static volatile sig_atomic_t previous_handler_hits;
+
+static void previous_handler(int signo)
+{
+ (void)signo;
+ previous_handler_hits++;
+}
+
+static void step_until(struct yasl_loop *l, const int *counter, int want)
+{
+ while (*counter < want)
+ assert(yasl_loop_step(l, -1) >= 0);
+}
+
+static void test_signals(void)
+{
+ struct sigaction prev = {.sa_handler = previous_handler};
+ sigemptyset(&prev.sa_mask);
+ assert(sigaction(SIGUSR2, &prev, NULL) == 0);
+
+ struct yasl_loop *l = yasl_loop_create();
+ struct yasl_loop *other = yasl_loop_create();
+ assert(l && other);
+ struct signal_log log = {.loop = l};
+
+ const int bad[] = {SIGKILL};
+ errno = 0;
+ assert(!yasl_loop_watch_signals(l, bad, 1, on_signal, &log) && errno == EINVAL);
+ const int sigs[] = {SIGUSR1, SIGUSR2, SIGCHLD};
+ errno = 0;
+ assert(!yasl_loop_watch_signals(l, sigs, 0, on_signal, &log) && errno == EINVAL);
+ errno = 0;
+ assert(!yasl_loop_watch_signals(l, sigs, 3, NULL, &log) && errno == EINVAL);
+ assert(yasl_loop_watch_signals(l, sigs, 3, on_signal, &log));
+
+ /* One signal belongs to one loop at a time. */
+ errno = 0;
+ assert(!yasl_loop_watch_signals(other, sigs, 1, on_signal, &log) && errno == EBUSY);
+
+ /* Deliveries before the callback runs coalesce per signal. */
+ raise(SIGUSR1);
+ raise(SIGUSR1);
+ raise(SIGUSR2);
+ assert(yasl_loop_step(l, 0) >= 1);
+ assert(log.seen[SIGUSR1] == 1 && log.seen[SIGUSR2] == 1);
+ assert(previous_handler_hits == 0);
+
+ /* A signal sent from another thread wakes an indefinite wait. */
+ struct thread_ctx ctx = {.loop = l, .signo = SIGUSR1};
+ pthread_t t;
+ assert(pthread_create(&t, NULL, wake_thread, &ctx) == 0);
+ step_until(l, &log.seen[SIGUSR1], 2);
+ pthread_join(t, NULL);
+
+ /* SIGCHLD arrives on the loop thread when a child exits. */
+ pid_t child = fork();
+ assert(child >= 0);
+ if (child == 0) _exit(0);
+ step_until(l, &log.seen[SIGCHLD], 1);
+ int status;
+ assert(waitpid(child, &status, 0) == child);
+
+ /* Replacing the watch restores the signals it drops. */
+ const int only_usr1[] = {SIGUSR1};
+ assert(yasl_loop_watch_signals(l, only_usr1, 1, on_signal, &log));
+ raise(SIGUSR2);
+ assert(previous_handler_hits == 1);
+ assert(yasl_loop_watch_signals(other, (const int[]){SIGUSR2}, 1, on_signal, &log));
+ yasl_loop_unwatch_signals(other);
+ assert(previous_handler_hits == 1);
+ raise(SIGUSR2);
+ assert(previous_handler_hits == 2);
+
+ /* A callback may unwatch; the previous default action comes back. */
+ log.unwatch = true;
+ raise(SIGUSR1);
+ step_until(l, &log.seen[SIGUSR1], 3);
+ struct sigaction now;
+ assert(sigaction(SIGUSR1, NULL, &now) == 0);
+ assert(now.sa_handler == SIG_DFL);
+ yasl_loop_unwatch_signals(l);
+
+ /* Destroy unwatches. */
+ log.unwatch = false;
+ assert(yasl_loop_watch_signals(l, only_usr1, 1, on_signal, &log));
+ yasl_loop_destroy(l);
+ assert(sigaction(SIGUSR1, NULL, &now) == 0);
+ assert(now.sa_handler == SIG_DFL);
+ yasl_loop_destroy(other);
+
+ struct sigaction dfl = {.sa_handler = SIG_DFL};
+ sigemptyset(&dfl.sa_mask);
+ sigaction(SIGUSR2, &dfl, NULL);
+}
+
+int main(void)
+{
+ signal(SIGALRM, on_alarm);
+ alarm(10);
+ test_post_order();
+ test_cross_thread();
+ test_signals();
+ printf("Loop tests passed.\n");
+ return 0;
+}
diff --git a/tests/meson.build b/tests/meson.build
index 980f5db..f072d3f 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -109,6 +109,14 @@ timer_test = executable(
)
test('timers', timer_test)
+loop_test = executable(
+ 'yasl-loop-test',
+ 'common/test_loop.c',
+ include_directories: inc,
+ dependencies: [yasl_dep, dependency('threads')],
+)
+test('loop', loop_test)
+
app_test = executable(
'yasl-app-test',
'common/test_app.c',