Skip to content

Commit

Permalink
gh-98586: Add vector call APIs to the Limited API (GH-98587)
Browse files Browse the repository at this point in the history
Expose the facilities for making vector calls through Python's limited API.
  • Loading branch information
wjakob authored Oct 27, 2022
1 parent d578aae commit e60892f
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 18 deletions.
3 changes: 3 additions & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions Include/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,22 @@ PyAPI_FUNC(Py_ssize_t) PyVectorcall_NARGS(size_t nargsf);
"tuple" and keyword arguments "dict". "dict" may also be NULL */
PyAPI_FUNC(PyObject *) PyVectorcall_Call(PyObject *callable, PyObject *tuple, PyObject *dict);

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030C0000
#define PY_VECTORCALL_ARGUMENTS_OFFSET \
(_Py_STATIC_CAST(size_t, 1) << (8 * sizeof(size_t) - 1))

/* Perform a PEP 590-style vector call on 'callable' */
PyAPI_FUNC(PyObject *) PyObject_Vectorcall(
PyObject *callable,
PyObject *const *args,
size_t nargsf,
PyObject *kwnames);

/* Call the method 'name' on args[0] with arguments in args[1..nargsf-1]. */
PyAPI_FUNC(PyObject *) PyObject_VectorcallMethod(
PyObject *name, PyObject *const *args,
size_t nargsf, PyObject *kwnames);
#endif

/* Implemented elsewhere:
Expand Down
13 changes: 0 additions & 13 deletions Include/cpython/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ PyAPI_FUNC(PyObject *) _PyObject_MakeTpCall(
PyObject *const *args, Py_ssize_t nargs,
PyObject *keywords);

#define PY_VECTORCALL_ARGUMENTS_OFFSET \
(_Py_STATIC_CAST(size_t, 1) << (8 * sizeof(size_t) - 1))

// PyVectorcall_NARGS() is exported as a function for the stable ABI.
// Here (when we are not using the stable ABI), the name is overridden to
// call a static inline function for best performance.
Expand All @@ -65,12 +62,6 @@ _PyVectorcall_NARGS(size_t n)

PyAPI_FUNC(vectorcallfunc) PyVectorcall_Function(PyObject *callable);

PyAPI_FUNC(PyObject *) PyObject_Vectorcall(
PyObject *callable,
PyObject *const *args,
size_t nargsf,
PyObject *kwnames);

// Backwards compatibility aliases for API that was provisional in Python 3.8
#define _PyObject_Vectorcall PyObject_Vectorcall
#define _PyObject_VectorcallMethod PyObject_VectorcallMethod
Expand All @@ -96,10 +87,6 @@ PyAPI_FUNC(PyObject *) _PyObject_FastCall(

PyAPI_FUNC(PyObject *) PyObject_CallOneArg(PyObject *func, PyObject *arg);

PyAPI_FUNC(PyObject *) PyObject_VectorcallMethod(
PyObject *name, PyObject *const *args,
size_t nargsf, PyObject *kwnames);

static inline PyObject *
PyObject_CallMethodNoArgs(PyObject *self, PyObject *name)
{
Expand Down
34 changes: 33 additions & 1 deletion Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,11 +812,43 @@ def get_a(x):
assert_equal("overridden", get_a(x))

@requires_limited_api
def test_vectorcall_limited(self):
def test_vectorcall_limited_incoming(self):
from _testcapi import pyobject_vectorcall
obj = _testcapi.LimitedVectorCallClass()
self.assertEqual(pyobject_vectorcall(obj, (), ()), "vectorcall called")

@requires_limited_api
def test_vectorcall_limited_outgoing(self):
from _testcapi import call_vectorcall

args_captured = []
kwargs_captured = []

def f(*args, **kwargs):
args_captured.append(args)
kwargs_captured.append(kwargs)
return "success"

self.assertEqual(call_vectorcall(f), "success")
self.assertEqual(args_captured, [("foo",)])
self.assertEqual(kwargs_captured, [{"baz": "bar"}])

@requires_limited_api
def test_vectorcall_limited_outgoing_method(self):
from _testcapi import call_vectorcall_method

args_captured = []
kwargs_captured = []

class TestInstance:
def f(self, *args, **kwargs):
args_captured.append(args)
kwargs_captured.append(kwargs)
return "success"

self.assertEqual(call_vectorcall_method(TestInstance()), "success")
self.assertEqual(args_captured, [("foo",)])
self.assertEqual(kwargs_captured, [{"baz": "bar"}])

class A:
def method_two_args(self, x, y):
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Added the methods :c:func:`PyObject_Vectorcall` and
:c:func:`PyObject_VectorcallMethod` to the :ref:`Limited API <stable>` along
with the auxiliary macro constant :c:macro:`PY_VECTORCALL_ARGUMENTS_OFFSET`.

The availability of these functions enables more efficient :PEP:`590` vector
calls from binary extension modules that avoid argument boxing/unboxing
overheads.
6 changes: 6 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2293,3 +2293,9 @@
added = '3.12'
[typedef.vectorcallfunc]
added = '3.12'
[function.PyObject_Vectorcall]
added = '3.12'
[function.PyObject_VectorcallMethod]
added = '3.12'
[macro.PY_VECTORCALL_ARGUMENTS_OFFSET]
added = '3.12'
105 changes: 101 additions & 4 deletions Modules/_testcapi/vectorcall_limited.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,105 @@ LimitedVectorCallClass_new(PyTypeObject *tp, PyTypeObject *a, PyTypeObject *kw)
return self;
}

static PyObject *
call_vectorcall(PyObject* self, PyObject *callable)
{
PyObject *args[3] = { NULL, NULL, NULL };
PyObject *kwname = NULL, *kwnames = NULL, *result = NULL;

args[1] = PyUnicode_FromString("foo");
if (!args[1]) {
goto leave;
}

args[2] = PyUnicode_FromString("bar");
if (!args[2]) {
goto leave;
}

kwname = PyUnicode_InternFromString("baz");
if (!kwname) {
goto leave;
}

kwnames = PyTuple_New(1);
if (!kwnames) {
goto leave;
}

if (PyTuple_SetItem(kwnames, 0, kwname)) {
goto leave;
}

result = PyObject_Vectorcall(
callable,
args + 1,
1 | PY_VECTORCALL_ARGUMENTS_OFFSET,
kwnames
);

leave:
Py_XDECREF(args[1]);
Py_XDECREF(args[2]);
Py_XDECREF(kwnames);

return result;
}

static PyObject *
call_vectorcall_method(PyObject* self, PyObject *callable)
{
PyObject *args[3] = { NULL, NULL, NULL };
PyObject *name = NULL, *kwname = NULL,
*kwnames = NULL, *result = NULL;

name = PyUnicode_FromString("f");
if (!name) {
goto leave;
}

args[0] = callable;
args[1] = PyUnicode_FromString("foo");
if (!args[1]) {
goto leave;
}

args[2] = PyUnicode_FromString("bar");
if (!args[2]) {
goto leave;
}

kwname = PyUnicode_InternFromString("baz");
if (!kwname) {
goto leave;
}

kwnames = PyTuple_New(1);
if (!kwnames) {
goto leave;
}

if (PyTuple_SetItem(kwnames, 0, kwname)) {
goto leave;
}


result = PyObject_VectorcallMethod(
name,
args,
2 | PY_VECTORCALL_ARGUMENTS_OFFSET,
kwnames
);

leave:
Py_XDECREF(name);
Py_XDECREF(args[1]);
Py_XDECREF(args[2]);
Py_XDECREF(kwnames);

return result;
}

static PyMemberDef LimitedVectorCallClass_members[] = {
{"__vectorcalloffset__", T_PYSSIZET, sizeof(PyObject), READONLY},
{NULL}
Expand All @@ -54,10 +153,8 @@ static PyType_Spec LimitedVectorCallClass_spec = {
};

static PyMethodDef TestMethods[] = {
/* Add module methods here.
* (Empty list left here as template/example, since using
* PyModule_AddFunctions isn't very common.)
*/
{"call_vectorcall", call_vectorcall, METH_O},
{"call_vectorcall_method", call_vectorcall_method, METH_O},
{NULL},
};

Expand Down
2 changes: 2 additions & 0 deletions PC/python3dll.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit e60892f

Please sign in to comment.