view.py icon indicating copy to clipboard operation
view.py copied to clipboard

ASGI server

Open ZeroIntensity opened this issue 2 years ago • 1 comments
trafficstars

Oh boy, this is gonna be a big one.

We need a fast, ASGI server that is written entirely in C.

ZeroIntensity avatar Aug 17 '23 14:08 ZeroIntensity

For anyone insane enough to try this before I do, here's some information on the C API:

Async Python stuff is handled entirely by the PyAwaitable API. For more information on what that is, take a look at this discussion.

The "awaitable" in the PyAwaitable API (created by PyAwaitable_New) is a sort of proxy object that holds all coroutines submitted to it and holds a proper generator for it. Each coroutine holds a callback (which may be NULL) to be called when the value is received. Then, PyAwaitable_AddAwait takes four parameters:

  • The PyAwaitable object
  • The coroutine (NOT awaitable function, but instead it's return value)
  • Callback to send return value of coroutine to (may be NULL)
  • Callback in case an error occurs while executing the coroutine (may be NULL)

Some other notable PyAwaitable methods:

  • PyAwaitable_SetResult, set's the return value for the awaitable (think of it like a return in an async function in Python)
  • PyAwaitable_SaveValues, saves a variable number of PyObject* values to be unpacked later. This allows use for in callback functions. This does increment and decrement the reference counts properly.
  • PyAwaitable_SaveArbValues, saves a variable number of void* values to be used in callbacks. Does not handle reference counting as it can take any pointer and not just PyObject*
  • PyAwaitable_UnpackValues, unpacks values saved by PyAwaitable_SaveValues. Sets to borrowed references.
  • PyAwaitable_UnpackArbValues, unpacks values saved by PyAwaitabled_SaveArbValues.
  • (macro) PyAwaitable_AWAIT(awaitable, coro), equivalent to PyAwaitable_AddAwait(awaitable, coro, NULL, NULL)

An example of PyAwaitable:

static int my_callback(PyObject* awaitable, PyObject* result) {
    PyObject* some_value;
    if (PyAwaitable_UnpackValues(awaitable, &some_value) < 0) { // unpack the some_value reference
        return -1;
    }

    // some_value is now a borrowed reference to the some_value object saved earlier
    PyObject_Print(result, stdout, Py_PRINT_RAW);
    puts("");

    return 0;
}

// pretend this is mapped to a my_func in Python
static PyObject* my_func(PyObject* self, PyObject* args) {
    PyObject* awaitable_func;
    PyObject* some_value;
    if (!PyArg_ParseTuple(args, "OO", &awaitable_func, &some_value)
        return NULL;
    // assume that awaitable_func is indeed an awaitable function
    
    PyObject* awaitable = PyAwaitable_New();
    if (!awaitable) return NULL;

    if (PyAwaitable_SaveValues(awaitable, 1, some_value) < 0) { // save a reference to some_value in awaitable
        Py_DECREF(awaitable):
        return NULL;
    }

    PyObject* coro = PyObject_CallNoArgs(awaitable_func);
    if (!coro) {
        Py_DECREF(awaitable);
        return NULL;
    }

    if (PyAwaitable_AddAwait(awaitable, coro, my_callback, NULL) < 0) { // submit coro to the awaitable
        // error occurred, decrement our references
        Py_DECREF(coro);
        Py_DECREF(awaitable);
        return NULL;
    }

    return awaitable; // the awaitable must get returned to make the function usable via `await`
}

ZeroIntensity avatar Nov 13 '23 13:11 ZeroIntensity