Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-98586: Add vector call APIs to the Limited API #98587

Merged
merged 10 commits into from
Oct 27, 2022
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'
97 changes: 97 additions & 0 deletions Modules/_testcapi/vectorcall_limited.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,101 @@ 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 @@ -58,6 +153,8 @@ static PyMethodDef TestMethods[] = {
* (Empty list left here as template/example, since using
* PyModule_AddFunctions isn't very common.)
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment can go away now.

{"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.