#include "epoll.h" #include #include #include #include #include // 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); }