diff options
| -rw-r--r-- | docs/guide.md | 27 | ||||
| -rw-r--r-- | include/yasl/app.h | 3 | ||||
| -rw-r--r-- | include/yasl/loop.h | 63 | ||||
| -rw-r--r-- | src/common/loop.h | 5 | ||||
| -rw-r--r-- | src/platform/cocoa/window.m | 102 | ||||
| -rw-r--r-- | src/platform/posix/loop.c | 219 | ||||
| -rw-r--r-- | tests/common/test_loop.c | 248 | ||||
| -rw-r--r-- | tests/meson.build | 8 |
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', |
