commit 84c5daa19aa76209ed633581abc1f3c591ee1d8f
Author: finwo <finwo@pm.me>
Date: Wed, 18 Mar 2026 18:37:25 +0100
Project init
Diffstat:
15 files changed, 853 insertions(+), 0 deletions(-)
diff --git a/.dep.export b/.dep.export
@@ -0,0 +1 @@
+include/finwo/scheduler.h src/scheduler.h
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
@@ -0,0 +1,6 @@
+<!-- 46b43825-f791-485e-9445-415ee7bbbf2d -->
+# Contributor Code of Conduct
+
+This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters.
+
+For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage.
diff --git a/LICENSE.md b/LICENSE.md
@@ -0,0 +1,34 @@
+Copyright (c) 2026 finwo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to use, copy,
+modify, and distribute the Software, subject to the following conditions:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions, and the following disclaimer.
+
+ 2. Redistributions in binary form, or any public offering of the Software
+ (including hosted or managed services), must reproduce the above copyright
+ notice, this list of conditions, and the following disclaimer in the
+ documentation and/or other materials provided.
+
+ 3. Any redistribution or public offering of the Software must clearly attribute
+ the Software to the original copyright holder, reference this License, and
+ include a link to the official project repository or website.
+
+ 4. The Software may not be renamed, rebranded, or marketed in a manner that
+ implies it is an independent or proprietary product. Derivative works must
+ clearly state that they are based on the Software.
+
+ 5. Modifications to copies of the Software must carry prominent notices stating
+ that changes were made, the nature of the modifications, and the date of the
+ modifications.
+
+Any violation of these conditions terminates the permissions granted herein.
+
+THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
@@ -0,0 +1,275 @@
+# scheduler — Minimal Event-Loop Scheduler in C
+
+A lightweight, single-file event-loop scheduler for Unix-like systems. Uses `select(2)` for I/O multiplexing with a linked list of tasks invoked on every tick.
+
+```c
+#include "src/scheduler.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+static int greet(int64_t ts, pt_task_t *task) {
+ (void)ts; (void)task;
+ printf("Hello, world!\n");
+ return SCHED_DONE;
+}
+
+int main(void) {
+ sched_create(greet, NULL);
+ sched_main();
+ return 0;
+}
+```
+
+Compile and run:
+```sh
+gcc -o greet examples/example_basic.c src/scheduler.c && ./greet
+# Hello, world!
+```
+
+---
+
+## Features
+
+- **Minimal** — ~120 lines of C, single header + single source file
+- **No dependencies** — only `<stdint.h>` and `<sys/select.h>`
+- **I/O multiplexing** — built-in `select(2)` integration via `sched_has_data()`
+- **Cross-project friendly** — zero-configuration include via `config.mk`
+- **Extensible** — task `udata` lets you attach arbitrary state
+
+---
+
+## Installation
+
+### With dep (recommended)
+
+```sh
+dep add finwo/scheduler
+```
+
+### As a git submodule
+
+```sh
+git submodule add https://github.com/finwo/scheduler.c.git scheduler
+```
+
+Then in your `Makefile`, include `scheduler/config.mk`:
+```make
+include scheduler/config.mk
+```
+
+And add the include path:
+```make
+CFLAGS += -I$(shell pwd)/scheduler
+```
+
+### Manual
+
+Copy `src/scheduler.h` and `src/scheduler.c` into your project.
+
+---
+
+## API Reference
+
+### Status Codes
+
+| Macro | Value | Meaning |
+|-------|-------|---------|
+| `SCHED_RUNNING` | 0 | Task wants to keep running; scheduler will call it again next tick |
+| `SCHED_DONE` | 1 | Task has finished; scheduler removes it from the list |
+| `SCHED_ERROR` | 2 | Task encountered an error; scheduler removes it |
+
+### Task Callback
+
+```c
+typedef int (*pt_task_fn)(int64_t timestamp, pt_task_t *task);
+```
+
+Every task is a function of this type. It receives:
+
+- `timestamp` — current time in **milliseconds since epoch**
+- `task` — the task handle; use `task->udata` to access your attached data
+
+Return one of `SCHED_RUNNING`, `SCHED_DONE`, or `SCHED_ERROR`.
+
+### Task Handle
+
+```c
+typedef struct pt_task {
+ struct pt_task *next;
+ pt_task_fn func;
+ void *udata;
+ char is_active;
+ int maxfd;
+} pt_task_t;
+```
+
+The struct is opaque — create tasks via `sched_create()` and manage them through the public API.
+
+### `sched_create`
+
+```c
+pt_task_t *sched_create(pt_task_fn fn, void *udata);
+```
+
+Register a new task. The task is prepended to the internal linked list, so it runs **before** any existing tasks on every tick (LIFO order).
+
+- `fn` — your task callback; must not be `NULL`
+- `udata` — arbitrary pointer passed to every callback invocation
+- **Returns** — the new `pt_task_t*`, or `NULL` if `fn` is `NULL`
+
+> [!TIP]
+> Storing the returned `pt_task_t*` lets you remove the task later from any context — including from a different task or signal handler.
+
+### `sched_remove`
+
+```c
+int sched_remove(pt_task_t *task);
+```
+
+Synchronously remove a task from the scheduler and free its memory.
+
+- `task` — handle returned by `sched_create`; `NULL` is a no-op
+- **Returns** — `0` on success, `1` if `task` was not found
+
+> [!WARNING]
+> Removing a task from within its own callback is safe — the scheduler saves the `next` pointer before invoking the callback.
+
+### `sched_main`
+
+```c
+int sched_main(void);
+```
+
+Start the main event loop. Blocks until all tasks have been removed (each returned `SCHED_DONE` or `SCHED_ERROR`).
+
+- **Returns** — `0` on normal exit (all tasks completed), or immediately `0` if the task list is empty on entry
+
+### `sched_has_data`
+
+```c
+int sched_has_data(int *in_fds);
+```
+
+Check which file descriptors are ready for I/O. Call this from within a task callback. The array format is `[count, fd1, fd2, ..., fdN]`, where negative file descriptors are silently skipped.
+
+**Phase 1 — register interest:** before `select()` fires, pass your FDs to register read interest.
+
+```c
+int fds[3] = { 2, sock_fd, pipe_fd };
+int ready = sched_has_data(fds); // registers sock_fd and pipe_fd
+```
+
+**Phase 2 — check result:** after `select()` returns, call again to get the first ready FD.
+
+```c
+int ready = sched_has_data(fds); // returns first ready FD, or -1 if none
+```
+
+- **Returns** — the first ready FD from the array, or `-1` if none are ready
+
+---
+
+## Usage Examples
+
+Each example is a complete, compilable program.
+
+| Example | Demonstrates |
+|---------|-------------|
+| [example_basic.c](examples/example_basic.c) | Minimal one-shot task |
+| [example_periodic.c](examples/example_periodic.c) | Timer-based periodic task |
+| [example_io.c](examples/example_io.c) | I/O monitoring with `sched_has_data()` |
+| [example_removal.c](examples/example_removal.c) | Parent removes child task via returned `pt_task_t*` |
+| [example_multi.c](examples/example_multi.c) | Multiple coordinated tasks sharing state |
+
+See [examples/README.md](examples/README.md) for build instructions.
+
+---
+
+## Architecture
+
+### Event Loop
+
+`sched_main()` runs an infinite `for(;;)` loop:
+
+1. Scan `g_want_fds` to find the highest file descriptor
+2. Call `select()` with a **100 ms timeout** on that fd set
+3. Record the `select()` result in `g_select_result`; clear `g_want_fds`
+4. Call `gettimeofday()` and compute a millisecond timestamp
+5. Iterate the task list; invoke each task callback with the timestamp
+6. If a callback returns anything other than `SCHED_RUNNING`, remove the task
+7. Exit when the task list is empty
+
+### Task List
+
+Tasks are stored in a singly-linked list. New tasks are **prepended** to the front (LIFO), meaning the most recently registered task runs first on each tick. This is a deliberate trade-off: O(1) insertion at the cost of insertion-order unpredictability.
+
+### I/O Model
+
+The scheduler uses `select(2)` rather than `poll(2)` or `epoll(7)`. This keeps the library dependency-free and portable across all Unix systems, but carries the standard `select(2)` limitations (see [Limitations](#limitations)).
+
+---
+
+## Memory Management
+
+| Who allocates | Who frees | Notes |
+|---|---|---|
+| `sched_create()` → `udata` pointer | **Caller** | The scheduler never touches your `udata`. You must `free()` it yourself, typically in a cleanup task or after `sched_main()` returns. |
+| `sched_create()` → `pt_task_t` struct | **`sched_remove()`** | Freed automatically when the task returns `SCHED_DONE` or `SCHED_ERROR`, or when explicitly removed. |
+
+**Pattern for owned `udata`:**
+
+```c
+static int cleanup_task(int64_t ts, pt_task_t *task) {
+ MyCtx *ctx = task->udata;
+ free(ctx->buf);
+ free(ctx);
+ return SCHED_DONE;
+}
+```
+
+---
+
+## Signal Handling
+
+`sed_main()` calls `select(2)`, which is **not async-signal-safe** by itself. A few considerations:
+
+### `select()` interruption
+
+If a signal arrives while `select()` is blocked, it returns `-1` with `errno == EINTR`. The scheduler currently **ignores** this error — it loops and retries. This means signals cause a ~100 ms delay in worst case (the timeout), which is usually fine.
+
+### Calling `sched_remove()` from a signal handler
+
+This is **unsafe** — `sched_remove()` calls `free()` and modifies linked-list pointers, which are not async-signal-safe operations. If you need to stop tasks from signal handlers, use a volatile flag:
+
+```c
+static volatile sig_atomic_t keep_running = 1;
+
+static int worker(int64_t ts, pt_task_t *task) {
+ if (!keep_running) return SCHED_DONE;
+ // ... do work
+ return SCHED_RUNNING;
+}
+```
+
+Then set `keep_running = 0` from the signal handler (this is safe because `sig_atomic_t` guarantees atomic read/write).
+
+### `SA_RESTART`
+
+If you install signal handlers with `sigaction()`, consider `SA_RESTART`. This restarts `select(2)` automatically rather than returning `-1/EINTR`, which avoids the spurious loop iteration. The scheduler tolerates both modes.
+
+---
+
+## Limitations
+
+- **Not thread-safe.** All state lives in global/static variables. Use from a single thread, or wrap all calls in a mutex.
+- **`select(2)` constraint.** File descriptors are limited to `[0, FD_SETSIZE)` (typically 1024). For higher fd counts, `poll(2)` or `epoll(7)` would be needed.
+- **100 ms tick resolution.** The `select()` timeout is hardcoded to 100 ms. Tasks cannot run more frequently than this.
+- **LIFO ordering.** New tasks are prepended, not appended. Execution order on each tick is newest-first.
+- **No priorities.** All tasks are equal; there is no scheduling priority or weighting.
+- **No recurring tasks built-in.** Tasks must explicitly return `SCHED_RUNNING` to be called again.
+
+---
+
+## License
+
+See [LICENSE.md](LICENSE.md). ISC-style license — free to use, copy, modify, and distribute with attribution.
diff --git a/config.mk b/config.mk
@@ -0,0 +1 @@
+SRC+={{module.dirname}}/src/scheduler.c
diff --git a/examples/.gitignore b/examples/.gitignore
@@ -0,0 +1,6 @@
+/example_basic
+/example_io
+/example_multi
+/example_periodic
+/example_removal
+/*.dSYM/
diff --git a/examples/Makefile b/examples/Makefile
@@ -0,0 +1,27 @@
+CC ?= gcc
+CFLAGS ?= -Wall -Wextra -g -I$(shell pwd)/..
+LDFLAGS ?=
+
+EXAMPLES = example_basic example_periodic example_io example_removal example_multi
+
+.PHONY: all clean
+
+all: $(EXAMPLES)
+
+example_basic: example_basic.c ../src/scheduler.c ../src/scheduler.h
+ $(CC) $(CFLAGS) -o $@ $< ../src/scheduler.c $(LDFLAGS)
+
+example_periodic: example_periodic.c ../src/scheduler.c ../src/scheduler.h
+ $(CC) $(CFLAGS) -o $@ $< ../src/scheduler.c $(LDFLAGS)
+
+example_io: example_io.c ../src/scheduler.c ../src/scheduler.h
+ $(CC) $(CFLAGS) -o $@ $< ../src/scheduler.c $(LDFLAGS)
+
+example_removal: example_removal.c ../src/scheduler.c ../src/scheduler.h
+ $(CC) $(CFLAGS) -o $@ $< ../src/scheduler.c $(LDFLAGS)
+
+example_multi: example_multi.c ../src/scheduler.c ../src/scheduler.h
+ $(CC) $(CFLAGS) -o $@ $< ../src/scheduler.c $(LDFLAGS)
+
+clean:
+ rm -f $(EXAMPLES)
diff --git a/examples/README.md b/examples/README.md
@@ -0,0 +1,39 @@
+# Examples
+
+Each file is a complete, compilable C program demonstrating a specific aspect of the scheduler.
+
+## Building
+
+From the repository root:
+
+```sh
+make -C examples
+```
+
+Or compile a single example manually:
+
+```sh
+gcc -o greet examples/example_basic.c src/scheduler.c
+```
+
+All examples are self-contained and require only `src/scheduler.h`, `src/scheduler.c`, and the C standard library.
+
+## Index
+
+| File | Description |
+|------|-------------|
+| [example_basic.c](example_basic.c) | Minimal one-shot task — runs once and exits |
+| [example_periodic.c](example_periodic.c) | Periodic task using timestamps to run every second |
+| [example_io.c](example_io.c) | I/O monitoring on a pipe; waits for data to be readable |
+| [example_removal.c](example_removal.c) | Parent task spawns a child task, then removes it after a delay |
+| [example_multi.c](example_multi.c) | Multiple tasks sharing a context; coordinated shutdown |
+
+## Running
+
+```sh
+./example_basic # Prints "Hello, world!" and exits immediately
+./example_periodic # Prints a timestamp every second, exits after 5s
+./example_io # Reads from a pipe and prints each line
+./example_removal # Prints tick counts from two tasks; parent kills child
+./example_multi # Reader and writer tasks share a buffer via udata
+```
diff --git a/examples/example_basic.c b/examples/example_basic.c
@@ -0,0 +1,15 @@
+#include "src/scheduler.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+static int greet(int64_t ts, pt_task_t *task) {
+ (void)ts; (void)task;
+ printf("Hello, world!\n");
+ return SCHED_DONE;
+}
+
+int main(void) {
+ sched_create(greet, NULL);
+ sched_main();
+ return 0;
+}
diff --git a/examples/example_io.c b/examples/example_io.c
@@ -0,0 +1,94 @@
+#include "src/scheduler.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+typedef struct {
+ int pipe_fd;
+ int fds[2];
+ int64_t last_write;
+} WriterCtx;
+
+typedef struct {
+ int pipe_fd;
+ int fds[2];
+ int done;
+} ReaderCtx;
+
+static int writer_task(int64_t ts, pt_task_t *task) {
+ static int count = 0;
+ WriterCtx *ctx = task->udata;
+
+ if (ts - ctx->last_write >= 200) {
+ char msg[32];
+ int len = snprintf(msg, sizeof(msg), "message #%d\n", ++count);
+ write(ctx->pipe_fd, msg, len);
+ ctx->last_write = ts;
+
+ if (count >= 3) {
+ snprintf(msg, sizeof(msg), "quit\n");
+ write(ctx->pipe_fd, msg, 4);
+ close(ctx->pipe_fd);
+ free(ctx);
+ return SCHED_DONE;
+ }
+ }
+
+ return SCHED_RUNNING;
+}
+
+static int reader_task(int64_t ts, pt_task_t *task) {
+ (void)ts;
+ ReaderCtx *ctx = task->udata;
+
+ int ready = sched_has_data(ctx->fds);
+ if (ready >= 0) {
+ char buf[256];
+ ssize_t n = read(ready, buf, sizeof(buf) - 1);
+ if (n > 0) {
+ buf[n] = '\0';
+ printf("[reader] %s", buf);
+ }
+ if (n == 0 || (n > 0 && strncmp(buf, "quit\n", 5) == 0)) {
+ ctx->done = 1;
+ }
+ }
+
+ if (ctx->done) {
+ close(ctx->pipe_fd);
+ free(ctx);
+ return SCHED_DONE;
+ }
+
+ return SCHED_RUNNING;
+}
+
+int main(void) {
+ int pipefd[2];
+ if (pipe(pipefd) < 0) {
+ perror("pipe");
+ return 1;
+ }
+
+ WriterCtx *wctx = calloc(1, sizeof(WriterCtx));
+ wctx->pipe_fd = pipefd[1];
+
+ ReaderCtx *rctx = calloc(1, sizeof(ReaderCtx));
+ rctx->pipe_fd = pipefd[0];
+ rctx->fds[0] = 1;
+ rctx->fds[1] = rctx->pipe_fd;
+
+ struct timeval now;
+ gettimeofday(&now, NULL);
+ wctx->last_write = (int64_t)now.tv_sec * 1000 + now.tv_usec / 1000;
+
+ sched_create(writer_task, wctx);
+ sched_create(reader_task, rctx);
+ sched_main();
+ return 0;
+}
diff --git a/examples/example_multi.c b/examples/example_multi.c
@@ -0,0 +1,105 @@
+#include "src/scheduler.h"
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <unistd.h>
+
+#define BUF_SIZE 64
+
+typedef struct {
+ int reader_fd;
+ int writer_fd;
+ int fds_reader[2];
+ char buf[BUF_SIZE];
+ int done;
+ int64_t last_write;
+} SharedCtx;
+
+static int reader_task(int64_t ts, pt_task_t *task) {
+ (void)ts;
+ SharedCtx *ctx = task->udata;
+
+ int ready = sched_has_data(ctx->fds_reader);
+ if (ready >= 0) {
+ ssize_t n = read(ready, ctx->buf, BUF_SIZE - 1);
+ if (n > 0) {
+ ctx->buf[n] = '\0';
+ printf("[reader] received: \"%s\"\n", ctx->buf);
+ }
+ if (n == 0 || (n > 0 && strncmp(ctx->buf, "done\n", 5) == 0)) {
+ ctx->done = 1;
+ }
+ }
+
+ if (ctx->done) {
+ close(ctx->reader_fd);
+ ctx->reader_fd = -1;
+ return SCHED_DONE;
+ }
+
+ return SCHED_RUNNING;
+}
+
+static int writer_task(int64_t ts, pt_task_t *task) {
+ SharedCtx *ctx = task->udata;
+
+ if (ctx->done) {
+ return SCHED_DONE;
+ }
+
+ if (ts - ctx->last_write >= 200) {
+ static int count = 0;
+ char msg[32];
+ int len = snprintf(msg, sizeof(msg), "message #%d\n", ++count);
+ write(ctx->writer_fd, msg, len);
+ ctx->last_write = ts;
+
+ if (count >= 3) {
+ snprintf(msg, sizeof(msg), "done\n");
+ write(ctx->writer_fd, msg, 4);
+ close(ctx->writer_fd);
+ ctx->writer_fd = -1;
+ }
+ }
+
+ return SCHED_RUNNING;
+}
+
+static int shutdown_task(int64_t ts, pt_task_t *task) {
+ (void)ts;
+ SharedCtx *ctx = task->udata;
+
+ if (ctx->done && ctx->reader_fd < 0 && ctx->writer_fd < 0) {
+ printf("[shutdown] all done, cleaning up.\n");
+ free(ctx);
+ return SCHED_DONE;
+ }
+
+ return SCHED_RUNNING;
+}
+
+int main(void) {
+ int pipefd[2];
+ if (pipe(pipefd) < 0) {
+ perror("pipe");
+ return 1;
+ }
+
+ SharedCtx *ctx = calloc(1, sizeof(SharedCtx));
+ ctx->reader_fd = pipefd[0];
+ ctx->writer_fd = pipefd[1];
+ ctx->fds_reader[0] = 1;
+ ctx->fds_reader[1] = ctx->reader_fd;
+
+ struct timeval now;
+ gettimeofday(&now, NULL);
+ ctx->last_write = (int64_t)now.tv_sec * 1000 + now.tv_usec / 1000;
+
+ sched_create(writer_task, ctx);
+ sched_create(reader_task, ctx);
+ sched_create(shutdown_task, ctx);
+ sched_main();
+ return 0;
+}
diff --git a/examples/example_periodic.c b/examples/example_periodic.c
@@ -0,0 +1,41 @@
+#include "src/scheduler.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/time.h>
+
+typedef struct {
+ int64_t start;
+ int64_t interval_ms;
+ int64_t created_at;
+} TimerCtx;
+
+static int ticker(int64_t ts, pt_task_t *task) {
+ TimerCtx *ctx = task->udata;
+
+ if (ts - ctx->start >= ctx->interval_ms) {
+ printf("[%lld ms] tick!\n", (long long)(ts - ctx->created_at));
+ ctx->start = ts;
+
+ if (ts - ctx->created_at >= 5000) {
+ printf("5 seconds elapsed, stopping.\n");
+ free(ctx);
+ return SCHED_DONE;
+ }
+ }
+
+ return SCHED_RUNNING;
+}
+
+int main(void) {
+ TimerCtx *ctx = calloc(1, sizeof(TimerCtx));
+
+ struct timeval now;
+ gettimeofday(&now, NULL);
+ ctx->created_at = (int64_t)now.tv_sec * 1000 + now.tv_usec / 1000;
+ ctx->start = ctx->created_at;
+ ctx->interval_ms = 1000;
+
+ sched_create(ticker, ctx);
+ sched_main();
+ return 0;
+}
diff --git a/examples/example_removal.c b/examples/example_removal.c
@@ -0,0 +1,65 @@
+#include "src/scheduler.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/time.h>
+
+typedef struct {
+ int64_t start;
+ int ticks;
+ int removed;
+} ChildCtx;
+
+typedef struct {
+ int64_t start;
+ int64_t max_age_ms;
+ ChildCtx *child_ctx;
+ int ticks;
+} ParentCtx;
+
+static int child_task(int64_t ts, pt_task_t *task) {
+ ChildCtx *ctx = task->udata;
+ if (ctx->removed) {
+ free(ctx);
+ return SCHED_DONE;
+ }
+ ctx->ticks++;
+ printf(" [child] tick %d (age: %lld ms)\n", ctx->ticks, (long long)(ts - ctx->start));
+ return SCHED_RUNNING;
+}
+
+static int parent_task(int64_t ts, pt_task_t *task) {
+ ParentCtx *ctx = task->udata;
+ ctx->ticks++;
+ int64_t age = ts - ctx->start;
+ printf("[parent] tick %d (elapsed: %lld ms)\n", ctx->ticks, (long long)age);
+
+ if (age >= ctx->max_age_ms) {
+ printf("[parent] child is old enough, signaling it to stop.\n");
+ ctx->child_ctx->removed = 1;
+ free(ctx);
+ return SCHED_DONE;
+ }
+
+ return SCHED_RUNNING;
+}
+
+int main(void) {
+ struct timeval now;
+ gettimeofday(&now, NULL);
+ int64_t ts = (int64_t)now.tv_sec * 1000 + now.tv_usec / 1000;
+
+ ChildCtx *child_ctx = calloc(1, sizeof(ChildCtx));
+ child_ctx->start = ts;
+
+ ParentCtx *parent_ctx = calloc(1, sizeof(ParentCtx));
+ parent_ctx->start = ts;
+ parent_ctx->max_age_ms = 2500;
+ parent_ctx->child_ctx = child_ctx;
+
+ sched_create(child_task, child_ctx);
+ sched_create(parent_task, parent_ctx);
+ sched_main();
+
+ printf("Both tasks are gone. Goodbye.\n");
+ return 0;
+}
diff --git a/src/scheduler.c b/src/scheduler.c
@@ -0,0 +1,116 @@
+#include "scheduler.h"
+
+#include <stdlib.h>
+#include <sys/select.h>
+#include <sys/time.h>
+
+#ifndef NULL
+#define NULL ((void *)0)
+#endif
+
+pt_task_t *pt_first = NULL;
+fd_set g_select_result;
+static fd_set g_want_fds;
+
+pt_task_t *sched_create(pt_task_fn fn, void *udata) {
+ if (!fn) return NULL;
+
+ pt_task_t *node = calloc(1, sizeof(pt_task_t));
+ node->next = pt_first;
+ node->func = fn;
+ node->udata = udata;
+ node->is_active = 1;
+ pt_first = node;
+
+ return node;
+}
+
+int sched_remove(pt_task_t *task) {
+ if (!task) return 1;
+
+ pt_task_t *curr = pt_first;
+ pt_task_t *prev = NULL;
+
+ while (curr) {
+ if (curr == task) {
+ if (prev) {
+ prev->next = curr->next;
+ } else {
+ pt_first = curr->next;
+ }
+ free(curr);
+ return 0;
+ }
+ prev = curr;
+ curr = curr->next;
+ }
+
+ return 1;
+}
+
+int sched_has_data(int *in_fds) {
+ if (!in_fds || in_fds[0] == 0) return -1;
+
+ for (int i = 1; i <= in_fds[0]; i++) {
+ int fd = in_fds[i];
+ if (fd >= 0) {
+ FD_SET(fd, &g_want_fds);
+ }
+ }
+
+ for (int i = 1; i <= in_fds[0]; i++) {
+ int fd = in_fds[i];
+ if (fd >= 0 && FD_ISSET(fd, &g_select_result)) {
+ FD_CLR(fd, &g_select_result);
+ return fd;
+ }
+ }
+
+ return -1;
+}
+
+int sched_main(void) {
+ if (!pt_first) return 0;
+
+ struct timeval tv;
+ int maxfd;
+
+ for (;;) {
+ maxfd = -1;
+ for (int fd = 0; fd < FD_SETSIZE; fd++) {
+ if (FD_ISSET(fd, &g_want_fds)) {
+ if (fd > maxfd) maxfd = fd;
+ }
+ }
+
+ if (maxfd < 0) {
+ tv.tv_sec = 0;
+ tv.tv_usec = 100000;
+ select(0, NULL, NULL, NULL, &tv);
+ } else {
+ tv.tv_sec = 0;
+ tv.tv_usec = 100000;
+ select(maxfd + 1, &g_want_fds, NULL, NULL, &tv);
+ g_select_result = g_want_fds;
+ FD_ZERO(&g_want_fds);
+ }
+
+ struct timeval now;
+ gettimeofday(&now, NULL);
+ int64_t timestamp = (int64_t)now.tv_sec * 1000 + now.tv_usec / 1000;
+
+ pt_task_t *task = pt_first;
+ while (task) {
+ pt_task_t *next = task->next;
+ task->is_active = (task->func(timestamp, task) == SCHED_RUNNING);
+ if (!task->is_active) {
+ sched_remove(task);
+ }
+ task = next;
+ }
+
+ if (!pt_first) break;
+ }
+
+ return 0;
+}
diff --git a/src/scheduler.h b/src/scheduler.h
@@ -0,0 +1,28 @@
+#ifndef __FINWO_SCHEDULER_H__
+#define __FINWO_SCHEDULER_H__
+
+#include <stdint.h>
+#include <sys/select.h>
+
+#define SCHED_RUNNING 0
+#define SCHED_DONE 1
+#define SCHED_ERROR 2
+
+typedef struct pt_task pt_task_t;
+
+typedef int (*pt_task_fn)(int64_t timestamp, pt_task_t *task);
+
+struct pt_task {
+ struct pt_task *next;
+ pt_task_fn func;
+ void *udata;
+ char is_active;
+ int maxfd;
+};
+
+pt_task_t *sched_create(pt_task_fn fn, void *udata);
+int sched_remove(pt_task_t *task);
+int sched_main(void);
+int sched_has_data(int *in_fds);
+
+#endif // __FINWO_SCHEDULER_H__