Skip to content

[C API] Add PyObject_VectorcallDict() to the limited C API #120881

Open
@vstinner

Description

@vstinner

From: #85283 (comment)

@AraHaan: Why do you need PyObject_VectorcallDict()? Can you elaborate?

In the limited ABI their lacks a version of vector call that accepts args as a tuple, and kwargs as a dict instead of the confusing to use kwnames versions that I see no example usages of at all on the internet it seems.

I got my code to work well with PyVectorcall_Call however, that is not in the limited ABI as far as I know in 3.12 at least.
Edit: **It is in the stable ABI and from looks of it now also in the limited ABI for 3.12 if one sets Py_LIMITED_API to 0x030C00F0 (There really needs to be a document that documents all of the stable released version values of python to be made for the limited ABI tho).

Also PyObject_VectorcallDict would be the only function that could accept a possibly null args, and kwargs for cases of my own wrapper calling functions in my own c extension:

PyObject *_PyObject_GetCallableMethod(PyObject *obj, PyObject *attr_name) {
  PyObject *method = PyObject_GetAttr(obj, attr_name);
  if (!PyCallable_Check(method)) {
    Py_XDECREF(method);
    return NULL;
  }

  return method;
}

PyObject *_PyObject_GetCallableMethodString(PyObject *obj, const char *name) {
  PyObject *attr_name = PyUnicode_FromString(name);
  PyObject *method = _PyObject_GetCallableMethod(obj, attr_name);
  Py_XDECREF(attr_name);
  return method;
}

int _PyObject_CallAndAwaitCoroutineAndKeywords(PyObject *awaitable, PyObject *obj, const char *name, PyObject *args, PyObject *kwargs, awaitcallback cb) {
  PyObject *method = _PyObject_GetCallableMethodString(obj, name);
  if (method == NULL || Py_IsNone(method)) {
    PyErr_SetString(PyExc_RuntimeError, "'method' is NULL or 'None'.\n");
    return -1;
  }

  // prevent problems where "PyObject_Call" would fail because args is NULL.
  PyObject *args_copy = args;
  if (args_copy == NULL) {
    args_copy = PyTuple_New(0);
  }

  if (!PyTuple_Check(args_copy)) {
    PyErr_SetString(PyExc_TypeError, "Provided args is not a tuple.\n");
    Py_DECREF(args);
    Py_DECREF(args_copy);
    Py_DECREF(method);
    return -1;
  }

  PyObject *coro = PyVectorcall_Call(method, args_copy, kwargs);
  Py_DECREF(args);
  Py_DECREF(args_copy);
  Py_DECREF(method);
  if (!coro) {
    PyErr_Print();
    return -1;
  }

  if (PyAwaitable_AddAwait(awaitable, coro, cb, NULL) < 0) {
    return -1;
  }

  Py_DECREF(coro);
  return 0;
}

int _PyObject_CallAndAwaitCoroutine(PyObject *awaitable, PyObject *obj, const char *name, PyObject *args, awaitcallback cb) {
  return _PyObject_CallAndAwaitCoroutineAndKeywords(awaitable, obj, name, args, NULL, cb);
}

int _PyObject_CallAndAwaitCoroutineNoArgs(PyObject *awaitable, PyObject *obj, const char *name, awaitcallback cb) {
  return _PyObject_CallAndAwaitCoroutine(awaitable, obj, name, PyTuple_New(0), cb);
}

In this code I use pyawaitable to both define my own coroutines (awaitables) within c extension code, as well as being able call call say for example ones from asyncio as well properly from C code without any RuntimeWarnings getting emitted.

Benefits of having PyObject_VectorcallDict in limited api:

  • PyObject_Vectorcall is already in limited api. And allows passing in args without needing to pack the args into a tuple first.
  • it allows similar functionality, but similar to PyVectorcall_Call it allows passing in keyword args as a dict.
  • less chances of reference leaks or trying to decref an object that has 0 reference counts already. Reference issues are a true pain in the butt to resolve even when they can happen in the python core itself as well and the whole review process can easily miss a few places at review time.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions