Skip to content

Instantly share code, notes, and snippets.

@doodspav
Last active June 17, 2020 09:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save doodspav/5d96c09696fa3ef1c89b4d6426ddc338 to your computer and use it in GitHub Desktop.
Save doodspav/5d96c09696fa3ef1c89b4d6426ddc338 to your computer and use it in GitHub Desktop.
Transparent python wrapper around eventfd syscall
#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