Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -16,3 +16,4 @@ wheels/
_liburing.c
_liburing.o
_liburing.*.so
_uringcore.*.so
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
include _ffi_build.py
include src/uringcore.c
recursive-include docs *.md
recursive-include uringloop *.py *.pyi
include uringloop/py.typed
exclude uringloop/_liburing.c
Expand Down
22 changes: 22 additions & 0 deletions docs/phase1-raw-syscall-spike.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Phase 1 raw-syscall ring spike

This is the first of the two native ring implementation spikes required by
Phase 1 of the roadmap. It establishes an importable `_uringcore` extension
whose `Ring` type owns:

- the file descriptor returned by `io_uring_setup`;
- the submission and completion queue mappings; and
- the submission queue entry mapping.

`Ring.close()` releases those resources deterministically and is idempotent.
Deallocation provides the same cleanup as a fallback.

The spike intentionally does not submit or reap operations and is not wired
into the Python proactor. The CFFI implementation remains the behavioral
oracle while the native API is developed.

The extension calls the kernel ABI directly and does not link to liburing.
The follow-up static-liburing spike should implement the same lifecycle
boundary. A decision record can then compare measured wheel size, build and
sanitizer complexity, syscall overhead, license obligations, and unsafe
surface before either route becomes the production backend.
12 changes: 10 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
from setuptools import setup
from setuptools import Extension, setup


setup(cffi_modules=["_ffi_build.py:ffibuilder"])
setup(
cffi_modules=["_ffi_build.py:ffibuilder"],
ext_modules=[
Extension(
"uringloop._uringcore",
sources=["src/uringcore.c"],
)
],
)
344 changes: 344 additions & 0 deletions src/uringcore.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <structmember.h>

#include <limits.h>
#include <linux/io_uring.h>
#include <stddef.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>

typedef struct {
PyObject_HEAD
int ring_fd;
void *sq_ring;
size_t sq_ring_size;
void *cq_ring;
size_t cq_ring_size;
void *sqes;
size_t sqes_size;
unsigned int sq_entries;
unsigned int cq_entries;
unsigned int features;
} UringCoreRing;

static void
uringcore_ring_close_resources(UringCoreRing *self)
{
if (self->sqes != NULL) {
munmap(self->sqes, self->sqes_size);
self->sqes = NULL;
self->sqes_size = 0;
}

if (self->cq_ring != NULL) {
munmap(self->cq_ring, self->cq_ring_size);
self->cq_ring = NULL;
self->cq_ring_size = 0;
}

if (self->sq_ring != NULL) {
munmap(self->sq_ring, self->sq_ring_size);
self->sq_ring = NULL;
self->sq_ring_size = 0;
}

if (self->ring_fd >= 0) {
close(self->ring_fd);
self->ring_fd = -1;
}
}

static PyObject *
uringcore_ring_new(
PyTypeObject *type,
PyObject *Py_UNUSED(args),
PyObject *Py_UNUSED(kwargs))
{
UringCoreRing *self = (UringCoreRing *)type->tp_alloc(type, 0);

if (self != NULL) {
self->ring_fd = -1;
}
return (PyObject *)self;
}

static int
uringcore_ring_init(UringCoreRing *self, PyObject *args, PyObject *kwargs)
{
static char *keyword_names[] = {"entries", NULL};
PyObject *entries_object = NULL;
PyObject *entries_index = NULL;
unsigned long parsed_entries;
unsigned int entries = 256;
struct io_uring_params params;
size_t sq_ring_size;
size_t cq_ring_size;

if (!PyArg_ParseTupleAndKeywords(
args, kwargs, "|O:Ring", keyword_names, &entries_object)) {
return -1;
}
if (entries_object != NULL) {
entries_index = PyNumber_Index(entries_object);
if (entries_index == NULL) {
return -1;
}
parsed_entries = PyLong_AsUnsignedLong(entries_index);
Py_DECREF(entries_index);
if (parsed_entries == (unsigned long)-1 && PyErr_Occurred()) {
PyErr_Clear();
PyErr_Format(
PyExc_ValueError,
"entries must be between 1 and %u",
UINT_MAX);
return -1;
}
if (parsed_entries == 0 || parsed_entries > UINT_MAX) {
PyErr_Format(
PyExc_ValueError,
"entries must be between 1 and %u",
UINT_MAX);
return -1;
}
entries = (unsigned int)parsed_entries;
}

uringcore_ring_close_resources(self);
memset(&params, 0, sizeof(params));

self->ring_fd = (int)syscall(__NR_io_uring_setup, entries, &params);
if (self->ring_fd < 0) {
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}

sq_ring_size =
params.sq_off.array + params.sq_entries * sizeof(unsigned int);
cq_ring_size =
params.cq_off.cqes + params.cq_entries * sizeof(struct io_uring_cqe);

if ((params.features & IORING_FEAT_SINGLE_MMAP) != 0) {
self->sq_ring_size =
sq_ring_size > cq_ring_size ? sq_ring_size : cq_ring_size;
self->sq_ring = mmap(
NULL,
self->sq_ring_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
self->ring_fd,
IORING_OFF_SQ_RING);
if (self->sq_ring == MAP_FAILED) {
self->sq_ring = NULL;
PyErr_SetFromErrno(PyExc_OSError);
goto error;
}
} else {
self->sq_ring_size = sq_ring_size;
self->sq_ring = mmap(
NULL,
self->sq_ring_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
self->ring_fd,
IORING_OFF_SQ_RING);
if (self->sq_ring == MAP_FAILED) {
self->sq_ring = NULL;
PyErr_SetFromErrno(PyExc_OSError);
goto error;
}

self->cq_ring_size = cq_ring_size;
self->cq_ring = mmap(
NULL,
self->cq_ring_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
self->ring_fd,
IORING_OFF_CQ_RING);
if (self->cq_ring == MAP_FAILED) {
self->cq_ring = NULL;
PyErr_SetFromErrno(PyExc_OSError);
goto error;
}
}

self->sqes_size =
params.sq_entries * sizeof(struct io_uring_sqe);
self->sqes = mmap(
NULL,
self->sqes_size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE,
self->ring_fd,
IORING_OFF_SQES);
if (self->sqes == MAP_FAILED) {
self->sqes = NULL;
PyErr_SetFromErrno(PyExc_OSError);
goto error;
}

self->sq_entries = params.sq_entries;
self->cq_entries = params.cq_entries;
self->features = params.features;
return 0;

error:
uringcore_ring_close_resources(self);
return -1;
}

static void
uringcore_ring_dealloc(UringCoreRing *self)
{
uringcore_ring_close_resources(self);
Py_TYPE(self)->tp_free((PyObject *)self);
}

static PyObject *
uringcore_ring_close(UringCoreRing *self, PyObject *Py_UNUSED(ignored))
{
uringcore_ring_close_resources(self);
Py_RETURN_NONE;
}

static PyObject *
uringcore_ring_enter(UringCoreRing *self, PyObject *Py_UNUSED(ignored))
{
if (self->ring_fd < 0) {
PyErr_SetString(PyExc_RuntimeError, "ring is closed");
return NULL;
}
return Py_NewRef(self);
}

static PyObject *
uringcore_ring_exit(
UringCoreRing *self,
PyObject *Py_UNUSED(args))
{
uringcore_ring_close_resources(self);
Py_RETURN_FALSE;
}

static PyObject *
uringcore_ring_get_closed(
UringCoreRing *self,
void *Py_UNUSED(context))
{
return PyBool_FromLong(self->ring_fd < 0);
}

static PyMethodDef uringcore_ring_methods[] = {
{
"close",
(PyCFunction)uringcore_ring_close,
METH_NOARGS,
PyDoc_STR("Release the ring mappings and file descriptor."),
},
{
"__enter__",
(PyCFunction)uringcore_ring_enter,
METH_NOARGS,
NULL,
},
{
"__exit__",
(PyCFunction)uringcore_ring_exit,
METH_VARARGS,
NULL,
},
{NULL, NULL, 0, NULL},
};

static PyMemberDef uringcore_ring_members[] = {
{
"sq_entries",
T_UINT,
offsetof(UringCoreRing, sq_entries),
READONLY,
PyDoc_STR("Number of submission queue entries allocated by the kernel."),
},
{
"cq_entries",
T_UINT,
offsetof(UringCoreRing, cq_entries),
READONLY,
PyDoc_STR("Number of completion queue entries allocated by the kernel."),
},
{
"features",
T_UINT,
offsetof(UringCoreRing, features),
READONLY,
PyDoc_STR("Feature flags returned by io_uring_setup."),
},
{NULL},
};

static PyGetSetDef uringcore_ring_getset[] = {
{
"closed",
(getter)uringcore_ring_get_closed,
NULL,
PyDoc_STR("Whether the native ring resources have been released."),
NULL,
},
{NULL},
};

PyDoc_STRVAR(
uringcore_ring_doc,
"Ring(entries=256)\n"
"--\n"
"\n"
"Own an io_uring file descriptor and its SQ, CQ, and SQE mappings.");

static PyTypeObject UringCoreRingType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "uringloop._uringcore.Ring",
.tp_basicsize = sizeof(UringCoreRing),
.tp_dealloc = (destructor)uringcore_ring_dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = uringcore_ring_doc,
.tp_methods = uringcore_ring_methods,
.tp_members = uringcore_ring_members,
.tp_getset = uringcore_ring_getset,
.tp_init = (initproc)uringcore_ring_init,
.tp_new = uringcore_ring_new,
};

static PyModuleDef uringcore_module = {
PyModuleDef_HEAD_INIT,
.m_name = "_uringcore",
.m_doc = "Native io_uring ring primitives.",
.m_size = -1,
};

PyMODINIT_FUNC
PyInit__uringcore(void)
{
PyObject *module;

if (PyType_Ready(&UringCoreRingType) < 0) {
return NULL;
}

module = PyModule_Create(&uringcore_module);
if (module == NULL) {
return NULL;
}

if (PyModule_AddObjectRef(
module, "Ring", (PyObject *)&UringCoreRingType) < 0) {
Py_DECREF(module);
return NULL;
}
if (PyModule_AddIntConstant(module, "ABI_VERSION", 1) < 0) {
Py_DECREF(module);
return NULL;
}
return module;
}
Loading
Loading