204 lines
5.8 KiB
C
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);
|
|
}
|