Web/reactor/epoll.c
2025-09-15 12:57:54 -05:00

204 lines
5.8 KiB
C

#include "epoll.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
// fd_data holds per-file-descriptor data for O(1) dispatch
typedef struct fd_data {
handler_fn handler; // callback for this FD
void *user_data; // arbitrary user data passed to handler
} fd_data_t;
// runtime manages the event loop
struct runtime {
int epfd; // epoll file descriptor
int max_events; // max events per epoll_wait
struct epoll_event *events; // buffer for epoll_wait results
fd_data_t *handlers; // array indexed by FD
int max_fd; // current size of handlers array
bool running; // controls event loop
};
// runtime_new creates a new event loop runtime.
// max_events controls batching - higher values mean fewer syscalls but more latency.
// Typical values are 128-1024.
runtime_t* runtime_new(int max_events) {
// Allocate runtime structure with all fields zeroed
runtime_t *rt = calloc(1, sizeof(runtime_t));
if (!rt) return NULL;
// Create epoll instance with CLOEXEC for exec() safety
rt->epfd = epoll_create1(EPOLL_CLOEXEC);
if (rt->epfd < 0) {
free(rt);
return NULL;
}
// Allocate event buffer for epoll_wait results
rt->max_events = max_events;
rt->events = calloc(max_events, sizeof(struct epoll_event));
if (!rt->events) {
close(rt->epfd);
free(rt);
return NULL;
}
// Initialize handler array with reasonable initial size
// This will grow dynamically as needed
rt->max_fd = 1024;
rt->handlers = calloc(rt->max_fd, sizeof(fd_data_t));
if (!rt->handlers) {
free(rt->events);
close(rt->epfd);
free(rt);
return NULL;
}
rt->running = false;
return rt;
}
// runtime_free destroys a runtime and frees all resources.
// Does NOT close user file descriptors - caller is responsible for those.
void runtime_free(runtime_t *rt) {
if (!rt) return;
close(rt->epfd);
free(rt->events);
free(rt->handlers);
free(rt);
}
// ensure_capacity grows the handlers array if needed.
// The handlers array is indexed by FD for O(1) lookup.
// Growth is done in chunks of 1024 to minimize reallocations.
static int ensure_capacity(runtime_t *rt, int fd) {
if (fd >= rt->max_fd) {
// Grow by 1024 FDs at a time to reduce realloc frequency
int new_max = fd + 1024;
fd_data_t *new_handlers = realloc(rt->handlers, new_max * sizeof(fd_data_t));
if (!new_handlers) return -1;
// Zero out the new portion for NULL handler checks
memset(new_handlers + rt->max_fd, 0, (new_max - rt->max_fd) * sizeof(fd_data_t));
rt->handlers = new_handlers;
rt->max_fd = new_max;
}
return 0;
}
// runtime_add adds a file descriptor to the event loop.
// Common event flags:
// - EPOLLIN: Ready for reading
// - EPOLLOUT: Ready for writing
// - EPOLLET: Edge-triggered mode (recommended for performance)
// - EPOLLONESHOT: Disable after one event (useful for thread pools)
int runtime_add(runtime_t *rt, int fd, uint32_t events, handler_fn handler, void *data) {
// Make sure our handler array is big enough
if (ensure_capacity(rt, fd) < 0) return -1;
// Configure epoll_event - store FD for easy retrieval
struct epoll_event ev = {
.events = events,
.data.fd = fd
};
// Register with epoll
if (epoll_ctl(rt->epfd, EPOLL_CTL_ADD, fd, &ev) < 0) {
return -1;
}
// Store handler and user data for O(1) dispatch
rt->handlers[fd].handler = handler;
rt->handlers[fd].user_data = data;
return 0;
}
// runtime_mod modifies events and/or handler for an existing FD.
// Useful for switching between read/write modes or changing handlers.
int runtime_mod(runtime_t *rt, int fd, uint32_t events, handler_fn handler, void *data) {
if (ensure_capacity(rt, fd) < 0) return -1;
struct epoll_event ev = {
.events = events,
.data.fd = fd
};
// Update epoll registration
if (epoll_ctl(rt->epfd, EPOLL_CTL_MOD, fd, &ev) < 0) {
return -1;
}
// Update handler storage
rt->handlers[fd].handler = handler;
rt->handlers[fd].user_data = data;
return 0;
}
// runtime_del removes a file descriptor from the event loop.
// Does NOT close the FD - caller is responsible for that.
int runtime_del(runtime_t *rt, int fd) {
// Remove from epoll (NULL event allowed on Linux >= 2.6.9)
if (epoll_ctl(rt->epfd, EPOLL_CTL_DEL, fd, NULL) < 0) {
return -1;
}
// Clear handler storage if FD is in range
if (fd < rt->max_fd) {
rt->handlers[fd].handler = NULL;
rt->handlers[fd].user_data = NULL;
}
return 0;
}
// runtime_run starts the event loop.
// Blocks and dispatches handlers as events occur until runtime_stop() is called.
// Handlers receive: runtime, fd, events mask, and their user data.
int runtime_run(runtime_t *rt) {
rt->running = true;
while (rt->running) {
// Wait for events. -1 timeout means block indefinitely
int n = epoll_wait(rt->epfd, rt->events, rt->max_events, -1);
if (n < 0) {
// EINTR happens on signals - just retry
if (errno == EINTR) continue;
return -1;
}
// Process all ready file descriptors
for (int i = 0; i < n; i++) {
int fd = rt->events[i].data.fd;
uint32_t events = rt->events[i].events;
// Bounds check and NULL check before dispatch
// FDs can be removed by handlers, so we must check
if (fd < rt->max_fd && rt->handlers[fd].handler) {
rt->handlers[fd].handler(rt, fd, events, rt->handlers[fd].user_data);
}
}
}
return 0;
}
// runtime_stop signals the event loop to stop.
// Can be called from signal handlers or event handlers for clean shutdown.
void runtime_stop(runtime_t *rt) {
rt->running = false;
}
// set_nonblocking makes a file descriptor non-blocking.
// Essential for epoll servers - blocking I/O would freeze the entire event loop.
int set_nonblocking(int fd) {
// Get current flags
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return -1;
// Add O_NONBLOCK flag
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}