├── .gitignore ├── README.txt ├── UNLICENSE ├── build.bat ├── build.sh ├── example.c ├── example2.c ├── io.c └── io.h /.gitignore: -------------------------------------------------------------------------------- 1 | example 2 | example.exe 3 | example2 4 | example2.exe 5 | file_*.txt -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | === TinyIO === 2 | 3 | TinyIO is a cross-platform (Windows and Linux) API for asynchronous I/O operations. It's backed by io_uring on Linux and I/O completion ports (IOCP) on Windows. It has no dependencies (other than OS stuff and freestanding libc headers) and does no dynamic allocations. To use it, you need to add io.c and io.h in your source tree and compile them as they were your own files. 4 | 5 | <<< WARNING >>> It's likely that the io_uring code has some bugs, so be wary! Also bug reports are appreciated :) 6 | 7 | It's released in the public domain, so you can just adapt the code for your own projects. Pull requests would be appreciated though. 8 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | gcc example.c io.c -o example -Wall -Wextra -ggdb -lws2_32 2 | gcc example2.c io.c -o example2 -Wall -Wextra -ggdb -lws2_32 -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | gcc example.c io.c -o example -Wall -Wextra -ggdb 2 | gcc example2.c io.c -o example2 -Wall -Wextra -ggdb 3 | -------------------------------------------------------------------------------- /example.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "io.h" 3 | 4 | #define NUM_OPS 3 5 | 6 | int main() 7 | { 8 | io_global_init(); 9 | 10 | struct io_operation ops[100]; 11 | struct io_resource res[100]; 12 | struct io_context ioc; 13 | if (!io_init(&ioc, res, ops, sizeof(res)/sizeof(res[0]), sizeof(ops)/sizeof(ops[0]))) 14 | return -1; 15 | 16 | io_handle files[NUM_OPS]; 17 | 18 | for (int i = 0; i < NUM_OPS; i++) { 19 | char name[1<<8]; 20 | snprintf(name, sizeof(name), "file_%d.txt", i); 21 | files[i] = io_create_file(&ioc, name, IO_CREATE_CANTEXIST); 22 | if (files[i] == IO_INVALID) 23 | fprintf(stderr, "Couldn't create '%s'\n", name); 24 | } 25 | 26 | int started = 0; 27 | char msg[] = "Hello, world!\n"; 28 | for (int i = 0; i < NUM_OPS; i++) { 29 | if (io_send(&ioc, NULL, files[i], msg, sizeof(msg)-1)) 30 | started++; 31 | else 32 | fprintf(stderr, "ERROR\n"); 33 | } 34 | 35 | for (int i = 0; i < started; i++) { 36 | struct io_event ev; 37 | io_wait(&ioc, &ev); 38 | fprintf(stderr, "CONCLUDED\n"); 39 | } 40 | 41 | io_free(&ioc); 42 | io_global_free(); 43 | return 0; 44 | } 45 | -------------------------------------------------------------------------------- /example2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "io.h" 4 | 5 | #define MAX_RES 100 6 | #define MAX_OPS 100 7 | 8 | int main(void) 9 | { 10 | int port = 8080; 11 | 12 | fprintf(stderr, "port=%d\n", port); 13 | 14 | struct io_operation ops[MAX_OPS]; 15 | struct io_resource res[MAX_RES]; 16 | struct io_context ioc; 17 | 18 | if (!io_global_init()) { 19 | fprintf(stderr, "Couldn't perform the global initialization\n"); 20 | return -1; 21 | } 22 | 23 | if (!io_init(&ioc, res, ops, MAX_RES, MAX_OPS)) { 24 | fprintf(stderr, "Couldn't initialize I/O context\n"); 25 | return -1; 26 | } 27 | 28 | io_handle socket = io_start_server(&ioc, NULL, port); 29 | if (socket == IO_INVALID) { 30 | fprintf(stderr, "Couldn't start listening\n"); 31 | return -1; 32 | } 33 | if (!io_accept(&ioc, NULL, socket)) { 34 | fprintf(stderr, "Couldn't start accept operation\n"); 35 | return -1; 36 | } 37 | 38 | for (;;) { 39 | struct io_event ev; 40 | io_wait(&ioc, &ev); 41 | 42 | assert(ev.optype == IO_ACCEPT); 43 | io_handle accepted = ev.accepted; 44 | 45 | fprintf(stderr, "Accepted\n"); 46 | 47 | io_close(&ioc, accepted); 48 | 49 | if (!io_accept(&ioc, NULL, socket)) { 50 | fprintf(stderr, "Couldn't start accept operation\n"); 51 | return -1; 52 | } 53 | } 54 | 55 | io_free(&ioc); 56 | io_global_free(); 57 | return 0; 58 | } -------------------------------------------------------------------------------- /io.c: -------------------------------------------------------------------------------- 1 | #include "io.h" 2 | 3 | #include 4 | 5 | #if IO_PLATFORM_WINDOWS 6 | #define WIN32_LEAN_AND_MEAN 7 | #include 8 | #include 9 | #include 10 | #endif 11 | 12 | #if IO_PLATFORM_LINUX 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #endif 23 | 24 | #define IO_DEBUG 25 | 26 | #ifdef IO_DEBUG 27 | #include 28 | #define DEBUG_LOG(fmt, ...) fprintf(stderr, "Log :: %s:%d :: " fmt, __FILE__, __LINE__, ## __VA_ARGS__); 29 | #else 30 | #define DEBUG_LOG(...) 31 | #endif 32 | 33 | bool io_global_init(void) 34 | { 35 | #if IO_PLATFORM_WINDOWS 36 | WSADATA data; 37 | return WSAStartup(MAKEWORD(2, 2), &data) == NO_ERROR; 38 | #endif 39 | 40 | #if IO_PLATFORM_LINUX 41 | return true; 42 | #endif 43 | } 44 | 45 | void io_global_free(void) 46 | { 47 | #if IO_PLATFORM_WINDOWS 48 | WSACleanup(); 49 | #endif 50 | } 51 | 52 | #if IO_PLATFORM_WINDOWS 53 | static bool io_init_windows(struct io_context *ioc) 54 | { 55 | io_os_handle os_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1); 56 | if (os_handle == INVALID_HANDLE_VALUE) 57 | return false; 58 | 59 | ioc->os_handle = os_handle; 60 | return true; 61 | } 62 | #endif 63 | 64 | #if IO_PLATFORM_WINDOWS 65 | static void io_free_windows(struct io_context *ioc) 66 | { 67 | CloseHandle(ioc->os_handle); 68 | } 69 | #endif 70 | 71 | #if IO_PLATFORM_LINUX 72 | static int 73 | io_uring_setup(unsigned entries, 74 | struct io_uring_params *p) 75 | { 76 | return (int) syscall(SYS_io_uring_setup, entries, p); 77 | } 78 | static int 79 | io_uring_enter(int ring_fd, unsigned int to_submit, 80 | unsigned int min_complete, unsigned int flags) 81 | { 82 | return (int) syscall(SYS_io_uring_enter, ring_fd, to_submit, 83 | min_complete, flags, NULL, 0); 84 | } 85 | #endif 86 | 87 | #if IO_PLATFORM_LINUX 88 | static bool io_init_linux(struct io_context *ioc) 89 | { 90 | 91 | struct io_uring_params p; 92 | void *sq_ptr, *cq_ptr; 93 | /* See io_uring_setup(2) for io_uring_params.flags you can set */ 94 | memset(&p, 0, sizeof(p)); 95 | int fd = io_uring_setup(32, &p); 96 | if (fd < 0) 97 | return false; 98 | 99 | ioc->os_handle = fd; 100 | 101 | /* 102 | * io_uring communication happens via 2 shared kernel-user space ring 103 | * buffers, which can be jointly mapped with a single mmap() call in 104 | * kernels >= 5.4. 105 | */ 106 | int sring_sz = p.sq_off.array + p.sq_entries * sizeof(unsigned); 107 | int cring_sz = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe); 108 | /* Rather than check for kernel version, the recommended way is to 109 | * check the features field of the io_uring_params structure, which is a 110 | * bitmask. If IORING_FEAT_SINGLE_MMAP is set, we can do away with the 111 | * second mmap() call to map in the completion ring separately. 112 | */ 113 | if (p.features & IORING_FEAT_SINGLE_MMAP) { 114 | if (cring_sz > sring_sz) 115 | sring_sz = cring_sz; 116 | cring_sz = sring_sz; 117 | } 118 | /* Map in the submission and completion queue ring buffers. 119 | * Kernels < 5.4 only map in the submission queue, though. 120 | */ 121 | sq_ptr = mmap(0, sring_sz, PROT_READ | PROT_WRITE, 122 | MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING); 123 | if (sq_ptr == MAP_FAILED) { 124 | // TODO: Cleanup 125 | return false; 126 | } 127 | if (p.features & IORING_FEAT_SINGLE_MMAP) { 128 | cq_ptr = sq_ptr; 129 | } else { 130 | /* Map in the completion queue ring buffer in older kernels separately */ 131 | cq_ptr = mmap(0, cring_sz, PROT_READ | PROT_WRITE, 132 | MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_CQ_RING); 133 | if (cq_ptr == MAP_FAILED) { 134 | // TODO: Cleanup 135 | return false; 136 | } 137 | } 138 | 139 | /* Save useful fields for later easy reference */ 140 | ioc->submissions.head = (_Atomic unsigned*) (sq_ptr + p.sq_off.head); 141 | ioc->submissions.tail = (_Atomic unsigned*) (sq_ptr + p.sq_off.tail); 142 | ioc->submissions.mask = (unsigned*) (sq_ptr + p.sq_off.ring_mask); 143 | ioc->submissions.array = sq_ptr + p.sq_off.array; 144 | ioc->submissions.limit = p.sq_entries; 145 | 146 | /* Map in the submission queue entries array */ 147 | ioc->submissions.entries = mmap(0, p.sq_entries * sizeof(struct io_uring_sqe), 148 | PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, 149 | fd, IORING_OFF_SQES); 150 | if (ioc->submissions.entries == MAP_FAILED) { 151 | // TODO: Cleanup 152 | return false; 153 | } 154 | 155 | /* Save useful fields for later easy reference */ 156 | ioc->completions.head = cq_ptr + p.cq_off.head; 157 | ioc->completions.tail = cq_ptr + p.cq_off.tail; 158 | ioc->completions.mask = cq_ptr + p.cq_off.ring_mask; 159 | ioc->completions.entries = cq_ptr + p.cq_off.cqes; 160 | ioc->completions.limit = p.cq_entries; 161 | return true; 162 | } 163 | #endif 164 | 165 | #if IO_PLATFORM_LINUX 166 | static void io_free_linux(struct io_context *ioc) 167 | { 168 | close(ioc->os_handle); 169 | } 170 | #endif 171 | 172 | #if IO_PLATFORM_LINUX 173 | static bool start_uring_op(struct io_context *ioc, 174 | struct io_uring_sqe sqe) 175 | { 176 | unsigned int mask = *ioc->submissions.mask; 177 | unsigned int tail = atomic_load(ioc->submissions.tail); 178 | unsigned int head = atomic_load(ioc->submissions.head); 179 | 180 | if (tail >= head + ioc->submissions.limit) 181 | return false; 182 | 183 | unsigned int index = tail & mask; 184 | ioc->submissions.entries[index] = sqe; 185 | ioc->submissions.array[index] = index; 186 | 187 | atomic_store(ioc->submissions.tail, tail+1); 188 | 189 | int ret = io_uring_enter(ioc->os_handle, 1, 0, 0); 190 | if (ret < 0) 191 | return false; 192 | 193 | return true; 194 | } 195 | #endif 196 | 197 | static void clear_res(struct io_resource *res) 198 | { 199 | res->type = IO_RES_VOID; 200 | res->pending = 0; 201 | res->callback = NULL; 202 | 203 | #if IO_PLATFORM_WINDOWS 204 | res->os_handle = INVALID_HANDLE_VALUE; 205 | #endif 206 | 207 | #if IO_PLATFORM_LINUX 208 | res->os_handle = -1; 209 | #endif 210 | } 211 | 212 | bool io_init(struct io_context *ioc, 213 | struct io_resource *res, 214 | struct io_operation *ops, 215 | uint16_t max_res, 216 | uint16_t max_ops) 217 | { 218 | ioc->res = res; 219 | ioc->ops = ops; 220 | ioc->max_res = max_res; 221 | ioc->max_ops = max_ops; 222 | 223 | for (uint16_t i = 0; i < max_res; i++) { 224 | res[i].gen = 0; 225 | clear_res(&res[i]); 226 | } 227 | 228 | for (uint16_t i = 0; i < max_ops; i++) 229 | ops[i].type = IO_VOID; 230 | 231 | #if IO_PLATFORM_WINDOWS 232 | return io_init_windows(ioc); 233 | #endif 234 | 235 | #if IO_PLATFORM_LINUX 236 | return io_init_linux(ioc); 237 | #endif 238 | } 239 | 240 | static void 241 | close_internal(struct io_context *ioc, 242 | struct io_resource *res) 243 | { 244 | #if IO_PLATFORM_WINDOWS 245 | if (res->type == IO_RES_SOCKET) 246 | closesocket((SOCKET) res->os_handle); 247 | else 248 | CloseHandle(res->os_handle); 249 | #elif IO_PLATFORM_LINUX 250 | close(res->os_handle); 251 | #endif 252 | 253 | // Mark associated operation structures as unused 254 | for (uint16_t i = 0, marked = 0; marked < res->pending; i++) { 255 | struct io_operation *op; 256 | op = &ioc->ops[i]; 257 | if (op->type != IO_VOID && op->res == res) { 258 | op->type = IO_VOID; 259 | op->res = NULL; 260 | marked++; 261 | } 262 | } 263 | 264 | clear_res(res); 265 | 266 | res->gen++; 267 | if (res->gen == UINT16_MAX) 268 | res->gen = 0; 269 | } 270 | 271 | static struct io_resource* 272 | res_from_handle(struct io_context *ioc, io_handle handle) 273 | { 274 | if (handle == IO_INVALID) 275 | return NULL; 276 | 277 | static_assert(sizeof(uint32_t) == sizeof(io_handle)); 278 | uint16_t idx = handle & 0xFFFF; 279 | uint16_t gen = handle >> 16; 280 | if (idx >= ioc->max_res) 281 | return NULL; 282 | 283 | struct io_resource *res = &ioc->res[idx]; 284 | if (res->gen != gen) 285 | return NULL; 286 | 287 | return res; 288 | } 289 | 290 | static io_handle handle_from_res(struct io_context *ioc, 291 | struct io_resource *res) 292 | { 293 | io_handle handle; 294 | static_assert(sizeof(uint32_t) == sizeof(io_handle)); 295 | 296 | uint32_t idx = res - ioc->res; 297 | uint32_t gen = res->gen; 298 | handle = idx | (gen << 16); 299 | 300 | assert(gen != UINT16_MAX); 301 | assert(handle != IO_INVALID); 302 | 303 | return handle; 304 | } 305 | 306 | void io_close(struct io_context *ioc, 307 | io_handle handle) 308 | { 309 | struct io_resource *res; 310 | 311 | res = res_from_handle(ioc, handle); 312 | if (res == NULL) 313 | return; 314 | 315 | close_internal(ioc, res); 316 | } 317 | 318 | void io_free(struct io_context *ioc) 319 | { 320 | for (uint16_t i = 0; i < ioc->max_res; i++) 321 | if (ioc->res[i].type != IO_RES_VOID) 322 | close_internal(ioc, &ioc->res[i]); 323 | 324 | #if IO_PLATFORM_WINDOWS 325 | io_free_windows(ioc); 326 | #endif 327 | 328 | #if IO_PLATFORM_LINUX 329 | io_free_linux(ioc); 330 | #endif 331 | } 332 | 333 | static struct io_operation* 334 | find_unused_op(struct io_context *ioc) 335 | { 336 | for (uint16_t i = 0; i < ioc->max_ops; i++) { 337 | struct io_operation *op = &ioc->ops[i]; 338 | if (op->type == IO_VOID) 339 | return op; 340 | } 341 | return NULL; 342 | } 343 | 344 | #if IO_PLATFORM_LINUX 345 | static bool io_recv_linux(struct io_context *ioc, 346 | struct io_resource *res, 347 | struct io_operation *op, 348 | void *dst, uint32_t max) 349 | { 350 | struct io_uring_sqe sqe; 351 | memset(&sqe, 0, sizeof(sqe)); 352 | sqe.opcode = IORING_OP_READ; 353 | sqe.fd = (int) res->os_handle; 354 | sqe.addr = (uint64_t) dst; 355 | sqe.len = max; 356 | sqe.user_data = (uint64_t) op; 357 | return start_uring_op(ioc, sqe); 358 | } 359 | #endif 360 | 361 | #if IO_PLATFORM_LINUX 362 | static bool io_send_linux(struct io_context *ioc, 363 | struct io_resource *res, 364 | struct io_operation *op, 365 | void *src, uint32_t num) 366 | { 367 | struct io_uring_sqe sqe; 368 | memset(&sqe, 0, sizeof(sqe)); 369 | sqe.opcode = IORING_OP_WRITE; 370 | sqe.fd = (int) res->os_handle; 371 | sqe.addr = (uint64_t) src; 372 | sqe.len = num; 373 | sqe.user_data = (uint64_t) op; 374 | return start_uring_op(ioc, sqe); 375 | } 376 | #endif 377 | 378 | #if IO_PLATFORM_LINUX 379 | static bool io_accept_linux(struct io_context *ioc, 380 | struct io_resource *res, 381 | struct io_operation *op, 382 | io_os_handle os_handle) 383 | { 384 | (void) ioc; 385 | (void) res; 386 | 387 | struct io_uring_sqe sqe; 388 | memset(&sqe, 0, sizeof(sqe)); 389 | sqe.opcode = IORING_OP_ACCEPT; 390 | sqe.fd = (int) os_handle; 391 | sqe.user_data = (uint64_t) op; 392 | return start_uring_op(ioc, sqe); 393 | } 394 | #endif 395 | 396 | #if IO_PLATFORM_WINDOWS 397 | static bool io_recv_windows(struct io_context *ioc, 398 | struct io_resource *res, 399 | struct io_operation *op, 400 | void *dst, uint32_t max) 401 | { 402 | (void) ioc; 403 | 404 | memset(&op->ov, 0, sizeof(struct io_os_overlap)); 405 | int ok = ReadFile(res->os_handle, dst, max, NULL, (OVERLAPPED*) &op->ov); 406 | if (!ok && GetLastError() != ERROR_IO_PENDING) 407 | return false; 408 | return true; 409 | } 410 | #endif 411 | 412 | #if IO_PLATFORM_WINDOWS 413 | static bool io_send_windows(struct io_context *ioc, 414 | struct io_resource *res, 415 | struct io_operation *op, 416 | void *src, uint32_t num) 417 | { 418 | (void) ioc; 419 | 420 | memset(&op->ov, 0, sizeof(struct io_os_overlap)); 421 | int ok = ReadFile(res->os_handle, src, num, NULL, (OVERLAPPED*) &op->ov); 422 | if (!ok && GetLastError() != ERROR_IO_PENDING) 423 | return false; 424 | return true; 425 | } 426 | #endif 427 | 428 | #if IO_PLATFORM_WINDOWS 429 | static bool io_accept_windows(struct io_context *ioc, 430 | struct io_resource *res, 431 | struct io_operation *op, 432 | io_os_handle os_handle) 433 | { 434 | (void) ioc; 435 | (void) res; 436 | 437 | memset(&op->ov, 0, sizeof(struct io_os_overlap)); 438 | 439 | SOCKET new_os_handle = socket(AF_INET, SOCK_STREAM, 0); 440 | if (new_os_handle == INVALID_SOCKET) 441 | return false; 442 | 443 | LPFN_ACCEPTEX lpfnAcceptEx = res->acceptfn; 444 | 445 | _Static_assert(IO_SOCKADDR_IN_SIZE == sizeof(struct sockaddr_in)); 446 | 447 | unsigned long num; 448 | int ok = lpfnAcceptEx((SOCKET) os_handle, new_os_handle, res->accept_buffer, 449 | sizeof(res->accept_buffer) - ((sizeof(struct sockaddr_in) + 16) * 2), 450 | sizeof(struct sockaddr_in) + 16, 451 | sizeof(struct sockaddr_in) + 16, 452 | &num, (OVERLAPPED*) &op->ov); 453 | if (!ok && GetLastError() != ERROR_IO_PENDING) { 454 | DEBUG_LOG("AcceptEx failure\n"); 455 | closesocket(new_os_handle); 456 | return false; 457 | } 458 | 459 | op->accepted = (io_os_handle) new_os_handle; 460 | return true; 461 | } 462 | #endif 463 | 464 | bool io_recv(struct io_context *ioc, 465 | void *user, io_handle handle, 466 | void *dst, uint32_t max) 467 | { 468 | struct io_operation *op; 469 | struct io_resource *res; 470 | 471 | res = res_from_handle(ioc, handle); 472 | if (res == NULL) 473 | return false; 474 | 475 | op = find_unused_op(ioc); 476 | if (op == NULL) 477 | return false; 478 | 479 | enum io_optype type = IO_RECV; 480 | 481 | #if IO_PLATFORM_LINUX 482 | if (!io_recv_linux(ioc, res, op, dst, max)) 483 | return false; 484 | #endif 485 | 486 | #if IO_PLATFORM_WINDOWS 487 | if (!io_recv_windows(ioc, res, op, dst, max)) 488 | return false; 489 | #endif 490 | 491 | res->pending++; 492 | op->res = res; 493 | op->type = type; 494 | op->user = user; 495 | return true; 496 | } 497 | 498 | bool io_send(struct io_context *ioc, 499 | void *user, io_handle handle, 500 | void *src, uint32_t num) 501 | { 502 | struct io_operation *op; 503 | struct io_resource *res; 504 | 505 | res = res_from_handle(ioc, handle); 506 | if (res == NULL) 507 | return false; 508 | 509 | op = find_unused_op(ioc); 510 | if (op == NULL) 511 | return false; 512 | 513 | enum io_optype type = IO_SEND; 514 | 515 | #if IO_PLATFORM_LINUX 516 | if (!io_send_linux(ioc, res, op, src, num)) 517 | return false; 518 | #endif 519 | 520 | #if IO_PLATFORM_WINDOWS 521 | if (!io_send_windows(ioc, res, op, src, num)) 522 | return false; 523 | #endif 524 | 525 | res->pending++; 526 | op->res = res; 527 | op->type = type; 528 | op->user = user; 529 | return true; 530 | } 531 | 532 | bool io_accept(struct io_context *ioc, 533 | void *user, io_handle handle) 534 | { 535 | struct io_operation *op; 536 | struct io_resource *res; 537 | 538 | res = res_from_handle(ioc, handle); 539 | if (res == NULL) 540 | return false; 541 | 542 | op = find_unused_op(ioc); 543 | if (op == NULL) 544 | return false; 545 | 546 | enum io_optype type = IO_ACCEPT; 547 | 548 | #if IO_PLATFORM_LINUX 549 | if (!io_accept_linux(ioc, res, op, res->os_handle)) 550 | return false; 551 | #endif 552 | 553 | #if IO_PLATFORM_WINDOWS 554 | if (!io_accept_windows(ioc, res, op, res->os_handle)) 555 | return false; 556 | #endif 557 | 558 | res->pending++; 559 | op->res = res; 560 | op->type = type; 561 | op->user = user; 562 | return true; 563 | } 564 | 565 | static struct io_resource* 566 | find_unused_res(struct io_context *ioc) 567 | { 568 | for (uint16_t i = 0; i < ioc->max_res; i++) { 569 | struct io_resource *res = &ioc->res[i]; 570 | if (res->type == IO_RES_VOID) 571 | return res; 572 | } 573 | return NULL; 574 | } 575 | 576 | #if IO_PLATFORM_WINDOWS 577 | static io_os_handle 578 | io_open_file_windows(struct io_context *ioc, 579 | const char *file, int flags) 580 | { 581 | unsigned long access = 0; 582 | if (flags & IO_ACCESS_RD) access |= GENERIC_READ; 583 | if (flags & IO_ACCESS_WR) access |= GENERIC_WRITE; 584 | 585 | io_os_handle os_handle = CreateFileA(file, access, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); 586 | if (os_handle == INVALID_HANDLE_VALUE) 587 | return INVALID_HANDLE_VALUE; 588 | 589 | if (CreateIoCompletionPort(os_handle, ioc->os_handle, 0, 0) == NULL) { 590 | CloseHandle(os_handle); 591 | return INVALID_HANDLE_VALUE; 592 | } 593 | 594 | return os_handle; 595 | } 596 | #endif 597 | 598 | #if IO_PLATFORM_LINUX 599 | static io_os_handle 600 | io_open_file_linux(struct io_context *ioc, 601 | const char *file, int flags) 602 | { 603 | (void) ioc; 604 | 605 | int flags2 = 0; 606 | if (flags & IO_ACCESS_RD) flags2 |= O_RDONLY; 607 | if (flags & IO_ACCESS_WR) flags2 |= O_WRONLY; 608 | 609 | return open(file, flags2); 610 | } 611 | #endif 612 | 613 | io_handle io_open_file(struct io_context *ioc, 614 | const char *file, int flags) 615 | { 616 | io_os_handle os_handle; 617 | struct io_resource *res; 618 | 619 | res = find_unused_res(ioc); 620 | if (res == NULL) 621 | return IO_INVALID; 622 | 623 | #if IO_PLATFORM_WINDOWS 624 | os_handle = io_open_file_windows(ioc, file, flags); 625 | if (os_handle == INVALID_HANDLE_VALUE) 626 | return IO_INVALID; 627 | #endif 628 | 629 | #if IO_PLATFORM_LINUX 630 | os_handle = io_open_file_linux(ioc, file, flags); 631 | if (os_handle < 0) 632 | return IO_INVALID; 633 | #endif 634 | 635 | res->type = IO_RES_FILE; 636 | res->pending = 0; 637 | res->os_handle = os_handle; 638 | return handle_from_res(ioc, res); 639 | } 640 | 641 | #if IO_PLATFORM_WINDOWS 642 | static io_os_handle 643 | io_create_file_windows(struct io_context *ioc, 644 | const char *file, int flags) 645 | { 646 | unsigned long flags2 = 0; 647 | 648 | if (flags & IO_CREATE_CANTEXIST) 649 | flags2 = CREATE_NEW; 650 | else { 651 | if (flags & IO_CREATE_OVERWRITE) 652 | flags2 = CREATE_ALWAYS; 653 | else 654 | flags2 = OPEN_ALWAYS; 655 | } 656 | 657 | io_os_handle os_handle = CreateFileA(file, GENERIC_WRITE, 0, NULL, flags2, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); 658 | if (os_handle == INVALID_HANDLE_VALUE) 659 | return INVALID_HANDLE_VALUE; 660 | 661 | if (CreateIoCompletionPort(os_handle, ioc->os_handle, 0, 0) == NULL) { 662 | CloseHandle(os_handle); 663 | return INVALID_HANDLE_VALUE; 664 | } 665 | 666 | return os_handle; 667 | } 668 | #endif 669 | 670 | #if IO_PLATFORM_LINUX 671 | static io_os_handle 672 | io_create_file_linux(struct io_context *ioc, 673 | const char *file, int flags) 674 | { 675 | (void) ioc; 676 | 677 | int flags2 = O_CREAT | O_WRONLY; 678 | 679 | if (flags & IO_CREATE_CANTEXIST) 680 | flags2 |= O_EXCL; 681 | else { 682 | if (flags & IO_CREATE_OVERWRITE) 683 | flags2 |= O_TRUNC; 684 | } 685 | 686 | // TODO: is 0666 ok? 687 | return open(file, flags2, 0666); 688 | } 689 | #endif 690 | 691 | io_handle io_create_file(struct io_context *ioc, 692 | const char *file, int flags) 693 | { 694 | io_os_handle os_handle; 695 | struct io_resource *res; 696 | 697 | res = find_unused_res(ioc); 698 | if (res == NULL) 699 | return IO_INVALID; 700 | 701 | #if IO_PLATFORM_WINDOWS 702 | os_handle = io_create_file_windows(ioc, file, flags); 703 | if (os_handle == INVALID_HANDLE_VALUE) 704 | return IO_INVALID; 705 | #endif 706 | 707 | #if IO_PLATFORM_LINUX 708 | os_handle = io_create_file_linux(ioc, file, flags); 709 | if (os_handle < 0) 710 | return IO_INVALID; 711 | #endif 712 | 713 | res->type = IO_RES_FILE; 714 | res->pending = 0; 715 | res->os_handle = os_handle; 716 | return handle_from_res(ioc, res); 717 | } 718 | 719 | io_handle io_start_server(struct io_context *ioc, 720 | const char *addr, int port) 721 | { 722 | if (port < 1 || port > UINT16_MAX) 723 | return IO_INVALID; 724 | 725 | struct in_addr addr2; 726 | if (addr == NULL) 727 | addr2.s_addr = INADDR_ANY; 728 | else { 729 | if (1 != inet_pton(AF_INET, addr, &addr2)) 730 | return IO_INVALID; 731 | } 732 | 733 | #if IO_PLATFORM_WINDOWS 734 | SOCKET fd; 735 | #endif 736 | 737 | #if IO_PLATFORM_LINUX 738 | int fd; 739 | #endif 740 | 741 | struct io_resource *res; 742 | 743 | res = find_unused_res(ioc); 744 | if (res == NULL) 745 | return IO_INVALID; 746 | 747 | fd = socket(AF_INET, SOCK_STREAM, 0); 748 | 749 | #if IO_PLATFORM_LINUX 750 | if (fd < 0) 751 | return IO_INVALID; 752 | #endif 753 | 754 | #if IO_PLATFORM_WINDOWS 755 | if (fd == INVALID_SOCKET) 756 | return IO_INVALID; 757 | #endif 758 | 759 | int one = 1; 760 | setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*) &one, sizeof(one)); 761 | 762 | struct sockaddr_in buf; 763 | buf.sin_family = AF_INET; 764 | buf.sin_port = htons(port); 765 | buf.sin_addr = addr2; 766 | if (bind(fd, (struct sockaddr*) &buf, sizeof(buf))) { 767 | #if IO_PLATFORM_WINDOWS 768 | closesocket(fd); 769 | #else 770 | close(fd); 771 | #endif 772 | return IO_INVALID; 773 | } 774 | 775 | int backlog = 32; 776 | if (listen(fd, backlog)) { 777 | #if IO_PLATFORM_WINDOWS 778 | closesocket(fd); 779 | #else 780 | close(fd); 781 | #endif 782 | return IO_INVALID; 783 | } 784 | 785 | #if IO_PLATFORM_WINDOWS 786 | LPFN_ACCEPTEX lpfnAcceptEx = NULL; 787 | GUID GuidAcceptEx = WSAID_ACCEPTEX; 788 | unsigned long num; 789 | int ret = WSAIoctl(fd, 790 | SIO_GET_EXTENSION_FUNCTION_POINTER, 791 | &GuidAcceptEx, sizeof(GuidAcceptEx), 792 | &lpfnAcceptEx, sizeof(lpfnAcceptEx), 793 | &num, NULL, NULL); 794 | if (ret == SOCKET_ERROR) { 795 | DEBUG_LOG("WSAIoctl failure\n"); 796 | closesocket(fd); 797 | return IO_INVALID; 798 | } 799 | if (CreateIoCompletionPort((HANDLE) fd, ioc->os_handle, 0, 0) == NULL) { 800 | closesocket(fd); 801 | return IO_INVALID; 802 | } 803 | res->acceptfn = lpfnAcceptEx; 804 | #endif 805 | 806 | res->type = IO_RES_SOCKET; 807 | res->pending = 0; 808 | res->os_handle = (io_os_handle) fd; 809 | return handle_from_res(ioc, res); 810 | } 811 | 812 | #if IO_PLATFORM_WINDOWS 813 | static struct io_operation* 814 | op_from_ov(struct io_os_overlap *ov) 815 | { 816 | return (struct io_operation*) ((char*) ov - offsetof(struct io_operation, ov)); 817 | } 818 | #endif 819 | 820 | #if IO_PLATFORM_WINDOWS 821 | static void 822 | io_wait_internal_windows(struct io_context *ioc, 823 | struct io_event *ev) 824 | { 825 | int timeout = -1; 826 | 827 | unsigned long timeout2; 828 | if (timeout < 0) 829 | timeout2 = INFINITE; 830 | else 831 | timeout2 = timeout; 832 | 833 | unsigned long long unused; 834 | struct io_os_overlap *ov; 835 | unsigned long num; 836 | int ok = GetQueuedCompletionStatus(ioc->os_handle, &num, &unused, (OVERLAPPED**) &ov, timeout2); 837 | 838 | if (!ok) { 839 | 840 | if (ov == NULL) { 841 | 842 | /* 843 | * General failure 844 | */ 845 | 846 | ev->evtype = IO_ERROR; 847 | ev->optype = IO_VOID; 848 | ev->handle = IO_INVALID; 849 | ev->user = NULL; 850 | 851 | } else { 852 | 853 | /* 854 | * Operation failure 855 | */ 856 | 857 | struct io_operation *op = op_from_ov(ov); 858 | struct io_resource *res = op->res; 859 | 860 | ev->evtype = IO_ABORT; 861 | ev->optype = op->type; 862 | ev->handle = handle_from_res(ioc, res); 863 | ev->user = op->user; 864 | 865 | if (op->type == IO_ACCEPT) 866 | closesocket((SOCKET) op->accepted); 867 | 868 | op->type = IO_VOID; // Mark unused 869 | 870 | assert(res->pending > 0); 871 | res->pending--; 872 | } 873 | return; 874 | } 875 | 876 | struct io_operation *op = op_from_ov(ov); 877 | struct io_resource *res = op->res; 878 | 879 | ev->evtype = IO_COMPLETE; 880 | ev->optype = op->type; 881 | ev->handle = handle_from_res(ioc, res); 882 | ev->user = op->user; 883 | 884 | switch (op->type) { 885 | 886 | case IO_RECV: 887 | case IO_SEND: 888 | ev->num = num; 889 | break; 890 | 891 | case IO_ACCEPT: 892 | { 893 | struct io_resource *res2; 894 | 895 | res2 = find_unused_res(ioc); 896 | if (res2 == NULL) { 897 | 898 | closesocket((SOCKET) op->accepted); 899 | 900 | ev->evtype = IO_ABORT; 901 | ev->optype = IO_ACCEPT; 902 | ev->handle = handle_from_res(ioc, res); 903 | ev->user = op->user; 904 | 905 | assert(res->pending > 0); 906 | res->pending--; 907 | op->type = IO_VOID; 908 | return; 909 | } 910 | 911 | res2->type = IO_RES_SOCKET; 912 | res2->pending = 0; 913 | res2->os_handle = op->accepted; 914 | 915 | ev->accepted = handle_from_res(ioc, res2); 916 | } 917 | break; 918 | 919 | default: 920 | break; 921 | } 922 | 923 | assert(res->pending > 0); 924 | res->pending--; 925 | 926 | op->type = IO_VOID; // Mark unused 927 | } 928 | #endif 929 | 930 | #if IO_PLATFORM_LINUX 931 | static void 932 | io_wait_internal_linux(struct io_context *ioc, 933 | struct io_event *ev) 934 | { 935 | /* --- Read barrier --- */ 936 | unsigned int head = atomic_load(ioc->completions.head); 937 | unsigned int tail = atomic_load(ioc->completions.tail); 938 | 939 | if (head == tail) { 940 | 941 | /* 942 | * Completion queue is empty. Wait for some operations to complete. 943 | */ 944 | int ret = io_uring_enter(ioc->os_handle, 0, 1, IORING_ENTER_GETEVENTS); 945 | if (ret < 0) { 946 | ev->evtype = IO_ERROR; 947 | ev->optype = IO_VOID; 948 | ev->handle = IO_INVALID; 949 | ev->user = NULL; 950 | return; 951 | } 952 | } 953 | 954 | struct io_uring_cqe *cqe; 955 | struct io_operation *op; 956 | struct io_resource *res; 957 | 958 | cqe = &ioc->completions.entries[head & (*ioc->completions.mask)]; 959 | 960 | op = (void*) cqe->user_data; 961 | res = op->res; 962 | 963 | ev->user = op->user; 964 | ev->handle = handle_from_res(ioc, op->res); 965 | ev->optype = op->type; 966 | 967 | if (cqe->res < 0) 968 | ev->evtype = IO_ABORT; 969 | else { 970 | ev->evtype = IO_COMPLETE; 971 | switch (op->type) { 972 | case IO_RECV: ev->num = cqe->res; break; 973 | case IO_SEND: ev->num = cqe->res; break; 974 | case IO_ACCEPT: ev->accepted = cqe->res; break; 975 | default:break; 976 | } 977 | } 978 | 979 | assert(res->pending > 0); 980 | res->pending--; 981 | op->type = IO_VOID; // Mark unused 982 | 983 | /* --- write barrier --- */ 984 | atomic_store(ioc->completions.head, head+1); 985 | } 986 | #endif 987 | 988 | static void 989 | io_wait_internal(struct io_context *ioc, 990 | struct io_event *ev) 991 | { 992 | #if IO_PLATFORM_WINDOWS 993 | io_wait_internal_windows(ioc, ev); 994 | #endif 995 | 996 | #if IO_PLATFORM_LINUX 997 | io_wait_internal_linux(ioc, ev); 998 | #endif 999 | } 1000 | 1001 | void io_wait(struct io_context *ioc, 1002 | struct io_event *ev) 1003 | { 1004 | for (;;) { 1005 | 1006 | io_wait_internal(ioc, ev); 1007 | 1008 | if (ev->handle == IO_INVALID) 1009 | break; 1010 | 1011 | assert(ev->handle != IO_INVALID); 1012 | 1013 | struct io_resource *res; 1014 | res = res_from_handle(ioc, ev->handle); 1015 | assert(res); 1016 | 1017 | if (res->callback == NULL) 1018 | break; 1019 | 1020 | res->callback(ioc, *ev); 1021 | } 1022 | } 1023 | 1024 | void io_set_callback(struct io_context *ioc, 1025 | io_handle handle, 1026 | io_callback callback) 1027 | { 1028 | struct io_resource *res; 1029 | res = res_from_handle(ioc, handle); 1030 | if (res == NULL) 1031 | return; 1032 | res->callback = callback; 1033 | } -------------------------------------------------------------------------------- /io.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define IO_VERSION_MAJOR 0 5 | #define IO_VERSION_MINOR 0 6 | 7 | #ifdef _WIN32 8 | # define IO_PLATFORM_WINDOWS 1 9 | # define IO_PLATFORM_LINUX 0 10 | # define IO_PLATFORM_OTHER 0 11 | #elif __linux__ 12 | # define IO_PLATFORM_WINDOWS 0 13 | # define IO_PLATFORM_LINUX 1 14 | # define IO_PLATFORM_OTHER 0 15 | #else 16 | # define IO_PLATFORM_WINDOWS 0 17 | # define IO_PLATFORM_LINUX 0 18 | # define IO_PLATFORM_OTHER 1 19 | #endif 20 | 21 | #if IO_PLATFORM_WINDOWS 22 | typedef void *io_os_handle; 23 | #endif 24 | 25 | #if IO_PLATFORM_LINUX 26 | typedef int io_os_handle; 27 | #endif 28 | 29 | typedef uint32_t io_handle; 30 | #define IO_INVALID ((uint32_t) -1) 31 | 32 | struct io_os_overlap { 33 | unsigned long *internal; 34 | unsigned long *internal_high; 35 | union { 36 | struct { 37 | unsigned long offset; 38 | unsigned long offset_high; 39 | }; 40 | void *pointer; 41 | }; 42 | void *event; 43 | }; 44 | 45 | enum io_optype { 46 | IO_VOID, 47 | IO_RECV, 48 | IO_SEND, 49 | IO_ACCEPT, 50 | }; 51 | 52 | #define IO_SOCKADDR_IN_SIZE 16 53 | 54 | struct io_operation { 55 | 56 | enum io_optype type; 57 | struct io_resource *res; 58 | void *user; 59 | 60 | #if IO_PLATFORM_WINDOWS 61 | io_os_handle accepted; 62 | struct io_os_overlap ov; 63 | #endif 64 | }; 65 | 66 | enum io_evtype { 67 | IO_ERROR, 68 | IO_ABORT, 69 | IO_COMPLETE, 70 | }; 71 | 72 | struct io_event { 73 | enum io_evtype evtype; 74 | enum io_optype optype; 75 | io_handle handle; 76 | void *user; 77 | 78 | union { 79 | uint32_t num; 80 | io_handle accepted; 81 | }; 82 | }; 83 | 84 | struct io_context; 85 | 86 | typedef void (*io_callback)(struct io_context *ioc, struct io_event); 87 | 88 | enum io_restype { 89 | IO_RES_VOID, 90 | IO_RES_FILE, 91 | IO_RES_SOCKET, 92 | }; 93 | 94 | struct io_resource { 95 | enum io_restype type; 96 | io_os_handle os_handle; 97 | uint16_t pending; 98 | uint16_t gen; 99 | 100 | io_callback callback; 101 | 102 | #if IO_PLATFORM_WINDOWS 103 | void *acceptfn; 104 | char accept_buffer[2 * (IO_SOCKADDR_IN_SIZE + 16)]; 105 | #endif 106 | }; 107 | 108 | /* 109 | * io_uring's input queue 110 | */ 111 | #if IO_PLATFORM_LINUX 112 | struct io_submission_queue { 113 | _Atomic unsigned int *head; 114 | _Atomic unsigned int *tail; 115 | unsigned int *mask; 116 | unsigned int *array; 117 | unsigned int limit; 118 | struct io_uring_sqe *entries; 119 | }; 120 | #endif 121 | 122 | /* 123 | * io_uring's output queue 124 | */ 125 | #if IO_PLATFORM_LINUX 126 | struct io_completion_queue { 127 | _Atomic unsigned int *head; 128 | _Atomic unsigned int *tail; 129 | unsigned int *mask; 130 | unsigned int limit; 131 | struct io_uring_cqe *entries; 132 | }; 133 | #endif 134 | 135 | struct io_context { 136 | io_os_handle os_handle; 137 | uint16_t max_res; 138 | uint16_t max_ops; 139 | struct io_resource *res; 140 | struct io_operation *ops; 141 | 142 | #if IO_PLATFORM_LINUX 143 | struct io_submission_queue submissions; 144 | struct io_completion_queue completions; 145 | #endif 146 | }; 147 | 148 | bool io_global_init(void); 149 | void io_global_free(void); 150 | 151 | bool io_init(struct io_context *ioc, 152 | struct io_resource *res, 153 | struct io_operation *ops, 154 | uint16_t max_res, 155 | uint16_t max_ops); 156 | 157 | void io_free(struct io_context *ioc); 158 | 159 | void io_wait(struct io_context *ioc, 160 | struct io_event *ev); 161 | 162 | bool io_recv(struct io_context *ioc, 163 | void *user, io_handle handle, 164 | void *dsc, uint32_t max); 165 | 166 | bool io_send(struct io_context *ioc, 167 | void *user, io_handle handle, 168 | void *src, uint32_t num); 169 | 170 | bool io_accept(struct io_context *ioc, 171 | void *user, io_handle handle); 172 | 173 | void io_close(struct io_context *ioc, 174 | io_handle handle); 175 | 176 | /* 177 | * Flags for io_open_file and io_create_file 178 | */ 179 | enum { 180 | IO_ACCESS_RD = 1 << 0, 181 | IO_ACCESS_WR = 1 << 1, 182 | IO_CREATE_OVERWRITE = 1 << 2, 183 | IO_CREATE_CANTEXIST = 1 << 3, 184 | }; 185 | 186 | io_handle io_open_file(struct io_context *ioc, 187 | const char *file, int flags); 188 | 189 | io_handle io_create_file(struct io_context *ioc, 190 | const char *file, int flags); 191 | 192 | io_handle io_start_server(struct io_context *ioc, 193 | const char *addr, int port); 194 | 195 | void io_set_callback(struct io_context *ioc, 196 | io_handle handle, 197 | io_callback callback); 198 | --------------------------------------------------------------------------------