But why is that such a bad thing? The callbacks will need to share a lot of information anyway, because a callback isn’t meant to be reusable across any arbitrary PyAsync object.
I’m really hesitant to do per-callback closures because it would make the proposed API much more verbose. For what it’s worth, a C++ wrapper should still be fairly easy to make when using a single data pointer. Something like this should work:
// No error handling
using Callback = std::function<void(PyObject *asyncobj, PyObject *result)>;
using Queue = std::deque<Callback>;
static int trampoline(PyObject *asyncobj, PyObject *result) {
auto *queue = static_cast<Queue *>(PyAsync_GetArbValue(asyncobj));
Callback callback = std::move(queue->front());
queue->pop_front();
callback(asyncobj, result);
return 1;
}
static void await(PyObject *asyncobj, PyObject *awaitable, Callback callback) {
auto *queue = static_cast<Queue *>(PyAsync_GetArbValue(asyncobj));
if (queue == nullptr) {
queue = new Queue; // never freed, for brevity
PyAsync_SetArbValue(asyncobj, queue);
}
queue->push_back(std::move(callback));
PyAsync_AddAwait(asyncobj, awaitable, trampoline, nullptr);
}
/* async def both(a, b): print(await a, "first"); print(await b, "second") */
static PyObject *
both(PyObject *self, PyObject *args)
{
PyObject *a, *b;
if (!PyArg_ParseTuple(args, "OO", &a, &b)) {
return nullptr;
}
PyObject *asyncobj = PyAsync_New();
await(asyncobj, a, [](PyObject *, PyObject *result) {
PySys_FormatStdout("%R first\n", result);
});
await(asyncobj, b, [](PyObject *, PyObject *result) {
PySys_FormatStdout("%R second\n", result);
});
return asyncobj;
}
The only thing a per-callback data pointer would do here is avoid the need for an extra queue, so it wouldn’t save all that much. (You could argue that the data pointer approach requires more memory, but keep in mind that adding per-callback data pointers will increase the size of every callback entry by 2 * sizeof(void *), so I’m not sure it would actually be better in practice.)