first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
example
|
||||
example.exe
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
This repository implements 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.
|
||||
|
||||
It's likely that the io_uring code has some bugs, so be wary! Also bug reports are appreciated :)
|
||||
|
||||
To use it, you need to add io_linux.c and io_win.c in your source tree and compile them as they were your files. Each .c file is ignored when not compiling for its platform, so you can include them both in all builds.
|
||||
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
@@ -0,0 +1,25 @@
|
||||
#include <stdio.h>
|
||||
#include "io.h"
|
||||
|
||||
int main()
|
||||
{
|
||||
struct io_operation ops[100];
|
||||
struct io_context ioc;
|
||||
if (!io_context_init(&ioc, ops, sizeof(ops)/sizeof(ops[0])))
|
||||
return -1;
|
||||
|
||||
char msg[] = "Hello, world!\n";
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (!io_start_send(&ioc, io_get_stdout(), msg, sizeof(msg), NULL))
|
||||
fprintf(stderr, "ERROR\n");
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
struct io_event ev;
|
||||
io_wait(&ioc, &ev);
|
||||
fprintf(stderr, "CONCLUDED\n");
|
||||
}
|
||||
|
||||
io_context_free(&ioc);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
|
||||
#define IO_VERSION_MAJOR 0
|
||||
#define IO_VERSION_MINOR 0
|
||||
|
||||
# ifdef _WIN32
|
||||
# define IO_PLATFORM_WINDOWS 1
|
||||
# define IO_PLATFORM_LINUX 0
|
||||
# define IO_PLATFORM_OTHER 0
|
||||
# elif __linux__
|
||||
# define IO_PLATFORM_WINDOWS 0
|
||||
# define IO_PLATFORM_LINUX 1
|
||||
# define IO_PLATFORM_OTHER 0
|
||||
# else
|
||||
# define IO_PLATFORM_WINDOWS 0
|
||||
# define IO_PLATFORM_LINUX 0
|
||||
# define IO_PLATFORM_OTHER 0
|
||||
# endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#if IO_PLATFORM_LINUX
|
||||
#include <stdatomic.h>
|
||||
#include <linux/io_uring.h>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* The OS handle type.
|
||||
*/
|
||||
#if IO_PLATFORM_LINUX
|
||||
typedef int io_handle;
|
||||
#define IO_INVALID_HANDLE -1
|
||||
#elif IO_PLATFORM_WINDOWS
|
||||
typedef void *io_handle;
|
||||
#define IO_INVALID_HANDLE ((void*) -1)
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Windows calls this structure OVERLAPPED
|
||||
*/
|
||||
#if IO_PLATFORM_WINDOWS
|
||||
struct io_overlap {
|
||||
unsigned long *internal;
|
||||
unsigned long *internal_high;
|
||||
union {
|
||||
struct {
|
||||
unsigned long offset;
|
||||
unsigned long offset_high;
|
||||
};
|
||||
void *pointer;
|
||||
};
|
||||
void *event;
|
||||
};
|
||||
#endif
|
||||
|
||||
enum io_optype {
|
||||
IO_VOID,
|
||||
IO_RECV,
|
||||
IO_SEND,
|
||||
IO_ACCEPT,
|
||||
};
|
||||
|
||||
struct io_operation {
|
||||
enum io_optype type; // =IO_VOID when the struct is unused
|
||||
void *user;
|
||||
|
||||
#if IO_PLATFORM_WINDOWS
|
||||
struct io_overlap ov;
|
||||
#endif
|
||||
};
|
||||
|
||||
/*
|
||||
* io_uring's input queue
|
||||
*/
|
||||
#if IO_PLATFORM_LINUX
|
||||
struct io_submission_queue {
|
||||
_Atomic unsigned int *head;
|
||||
_Atomic unsigned int *tail;
|
||||
unsigned int *mask;
|
||||
unsigned int *array;
|
||||
unsigned int limit;
|
||||
struct io_uring_sqe *entries;
|
||||
};
|
||||
#endif
|
||||
|
||||
/*
|
||||
* io_uring's output queue
|
||||
*/
|
||||
#if IO_PLATFORM_LINUX
|
||||
struct io_completion_queue {
|
||||
_Atomic unsigned int *head;
|
||||
_Atomic unsigned int *tail;
|
||||
unsigned int *mask;
|
||||
unsigned int limit;
|
||||
struct io_uring_cqe *entries;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct io_context {
|
||||
|
||||
io_handle handle;
|
||||
uint32_t max_ops;
|
||||
struct io_operation *ops;
|
||||
|
||||
#if IO_PLATFORM_LINUX
|
||||
struct io_submission_queue submissions;
|
||||
struct io_completion_queue completions;
|
||||
#endif
|
||||
};
|
||||
|
||||
struct io_event {
|
||||
bool error;
|
||||
void *user;
|
||||
enum io_optype type;
|
||||
|
||||
/*
|
||||
* Operation-specific results
|
||||
*/
|
||||
union {
|
||||
uint32_t num; // recv, send
|
||||
io_handle handle; // accept
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Initialize an I/O context
|
||||
*/
|
||||
bool io_context_init(struct io_context *ioc,
|
||||
struct io_operation *ops,
|
||||
uint32_t max_ops);
|
||||
|
||||
/*
|
||||
* Deinitialize an I/O context. This will not close any previously
|
||||
* created handles.
|
||||
*/
|
||||
void io_context_free(struct io_context *ioc);
|
||||
|
||||
/*
|
||||
* Start an asynchronous receive operation on the handle.
|
||||
* Only one pending receive operation per handle is supported.
|
||||
*
|
||||
* When the operation completes, one of the following calls to
|
||||
* "io_wait" will return a completion event associated to this
|
||||
* recv. The "num" field of the event will hold the number of
|
||||
* bytes actually written from "dst".
|
||||
*/
|
||||
bool io_start_recv(struct io_context *ioc, io_handle handle,
|
||||
void *dst, uint32_t max, void *user);
|
||||
|
||||
/*
|
||||
* Works like "io_start_recv" but for sending.
|
||||
*/
|
||||
bool io_start_send(struct io_context *ioc, io_handle handle,
|
||||
void *src, uint32_t num, void *user);
|
||||
|
||||
|
||||
bool io_start_accept(struct io_context *ioc, io_handle handle,
|
||||
void *user);
|
||||
|
||||
/*
|
||||
* Wait for the completion of an I/O event.
|
||||
*/
|
||||
void io_wait(struct io_context *ioc,
|
||||
struct io_event *ev);
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
#include "io.h"
|
||||
|
||||
#if IO_PLATFORM_LINUX
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
static int
|
||||
io_uring_setup(unsigned entries,
|
||||
struct io_uring_params *p)
|
||||
{
|
||||
return (int) syscall(SYS_io_uring_setup, entries, p);
|
||||
}
|
||||
|
||||
static int
|
||||
io_uring_enter(int ring_fd, unsigned int to_submit,
|
||||
unsigned int min_complete, unsigned int flags)
|
||||
{
|
||||
return (int) syscall(SYS_io_uring_enter, ring_fd, to_submit,
|
||||
min_complete, flags, NULL, 0);
|
||||
}
|
||||
|
||||
bool io_context_init(struct io_context *ioc,
|
||||
struct io_operation *ops,
|
||||
uint32_t max_ops)
|
||||
{
|
||||
ioc->ops = ops;
|
||||
ioc->max_ops = max_ops;
|
||||
|
||||
for (uint32_t i = 0; i < max_ops; i++)
|
||||
ioc->ops[i].type = IO_VOID;
|
||||
|
||||
struct io_uring_params p;
|
||||
void *sq_ptr, *cq_ptr;
|
||||
/* See io_uring_setup(2) for io_uring_params.flags you can set */
|
||||
memset(&p, 0, sizeof(p));
|
||||
int fd = io_uring_setup(32, &p);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
|
||||
ioc->handle = fd;
|
||||
|
||||
/*
|
||||
* io_uring communication happens via 2 shared kernel-user space ring
|
||||
* buffers, which can be jointly mapped with a single mmap() call in
|
||||
* kernels >= 5.4.
|
||||
*/
|
||||
int sring_sz = p.sq_off.array + p.sq_entries * sizeof(unsigned);
|
||||
int cring_sz = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe);
|
||||
/* Rather than check for kernel version, the recommended way is to
|
||||
* check the features field of the io_uring_params structure, which is a
|
||||
* bitmask. If IORING_FEAT_SINGLE_MMAP is set, we can do away with the
|
||||
* second mmap() call to map in the completion ring separately.
|
||||
*/
|
||||
if (p.features & IORING_FEAT_SINGLE_MMAP) {
|
||||
if (cring_sz > sring_sz)
|
||||
sring_sz = cring_sz;
|
||||
cring_sz = sring_sz;
|
||||
}
|
||||
/* Map in the submission and completion queue ring buffers.
|
||||
* Kernels < 5.4 only map in the submission queue, though.
|
||||
*/
|
||||
sq_ptr = mmap(0, sring_sz, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING);
|
||||
if (sq_ptr == MAP_FAILED) {
|
||||
// TODO: Cleanup
|
||||
return false;
|
||||
}
|
||||
if (p.features & IORING_FEAT_SINGLE_MMAP) {
|
||||
cq_ptr = sq_ptr;
|
||||
} else {
|
||||
/* Map in the completion queue ring buffer in older kernels separately */
|
||||
cq_ptr = mmap(0, cring_sz, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_CQ_RING);
|
||||
if (cq_ptr == MAP_FAILED) {
|
||||
// TODO: Cleanup
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Save useful fields for later easy reference */
|
||||
ioc->submissions.head = (_Atomic unsigned*) (sq_ptr + p.sq_off.head);
|
||||
ioc->submissions.tail = (_Atomic unsigned*) (sq_ptr + p.sq_off.tail);
|
||||
ioc->submissions.mask = (unsigned*) (sq_ptr + p.sq_off.ring_mask);
|
||||
ioc->submissions.array = sq_ptr + p.sq_off.array;
|
||||
ioc->submissions.limit = p.sq_entries;
|
||||
|
||||
/* Map in the submission queue entries array */
|
||||
ioc->submissions.entries = mmap(0, p.sq_entries * sizeof(struct io_uring_sqe),
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
|
||||
fd, IORING_OFF_SQES);
|
||||
if (ioc->submissions.entries == MAP_FAILED) {
|
||||
// TODO: Cleanup
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Save useful fields for later easy reference */
|
||||
ioc->completions.head = cq_ptr + p.cq_off.head;
|
||||
ioc->completions.tail = cq_ptr + p.cq_off.tail;
|
||||
ioc->completions.mask = cq_ptr + p.cq_off.ring_mask;
|
||||
ioc->completions.entries = cq_ptr + p.cq_off.cqes;
|
||||
ioc->completions.limit = p.cq_entries;
|
||||
return true;
|
||||
}
|
||||
|
||||
void io_context_free(struct io_context *ioc)
|
||||
{
|
||||
close(ioc->handle);
|
||||
}
|
||||
|
||||
static bool start_oper(struct io_context *ioc,
|
||||
struct io_uring_sqe sqe)
|
||||
{
|
||||
unsigned int mask = *ioc->submissions.mask;
|
||||
unsigned int tail = atomic_load(ioc->submissions.tail);
|
||||
unsigned int head = atomic_load(ioc->submissions.head);
|
||||
|
||||
if (tail >= head + ioc->submissions.limit)
|
||||
return false;
|
||||
|
||||
unsigned int index = tail & mask;
|
||||
ioc->submissions.entries[index] = sqe;
|
||||
ioc->submissions.array[index] = index;
|
||||
|
||||
atomic_store(ioc->submissions.tail, tail+1);
|
||||
|
||||
int ret = io_uring_enter(ioc->handle, 1, 0, 0);
|
||||
if (ret < 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct io_operation *alloc_op(struct io_context *ioc)
|
||||
{
|
||||
for (uint32_t i = 0; i < ioc->max_ops; i++)
|
||||
if (ioc->ops[i].type == IO_VOID)
|
||||
return &ioc->ops[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool io_start_recv(struct io_context *ioc, io_handle handle,
|
||||
void *dst, uint32_t max, void *user)
|
||||
{
|
||||
struct io_operation *op;
|
||||
struct io_uring_sqe sqe;
|
||||
|
||||
op = alloc_op(ioc);
|
||||
if (op == NULL)
|
||||
return false;
|
||||
|
||||
memset(&sqe, 0, sizeof(sqe));
|
||||
sqe.opcode = IORING_OP_READ;
|
||||
sqe.fd = (int) handle;
|
||||
sqe.addr = (uint64_t) dst;
|
||||
sqe.len = max;
|
||||
sqe.user_data = (uint64_t) op;
|
||||
|
||||
if (!start_oper(ioc, sqe))
|
||||
return false;
|
||||
|
||||
op->user = user;
|
||||
op->type = IO_RECV; // Commit operation structure
|
||||
return true;
|
||||
}
|
||||
|
||||
bool io_start_send(struct io_context *ioc, io_handle handle,
|
||||
void *src, uint32_t num, void *user)
|
||||
{
|
||||
struct io_operation *op;
|
||||
struct io_uring_sqe sqe;
|
||||
|
||||
op = alloc_op(ioc);
|
||||
if (op == NULL)
|
||||
return false;
|
||||
|
||||
memset(&sqe, 0, sizeof(sqe));
|
||||
sqe.opcode = IORING_OP_WRITE;
|
||||
sqe.fd = (int) handle;
|
||||
sqe.addr = (uint64_t) src;
|
||||
sqe.len = num;
|
||||
sqe.user_data = (uint64_t) op;
|
||||
|
||||
if (!start_oper(ioc, sqe))
|
||||
return false;
|
||||
|
||||
op->user = user;
|
||||
op->type = IO_SEND; // Commit operation structure
|
||||
return true;
|
||||
}
|
||||
|
||||
bool io_start_accept(struct io_context *ioc, io_handle handle, void *user)
|
||||
{
|
||||
struct io_operation *op;
|
||||
struct io_uring_sqe sqe;
|
||||
|
||||
op = alloc_op(ioc);
|
||||
if (op == NULL)
|
||||
return false;
|
||||
|
||||
memset(&sqe, 0, sizeof(sqe));
|
||||
sqe.opcode = IORING_OP_ACCEPT;
|
||||
sqe.fd = (int) handle;
|
||||
sqe.user_data = (uint64_t) op;
|
||||
|
||||
if (!start_oper(ioc, sqe))
|
||||
return false;
|
||||
|
||||
op->user = user;
|
||||
op->type = IO_ACCEPT; // Commit operation structure
|
||||
return true;
|
||||
}
|
||||
|
||||
void io_wait(struct io_context *ioc, struct io_event *ev)
|
||||
{
|
||||
/* --- Read barrier --- */
|
||||
unsigned int head = atomic_load(ioc->completions.head);
|
||||
unsigned int tail = atomic_load(ioc->completions.tail);
|
||||
|
||||
if (head == tail) {
|
||||
|
||||
/*
|
||||
* Completion queue is empty. Wait for some operations to complete.
|
||||
*/
|
||||
int ret = io_uring_enter(ioc->handle, 0, 1, IORING_ENTER_GETEVENTS);
|
||||
if (ret < 0) {
|
||||
ev->error = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
struct io_uring_cqe *cqe;
|
||||
struct io_operation *op;
|
||||
|
||||
cqe = &ioc->completions.entries[head & (*ioc->completions.mask)];
|
||||
|
||||
op = (void*) cqe->user_data;
|
||||
ev->user = op->user;
|
||||
ev->type = op->type;
|
||||
ev->error = cqe->res < 0;
|
||||
|
||||
if (ev->error == false) {
|
||||
switch (op->type) {
|
||||
case IO_VOID: /* UNREACHABLE */ break;
|
||||
case IO_RECV: ev->num = cqe->res; break;
|
||||
case IO_SEND: ev->num = cqe->res; break;
|
||||
case IO_ACCEPT: ev->handle = cqe->res; break;
|
||||
}
|
||||
}
|
||||
|
||||
op->type = IO_VOID; // Mark unused
|
||||
|
||||
/* --- write barrier --- */
|
||||
atomic_store(ioc->completions.head, head+1);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "io.h"
|
||||
|
||||
#if IO_PLATFORM_WINDOWS
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* Declare Windows symbols locally to avoid including windows.h
|
||||
*/
|
||||
|
||||
#define ERROR_IO_PENDING 997l
|
||||
|
||||
extern int ReadFile(void *handle, void *dst, unsigned long max, unsigned long *num, struct io_overlap *ov);
|
||||
extern int WriteFile(void *handle, const void *src, unsigned long max, unsigned long *num, struct io_overlap *ov);
|
||||
extern void *CreateIoCompletionPort(void *handle, void *existing_ioport, unsigned long *ptr, unsigned long num_threads);
|
||||
extern int CloseHandle(void *handle);
|
||||
extern unsigned long GetLastError();
|
||||
extern int GetQueuedCompletionStatus(void *ioport, unsigned long *num, unsigned long *key, struct io_overlap **ov, unsigned long timeout);
|
||||
|
||||
bool io_context_init(struct io_context *ioc,
|
||||
struct io_operation *ops,
|
||||
uint32_t max_ops)
|
||||
{
|
||||
io_handle handle = CreateIoCompletionPort(IO_INVALID_HANDLE, NULL, 0, 1);
|
||||
if (handle == IO_INVALID_HANDLE)
|
||||
return false;
|
||||
|
||||
ioc->handle = handle;
|
||||
ioc->max_ops = max_ops;
|
||||
ioc->ops = ops;
|
||||
return true;
|
||||
}
|
||||
|
||||
void io_context_free(struct io_context *ioc)
|
||||
{
|
||||
CloseHandle(ioc->handle);
|
||||
}
|
||||
|
||||
static struct io_operation *alloc_op(struct io_context *ioc)
|
||||
{
|
||||
for (uint32_t i = 0; i < ioc->max_ops; i++)
|
||||
if (ioc->ops[i].type == IO_VOID)
|
||||
return &ioc->ops[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool io_start_recv(struct io_context *ioc, io_handle handle,
|
||||
void *dst, uint32_t max, void *user)
|
||||
{
|
||||
struct io_operation *op = alloc_op(ioc);
|
||||
if (op == NULL)
|
||||
return false;
|
||||
|
||||
memset(&op->ov, 0, sizeof(struct io_overlap));
|
||||
|
||||
int ok = ReadFile(handle, dst, max, NULL, &op->ov);
|
||||
if (!ok && GetLastError() != ERROR_IO_PENDING)
|
||||
return false;
|
||||
|
||||
op->user = user;
|
||||
op->type = IO_RECV;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool io_start_send(struct io_context *ioc, io_handle handle,
|
||||
void *src, uint32_t num, void *user)
|
||||
{
|
||||
struct io_operation *op = alloc_op(ioc);
|
||||
if (op == NULL)
|
||||
return false;
|
||||
|
||||
memset(&op->ov, 0, sizeof(struct io_overlap));
|
||||
|
||||
int ok = WriteFile(handle, src, num, NULL, &op->ov);
|
||||
if (!ok && GetLastError() != ERROR_IO_PENDING)
|
||||
return false;
|
||||
|
||||
op->user = user;
|
||||
op->type = IO_SEND;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool io_start_accept(struct io_context *ioc, io_handle handle, void *user)
|
||||
{
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
void io_wait(struct io_context *ioc, struct io_event *ev)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user