Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ dist
test
*_test
build
build-*
config.mk
hconfig.h
html/uploads
Expand Down
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ option(WITH_MBEDTLS "with mbedtls library" OFF)

option(WITH_KCP "compile event/kcp" OFF)

if(CMAKE_SYSTEM_NAME MATCHES "Linux")
option(WITH_IO_URING "with io_uring" OFF)
endif()

if(WIN32 OR MINGW)
option(WITH_WEPOLL "compile event/wepoll -> use iocp" ON)
option(ENABLE_WINDUMP "Windows MiniDumpWriteDump" OFF)
Expand Down Expand Up @@ -165,6 +169,10 @@ if(WITH_MBEDTLS)
set(LIBS ${LIBS} mbedtls mbedx509 mbedcrypto)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIBS unconditionally appends uring when WITH_IO_URING is ON. Since liburing is Linux-only, enabling this option on non-Linux platforms will fail at link time; consider guarding this with a Linux check (e.g. if(CMAKE_SYSTEM_NAME STREQUAL "Linux")) and emitting a clear configuration error otherwise.

Suggested change
set(LIBS ${LIBS} mbedtls mbedx509 mbedcrypto)
if(WITH_IO_URING)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "WITH_IO_URING is only supported on Linux because liburing is Linux-only.")
endif()

Copilot uses AI. Check for mistakes.
endif()

if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND WITH_IO_URING)
set(LIBS ${LIBS} uring)
endif()

if(WIN32 OR MINGW)
add_definitions(-DWIN32_LEAN_AND_MEAN -D_CRT_SECURE_NO_WARNINGS -D_WIN32_WINNT=0x0600)
set(LIBS ${LIBS} secur32 crypt32 winmm iphlpapi ws2_32)
Expand Down
4 changes: 4 additions & 0 deletions Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ endif
endif
endif

ifeq ($(WITH_IO_URING), yes)
LDFLAGS += -luring
endif
Comment on lines 199 to +203
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -luring linker flag is added whenever WITH_IO_URING=yes, regardless of platform. Since liburing is Linux-specific, enabling this flag on non-Linux targets will break linking; consider additionally guarding this block with ifeq ($(OS), Linux) (or equivalent) so the option is only effective on Linux.

Suggested change
ifeq ($(WITH_IO_URING), yes)
LDFLAGS += -luring
endif
ifeq ($(OS), Linux)
ifeq ($(WITH_IO_URING), yes)
LDFLAGS += -luring
endif
endif

Copilot uses AI. Check for mistakes.

LDFLAGS += $(addprefix -L, $(LIBDIRS))
LDFLAGS += $(addprefix -l, $(LIBS))

Expand Down
3 changes: 3 additions & 0 deletions config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ WITH_MBEDTLS=no

# rudp
WITH_KCP=no

# event
WITH_IO_URING=no
4 changes: 4 additions & 0 deletions configure
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ dependencies:
rudp:
--with-kcp compile with kcp? (DEFAULT: $WITH_KCP)

event:
--with-io_uring compile with io_uring? (DEFAULT: $WITH_IO_URING)

END
}

Expand Down Expand Up @@ -298,6 +301,7 @@ option=WITH_MBEDTLS && check_option
option=ENABLE_UDS && check_option
option=USE_MULTIMAP && check_option
option=WITH_KCP && check_option
option=WITH_IO_URING && check_option

# end confile
cat << END >> $confile
Expand Down
3 changes: 1 addition & 2 deletions docs/PLAN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Done

- base: cross platfrom infrastructure
- event: select/poll/epoll/wepoll/kqueue/port
- event: select/poll/epoll/wepoll/kqueue/port/io_uring
- ssl: openssl/gnutls/mbedtls/wintls/appletls
- rudp: KCP
- evpp: c++ EventLoop interface similar to muduo and evpp
Expand All @@ -22,7 +22,6 @@
- hrpc = libhv + protobuf
- rudp: FEC, ARQ, UDT, QUIC
- kcptun
- have a taste of io_uring
- coroutine
- cppsocket.io
- IM-libhv
Expand Down
1 change: 1 addition & 0 deletions event/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
├── select.c EVENT_SELECT实现
├── poll.c EVENT_POLL实现
├── epoll.c EVENT_EPOLL实现 (for OS_LINUX)
├── io_uring.c EVENT_IO_URING实现 (for OS_LINUX, with liburing)
├── iocp.c EVENT_IOCP实现 (for OS_WIN)
├── kqueue.c EVENT_KQUEUE实现(for OS_BSD/OS_MAC)
├── evport.c EVENT_PORT实现 (for OS_SOLARIS)
Expand Down
2 changes: 2 additions & 0 deletions event/hloop.c
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,8 @@ const char* hio_engine() {
return "iocp";
#elif defined(EVENT_PORT)
return "evport";
#elif defined(EVENT_IO_URING)
return "io_uring";
#else
return "noevent";
#endif
Expand Down
2 changes: 2 additions & 0 deletions event/hloop.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ const char* hio_engine() {
return "iocp";
#elif defined(EVENT_PORT)
return "evport";
#elif defined(EVENT_IO_URING)
return "io_uring";
#else
return "noevent";
#endif
Expand Down
209 changes: 209 additions & 0 deletions event/io_uring.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#include "iowatcher.h"

#ifdef EVENT_IO_URING
#include "hplatform.h"
#include "hdef.h"
#include "hevent.h"

#include <liburing.h>
#include <poll.h>

#define IO_URING_ENTRIES 1024
#define IO_URING_CANCEL_TAG UINT64_MAX

typedef struct io_uring_ctx_s {
struct io_uring ring;
int nfds;
} io_uring_ctx_t;

int iowatcher_init(hloop_t* loop) {
if (loop->iowatcher) return 0;
io_uring_ctx_t* ctx;
HV_ALLOC_SIZEOF(ctx);
int ret = io_uring_queue_init(IO_URING_ENTRIES, &ctx->ring, 0);
if (ret < 0) {
HV_FREE(ctx);
return ret;
}
ctx->nfds = 0;
loop->iowatcher = ctx;
return 0;
}

int iowatcher_cleanup(hloop_t* loop) {
if (loop->iowatcher == NULL) return 0;
io_uring_ctx_t* ctx = (io_uring_ctx_t*)loop->iowatcher;
io_uring_queue_exit(&ctx->ring);
HV_FREE(loop->iowatcher);
return 0;
}

int iowatcher_add_event(hloop_t* loop, int fd, int events) {
if (loop->iowatcher == NULL) {
iowatcher_init(loop);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iowatcher_init(loop) can return a negative error (when io_uring_queue_init fails), but iowatcher_add_event ignores that return value and immediately dereferences loop->iowatcher. This can crash on init failure; handle the error (e.g., return it) before using loop->iowatcher.

Suggested change
iowatcher_init(loop);
int ret = iowatcher_init(loop);
if (ret < 0) {
return ret;
}

Copilot uses AI. Check for mistakes.
}
io_uring_ctx_t* ctx = (io_uring_ctx_t*)loop->iowatcher;
hio_t* io = loop->ios.ptr[fd];

unsigned poll_mask = 0;
// pre events
if (io->events & HV_READ) {
poll_mask |= POLLIN;
}
if (io->events & HV_WRITE) {
poll_mask |= POLLOUT;
}
// now events
if (events & HV_READ) {
poll_mask |= POLLIN;
}
if (events & HV_WRITE) {
poll_mask |= POLLOUT;
}

struct io_uring_sqe* sqe;
if (io->events != 0) {
// Cancel the existing poll request first
sqe = io_uring_get_sqe(&ctx->ring);
if (sqe == NULL) return -1;
io_uring_prep_poll_remove(sqe, (uint64_t)fd);
io_uring_sqe_set_data64(sqe, IO_URING_CANCEL_TAG);
} else {
ctx->nfds++;
}

// Add poll for the combined events
sqe = io_uring_get_sqe(&ctx->ring);
if (sqe == NULL) return -1;
io_uring_prep_poll_add(sqe, fd, poll_mask);
io_uring_sqe_set_data64(sqe, (uint64_t)fd);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When io_uring_get_sqe returns NULL, this function returns -1, but callers (e.g., hio_add) don't check the return value and will still set io->events. This can silently leave an fd unarmed. Consider ensuring SQE availability (submit/flush + retry, or increase ring size) and/or changing the call path to only update io->events on success.

Copilot uses AI. Check for mistakes.

io_uring_submit(&ctx->ring);
return 0;
}

int iowatcher_del_event(hloop_t* loop, int fd, int events) {
io_uring_ctx_t* ctx = (io_uring_ctx_t*)loop->iowatcher;
if (ctx == NULL) return 0;
hio_t* io = loop->ios.ptr[fd];

// Calculate remaining events
unsigned poll_mask = 0;
// pre events
if (io->events & HV_READ) {
poll_mask |= POLLIN;
}
if (io->events & HV_WRITE) {
poll_mask |= POLLOUT;
}
// now events
if (events & HV_READ) {
poll_mask &= ~POLLIN;
}
if (events & HV_WRITE) {
poll_mask &= ~POLLOUT;
}

// Cancel existing poll
struct io_uring_sqe* sqe = io_uring_get_sqe(&ctx->ring);
if (sqe == NULL) return -1;
io_uring_prep_poll_remove(sqe, (uint64_t)fd);
io_uring_sqe_set_data64(sqe, IO_URING_CANCEL_TAG);

if (poll_mask == 0) {
ctx->nfds--;
} else {
// Re-add with remaining events
sqe = io_uring_get_sqe(&ctx->ring);
if (sqe == NULL) return -1;
io_uring_prep_poll_add(sqe, fd, poll_mask);
io_uring_sqe_set_data64(sqe, (uint64_t)fd);
}
Comment on lines +120 to +134
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iowatcher_del_event returns -1 when io_uring_get_sqe fails, but hio_del doesn't check this and will still clear io->events. That can desynchronize the loop's idea of what is armed vs. what's actually registered in io_uring; handle SQE exhaustion here (submit/flush + retry) and avoid returning an unhandled error.

Copilot uses AI. Check for mistakes.

io_uring_submit(&ctx->ring);
return 0;
}

int iowatcher_poll_events(hloop_t* loop, int timeout) {
io_uring_ctx_t* ctx = (io_uring_ctx_t*)loop->iowatcher;
if (ctx == NULL) return 0;
if (ctx->nfds == 0) return 0;

struct __kernel_timespec ts;
struct __kernel_timespec* tp = NULL;
if (timeout != INFINITE) {
ts.tv_sec = timeout / 1000;
ts.tv_nsec = (timeout % 1000) * 1000000LL;
tp = &ts;
}

struct io_uring_cqe* cqe;
int ret;
if (tp) {
ret = io_uring_wait_cqe_timeout(&ctx->ring, &cqe, tp);
} else {
ret = io_uring_wait_cqe(&ctx->ring, &cqe);
}
if (ret < 0) {
if (ret == -ETIME || ret == -EINTR) {
return 0;
}
perror("io_uring_wait_cqe");
return ret;
}

int nevents = 0;
unsigned head;
unsigned ncqes = 0;
io_uring_for_each_cqe(&ctx->ring, head, cqe) {
ncqes++;
uint64_t data = io_uring_cqe_get_data64(cqe);
if (data == IO_URING_CANCEL_TAG) {
continue;
}

int fd = (int)data;
if (fd < 0 || fd >= loop->ios.maxsize) continue;
hio_t* io = loop->ios.ptr[fd];
if (io == NULL) continue;

if (cqe->res < 0) {
io->revents |= HV_READ;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On cqe->res < 0 (poll request failed), the code sets only HV_READ. If the fd is registered for write-only (e.g. connect uses HV_WRITE), the pending callback will run but hio_handle_events will ignore the event, potentially stalling the connection/error handling. Consider mapping this failure to io->revents |= (io->events ? io->events : HV_RDWR) (or at least set both READ and WRITE) so the appropriate handler runs.

Suggested change
io->revents |= HV_READ;
unsigned ev = io->events ? io->events : HV_RDWR;
io->revents |= ev;

Copilot uses AI. Check for mistakes.
EVENT_PENDING(io);
++nevents;
} else {
int revents = cqe->res;
if (revents & (POLLIN | POLLHUP | POLLERR)) {
io->revents |= HV_READ;
}
if (revents & (POLLOUT | POLLHUP | POLLERR)) {
io->revents |= HV_WRITE;
}
if (io->revents) {
EVENT_PENDING(io);
++nevents;
}
}

// io_uring POLL_ADD is one-shot, re-arm for the same events
unsigned remask = 0;
if (io->events & HV_READ) remask |= POLLIN;
if (io->events & HV_WRITE) remask |= POLLOUT;
if (remask) {
struct io_uring_sqe* sqe = io_uring_get_sqe(&ctx->ring);
if (sqe) {
io_uring_prep_poll_add(sqe, fd, remask);
io_uring_sqe_set_data64(sqe, (uint64_t)fd);
}
}
}

io_uring_cq_advance(&ctx->ring, ncqes);

if (nevents > 0) {
io_uring_submit(&ctx->ring);
}
Comment on lines +212 to +228
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

io_uring_submit is only called when nevents > 0, but this loop can queue re-arm SQEs even when no HV_READ/HV_WRITE bits were set (e.g., if cqe->res contains only poll bits you don't map to hv events). In that case the re-arm SQEs remain unsubmitted and the loop can block with nfds > 0 but no active polls. Track whether any SQEs were queued and submit based on that instead of nevents.

Copilot uses AI. Check for mistakes.

return nevents;
}
#endif
7 changes: 6 additions & 1 deletion event/iowatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
!defined(EVENT_KQUEUE) && \
!defined(EVENT_IOCP) && \
!defined(EVENT_PORT) && \
!defined(EVENT_IO_URING) && \
!defined(EVENT_NOEVENT)
#ifdef OS_WIN
#if WITH_WEPOLL
Expand All @@ -18,7 +19,11 @@
#define EVENT_POLL // WSAPoll
#endif
#elif defined(OS_LINUX)
#define EVENT_EPOLL
#if WITH_IO_URING
#define EVENT_IO_URING
#else
#define EVENT_EPOLL
#endif
#elif defined(OS_MAC)
#define EVENT_KQUEUE
#elif defined(OS_BSD)
Expand Down
1 change: 1 addition & 0 deletions hconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,6 @@

#cmakedefine WITH_WEPOLL 1
#cmakedefine WITH_KCP 1
#cmakedefine WITH_IO_URING 1

#endif // HV_CONFIG_H_