Last active
June 17, 2020 09:46
-
-
Save doodspav/5d96c09696fa3ef1c89b4d6426ddc338 to your computer and use it in GitHub Desktop.
Transparent python wrapper around eventfd syscall
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define PY_SSIZE_T_CLEAN | |
#include <Python.h> | |
#include <errno.h> | |
#include <stdatomic.h> | |
#include <stdint.h> | |
#include <string.h> | |
#include <sys/eventfd.h> | |
#include <unistd.h> | |
// -1 was taken by syscall return value (fd will NEVER have this value) | |
#define PY_EVENTFD_SPECVAL_UNINIT -2 | |
#define PY_EVENTFD_SPECVAL_CLOSED -3 | |
/* | |
* EVENTFD object | |
* | |
* No need for any more state - this is the thinnest wrapper possible | |
* Needs to be atomic so it can be accessed from multiple threads if so desired | |
* (Otherwise you would need a mutex to synchronize the memory) | |
* None of the functions explicitly depend on it being atomic | |
* | |
* Some negative values are special - positive and 0 values aren't | |
* (-2) uninitialized (__init__() hasn't been run) | |
* (-3) explicitly closed (either in __del__() or close()) | |
* (-1 was taken by syscall return value - fd will NEVER have this value) | |
*/ | |
typedef struct { | |
PyObject_HEAD | |
_Atomic int fd; | |
} py_eventfd_object; | |
/* | |
* read(self) -> int | |
* -> RuntimeError() if fd < -1 (uninitialized or closed) | |
* -> OSError() if errno set by read(2) (unless set to EINTR) | |
* | |
* TODO: add a kwarg to return None rather than raise BlockingIOError ?? | |
*/ | |
static PyObject * | |
py_eventfd_read(py_eventfd_object *self, PyObject *Py_UNUSED(ign_args)) | |
{ | |
// PEP 475: retry syscall if EINTR | |
uint64_t value; | |
ssize_t ret; | |
do { | |
Py_BEGIN_ALLOW_THREADS | |
ret = read(self->fd, &value, sizeof(value)); | |
Py_END_ALLOW_THREADS | |
} while (ret == -1 && errno == EINTR && PyErr_CheckSignals() == 0); | |
// decide whether to throw RuntimeError or OSError | |
if (ret == -1) { | |
int fd = self->fd; // so that we have a fixed value to work with (not an atomic) | |
int ok_sig = PyErr_CheckSignals() == 0; // we shouldn't replace signal exception | |
if (ok_sig && fd == PY_EVENTFD_SPECVAL_UNINIT) { | |
PyErr_SetString(PyExc_RuntimeError, "__init__ hasn't been called"); | |
} | |
else if (ok_sig && fd == PY_EVENTFD_SPECVAL_CLOSED) { | |
PyErr_SetString(PyExc_RuntimeError, "fd has been closed"); | |
} | |
else { PyErr_SetFromErrno(PyExc_OSError); } | |
return NULL; | |
} | |
// success | |
return PyLong_FromUnsignedLongLong(value); | |
} | |
/* | |
* write(self, value: int) -> bool | |
* -> RuntimeError() if fd < -1 (uninitialized or closed) | |
* -> OSError() if errno set by read(2) (unless set to EINTR) | |
* | |
* TODO: add kwarg to return False rather than raise BlockingIOError ?? | |
*/ | |
static PyObject * | |
py_eventfd_write(py_eventfd_object *self, PyObject *pyobj_value) | |
{ | |
// bounds checking | |
unsigned long long ull_value = PyLong_AsUnsignedLongLong(pyobj_value); | |
#if SIZEOF_LONG_LONG > 8 // your arch is very strange if this is true (would python even compile?) | |
if (ull_value > ((uint64_t) -1)) { | |
PyErr_SetString(PyExc_OverflowError, "Python int too large to convert to C uint64_t"); | |
return NULL; | |
} | |
#endif | |
if (PyErr_Occurred() != NULL) { return NULL; } // from PyLong_AsUnsignedLong() | |
uint64_t value = ull_value; | |
// PEP 475: retry syscall if EINTR | |
ssize_t ret; | |
do { | |
Py_BEGIN_ALLOW_THREADS | |
ret = write(self->fd, &value, sizeof(value)); | |
Py_END_ALLOW_THREADS | |
} while (ret == -1 && errno == EINTR && PyErr_CheckSignals() == 0); | |
// decide whether to throw RuntimeError or OSError | |
if (ret == -1) { | |
int fd = self->fd; // so that we have a fixed value to work with (not an atomic) | |
int ok_sig = PyErr_CheckSignals() == 0; // we shouldn't replace signal exception | |
if (ok_sig && fd == PY_EVENTFD_SPECVAL_UNINIT) { | |
PyErr_SetString(PyExc_RuntimeError, "__init__ hasn't been called"); | |
} | |
else if (ok_sig && fd == PY_EVENTFD_SPECVAL_CLOSED) { | |
PyErr_SetString(PyExc_RuntimeError, "fd has been closed"); | |
} | |
else { PyErr_SetFromErrno(PyExc_OSError); } | |
return NULL; | |
} | |
// success | |
Py_RETURN_TRUE; | |
} | |
/* | |
* close(self) -> None | |
* -> RuntimeError() if fd < -1 and not closed | |
* -> OSError() if close(2) sets errno (unless set to EINTR) | |
* | |
* Closes the eventfd for the current process | |
* Same as __del__ except it will raise exceptions | |
* If already closed, will not raise an exception | |
*/ | |
static PyObject * | |
py_eventfd_close(py_eventfd_object *self, PyObject *Py_UNUSED(ign_args)) | |
{ | |
// quick check to save time | |
if (self->fd == PY_EVENTFD_SPECVAL_CLOSED) { Py_RETURN_NONE; } | |
// PEP 475: retry syscall if EINTR | |
int ret; | |
do { | |
Py_BEGIN_ALLOW_THREADS | |
ret = close(self->fd); // because apparently close(2) can block | |
Py_END_ALLOW_THREADS | |
} while (ret == -1 && errno == EINTR && PyErr_CheckSignals() == 0); | |
// decide whether to throw RuntimeError or OSError | |
if (ret == -1) { | |
int fd = self->fd; // so that we have a fixed value to work with (not an atomic) | |
int ok_sig = PyErr_CheckSignals() == 0; // we shouldn't replace signal exception | |
if (fd == PY_EVENTFD_SPECVAL_UNINIT && ok_sig) { | |
PyErr_SetString(PyExc_RuntimeError, "__init__ hasn't been called"); | |
} | |
else if (fd == PY_EVENTFD_SPECVAL_CLOSED && ok_sig) { Py_RETURN_NONE; } | |
else { PyErr_SetFromErrno(PyExc_OSError); } | |
return NULL; | |
} | |
// success | |
self->fd = PY_EVENTFD_SPECVAL_CLOSED; | |
Py_RETURN_NONE; | |
} | |
/* | |
* fileno(self) -> int | |
* | |
* Returns the current value of fd, special (SPECVAL) or not | |
* Will never throw | |
*/ | |
static PyObject * | |
py_eventfd_fileno(py_eventfd_object *self, PyObject *Py_UNUSED(ign_args)) | |
{ | |
return PyLong_FromLong(self->fd); | |
} | |
static PyMethodDef py_eventfd_methods[] = { | |
{"read", (PyCFunction) py_eventfd_read, METH_NOARGS, "docstring"}, | |
{"write", (PyCFunction) py_eventfd_write, METH_O, "docstring"}, | |
{"close", (PyCFunction) py_eventfd_close, METH_NOARGS, "docstring"}, | |
{"fileno", (PyCFunction) py_eventfd_fileno, METH_NOARGS, "docstring"}, | |
{NULL} | |
}; | |
/* | |
* getattr(self, "closed") -> bool | |
* | |
* Will return False if __init__ has somehow not been called yet | |
* This is defined through a function call so we don't need a field for it | |
* Don't want the other methods having to modify that field every time they're called | |
*/ | |
static PyObject * | |
py_eventfd_getattr_closed(py_eventfd_object *self, void *Py_UNUSED(ign_closure)) | |
{ | |
return PyBool_FromLong(self->fd == PY_EVENTFD_SPECVAL_CLOSED); | |
} | |
static PyGetSetDef py_eventfd_getsetters[] = { | |
{"closed", (getter) py_eventfd_getattr_closed, NULL, "docstring", NULL}, | |
{NULL} | |
}; | |
/* | |
* __new__(self) | |
* | |
* Explicitly set fd to PY_EVENTFD_SPECVAL_UNINIT | |
* So that multiple calls to __init__ don't create multiple fds | |
*/ | |
static PyObject * | |
py_eventfd_magic_new(PyTypeObject *type, PyObject *Py_UNUSED(ign_args), PyObject *Py_UNUSED(ign_kwds)) | |
{ | |
py_eventfd_object *self = (py_eventfd_object *) type->tp_alloc(type, 0); | |
if (self != NULL) { self->fd = PY_EVENTFD_SPECVAL_UNINIT; } | |
return (PyObject *) self; | |
} | |
/* | |
* __init__(self, value: int, flags: int) | |
* -> OverflowError() if value is out of bounds | |
* -> RuntimeError() if fd != PY_EVENTFD_SPECVAL_UNINIT | |
* -> OSError() if errno set by eventfd() | |
* | |
* Only assign fd if fd is PY_EVENTFD_SPECVAL_UNINIT (otherwise __init__ has already been run) | |
* Can't close existing fd in __init__ (shooting yourself in the foot) | |
* Replacing the fd without closing would be a resource leak | |
*/ | |
static int | |
py_eventfd_magic_init(py_eventfd_object *self, PyObject *args, PyObject *Py_UNUSED(ign_kwds)) | |
{ | |
// check we can assign fd before parsing args (saves time) | |
if (self->fd != PY_EVENTFD_SPECVAL_UNINIT) { | |
PyErr_SetString(PyExc_RuntimeError, "__init__ has already been called"); | |
return -1; | |
} | |
PyObject *pyobj_value; | |
int flags; | |
// PyArg_ParseTuple raises exception for us | |
// "O!" because "I" (unsigned int) isn't bounds checked and can overflow | |
if (!PyArg_ParseTuple(args, "O!i", &PyLong_Type, &pyobj_value, &flags)) { return -1; } | |
// do bounds checking manually | |
unsigned long ul_value = PyLong_AsUnsignedLong(pyobj_value); | |
if (ul_value > -1u) { // TODO: figure out why my <limits.h> didn't have UINT_MAX | |
PyErr_SetString(PyExc_OverflowError, "Python int too large to convert to C unsigned int"); | |
return -1; | |
} | |
else if (PyErr_Occurred() != NULL) { return -1; } // from PyLong_AsUnsignedLong() | |
unsigned int value = ul_value; | |
// PEP 475: retry syscall if EINTR | |
int fd; | |
do { | |
Py_BEGIN_ALLOW_THREADS | |
fd = eventfd(value, flags); | |
Py_END_ALLOW_THREADS | |
} while (fd == -1 && errno == EINTR && PyErr_CheckSignals() == 0); | |
if (fd == -1) { PyErr_SetFromErrno(PyExc_OSError); return -1; } | |
// atomically assign fd (can it be expected that systems will support CAS?) | |
int expected = PY_EVENTFD_SPECVAL_UNINIT; | |
if (!atomic_compare_exchange_strong((volatile int *) &self->fd, &expected, fd)) { | |
PyErr_SetString(PyExc_RuntimeError, "__init__ has already been called"); | |
// quick and dirty close before returning | |
int ret; | |
do { | |
Py_BEGIN_ALLOW_THREADS | |
ret = close(fd); | |
Py_END_ALLOW_THREADS | |
} while (ret == -1 && errno == EINTR && PyErr_CheckSignals() == 0); | |
return -1; | |
} | |
return 0; | |
} | |
/* | |
* __del__(self) | |
* | |
* Close fd regardless of fd value (we don't care if we get EBADF here) | |
* Set fd to PY_EVENTFD_SPECVAL_CLOSED | |
*/ | |
static void | |
py_eventfd_magic_del(py_eventfd_object *self) | |
{ | |
// PEP 475: retry syscall if EINTR | |
// We also can't modify current exception status (EBADF, EIO) | |
// Signal handler modifying it technically isn't us | |
int ret; | |
do { | |
Py_BEGIN_ALLOW_THREADS | |
ret = close(self->fd); | |
Py_END_ALLOW_THREADS | |
} while (ret == -1 && errno == EINTR && PyErr_CheckSignals() == 0); | |
// In case this has been called manually or something strange has happened | |
self->fd = PY_EVENTFD_SPECVAL_CLOSED; | |
} | |
static PyTypeObject py_eventfd_type = { | |
PyVarObject_HEAD_INIT(NULL, 0) | |
.tp_basicsize = sizeof(py_eventfd_object), | |
.tp_itemsize = 0, | |
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_FINALIZE, | |
.tp_name = "eventfd.eventfd", | |
.tp_doc = "docstring", | |
.tp_new = (newfunc) py_eventfd_magic_new, | |
.tp_init = (initproc) py_eventfd_magic_init, | |
.tp_finalize = (destructor) py_eventfd_magic_del, | |
.tp_methods = py_eventfd_methods, | |
.tp_getset = py_eventfd_getsetters, | |
}; | |
static PyModuleDef py_eventfd_module = { | |
PyModuleDef_HEAD_INIT, | |
.m_name = "eventfd", | |
.m_doc = "docstring", | |
.m_size = -1, // we have a global state? | |
}; | |
PyMODINIT_FUNC | |
PyInit_eventfd(void) | |
{ | |
PyObject* m; | |
if (PyType_Ready(&py_eventfd_type) < 0) { return NULL; } | |
m = PyModule_Create(&py_eventfd_module); | |
if (m == NULL) { return NULL; } | |
Py_INCREF(&py_eventfd_type); | |
if (PyModule_AddObject(m, "eventfd", (PyObject *) &py_eventfd_type) < 0) { | |
Py_DECREF(&py_eventfd_type); | |
Py_DECREF(&m); | |
return NULL; | |
} | |
// TODO: add error checking for return values here | |
(void) PyModule_AddIntMacro(m, EFD_CLOEXEC); | |
(void) PyModule_AddIntMacro(m, EFD_NONBLOCK); | |
(void) PyModule_AddIntMacro(m, EFD_SEMAPHORE); | |
return m; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment