Skip to content

Commit e60892f

Browse files
authored
gh-98586: Add vector call APIs to the Limited API (GH-98587)
Expose the facilities for making vector calls through Python's limited API.
1 parent d578aae commit e60892f

File tree

9 files changed

+170
-18
lines changed

9 files changed

+170
-18
lines changed

Doc/data/stable_abi.dat

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/abstract.h

+16
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,22 @@ PyAPI_FUNC(Py_ssize_t) PyVectorcall_NARGS(size_t nargsf);
238238
"tuple" and keyword arguments "dict". "dict" may also be NULL */
239239
PyAPI_FUNC(PyObject *) PyVectorcall_Call(PyObject *callable, PyObject *tuple, PyObject *dict);
240240

241+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030C0000
242+
#define PY_VECTORCALL_ARGUMENTS_OFFSET \
243+
(_Py_STATIC_CAST(size_t, 1) << (8 * sizeof(size_t) - 1))
244+
245+
/* Perform a PEP 590-style vector call on 'callable' */
246+
PyAPI_FUNC(PyObject *) PyObject_Vectorcall(
247+
PyObject *callable,
248+
PyObject *const *args,
249+
size_t nargsf,
250+
PyObject *kwnames);
251+
252+
/* Call the method 'name' on args[0] with arguments in args[1..nargsf-1]. */
253+
PyAPI_FUNC(PyObject *) PyObject_VectorcallMethod(
254+
PyObject *name, PyObject *const *args,
255+
size_t nargsf, PyObject *kwnames);
256+
#endif
241257

242258
/* Implemented elsewhere:
243259

Include/cpython/abstract.h

-13
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ PyAPI_FUNC(PyObject *) _PyObject_MakeTpCall(
5050
PyObject *const *args, Py_ssize_t nargs,
5151
PyObject *keywords);
5252

53-
#define PY_VECTORCALL_ARGUMENTS_OFFSET \
54-
(_Py_STATIC_CAST(size_t, 1) << (8 * sizeof(size_t) - 1))
55-
5653
// PyVectorcall_NARGS() is exported as a function for the stable ABI.
5754
// Here (when we are not using the stable ABI), the name is overridden to
5855
// call a static inline function for best performance.
@@ -65,12 +62,6 @@ _PyVectorcall_NARGS(size_t n)
6562

6663
PyAPI_FUNC(vectorcallfunc) PyVectorcall_Function(PyObject *callable);
6764

68-
PyAPI_FUNC(PyObject *) PyObject_Vectorcall(
69-
PyObject *callable,
70-
PyObject *const *args,
71-
size_t nargsf,
72-
PyObject *kwnames);
73-
7465
// Backwards compatibility aliases for API that was provisional in Python 3.8
7566
#define _PyObject_Vectorcall PyObject_Vectorcall
7667
#define _PyObject_VectorcallMethod PyObject_VectorcallMethod
@@ -96,10 +87,6 @@ PyAPI_FUNC(PyObject *) _PyObject_FastCall(
9687

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

99-
PyAPI_FUNC(PyObject *) PyObject_VectorcallMethod(
100-
PyObject *name, PyObject *const *args,
101-
size_t nargsf, PyObject *kwnames);
102-
10390
static inline PyObject *
10491
PyObject_CallMethodNoArgs(PyObject *self, PyObject *name)
10592
{

Lib/test/test_call.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -812,11 +812,43 @@ def get_a(x):
812812
assert_equal("overridden", get_a(x))
813813

814814
@requires_limited_api
815-
def test_vectorcall_limited(self):
815+
def test_vectorcall_limited_incoming(self):
816816
from _testcapi import pyobject_vectorcall
817817
obj = _testcapi.LimitedVectorCallClass()
818818
self.assertEqual(pyobject_vectorcall(obj, (), ()), "vectorcall called")
819819

820+
@requires_limited_api
821+
def test_vectorcall_limited_outgoing(self):
822+
from _testcapi import call_vectorcall
823+
824+
args_captured = []
825+
kwargs_captured = []
826+
827+
def f(*args, **kwargs):
828+
args_captured.append(args)
829+
kwargs_captured.append(kwargs)
830+
return "success"
831+
832+
self.assertEqual(call_vectorcall(f), "success")
833+
self.assertEqual(args_captured, [("foo",)])
834+
self.assertEqual(kwargs_captured, [{"baz": "bar"}])
835+
836+
@requires_limited_api
837+
def test_vectorcall_limited_outgoing_method(self):
838+
from _testcapi import call_vectorcall_method
839+
840+
args_captured = []
841+
kwargs_captured = []
842+
843+
class TestInstance:
844+
def f(self, *args, **kwargs):
845+
args_captured.append(args)
846+
kwargs_captured.append(kwargs)
847+
return "success"
848+
849+
self.assertEqual(call_vectorcall_method(TestInstance()), "success")
850+
self.assertEqual(args_captured, [("foo",)])
851+
self.assertEqual(kwargs_captured, [{"baz": "bar"}])
820852

821853
class A:
822854
def method_two_args(self, x, y):

Lib/test/test_stable_abi_ctypes.py

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Added the methods :c:func:`PyObject_Vectorcall` and
2+
:c:func:`PyObject_VectorcallMethod` to the :ref:`Limited API <stable>` along
3+
with the auxiliary macro constant :c:macro:`PY_VECTORCALL_ARGUMENTS_OFFSET`.
4+
5+
The availability of these functions enables more efficient :PEP:`590` vector
6+
calls from binary extension modules that avoid argument boxing/unboxing
7+
overheads.

Misc/stable_abi.toml

+6
Original file line numberDiff line numberDiff line change
@@ -2293,3 +2293,9 @@
22932293
added = '3.12'
22942294
[typedef.vectorcallfunc]
22952295
added = '3.12'
2296+
[function.PyObject_Vectorcall]
2297+
added = '3.12'
2298+
[function.PyObject_VectorcallMethod]
2299+
added = '3.12'
2300+
[macro.PY_VECTORCALL_ARGUMENTS_OFFSET]
2301+
added = '3.12'

Modules/_testcapi/vectorcall_limited.c

+101-4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,105 @@ LimitedVectorCallClass_new(PyTypeObject *tp, PyTypeObject *a, PyTypeObject *kw)
3232
return self;
3333
}
3434

35+
static PyObject *
36+
call_vectorcall(PyObject* self, PyObject *callable)
37+
{
38+
PyObject *args[3] = { NULL, NULL, NULL };
39+
PyObject *kwname = NULL, *kwnames = NULL, *result = NULL;
40+
41+
args[1] = PyUnicode_FromString("foo");
42+
if (!args[1]) {
43+
goto leave;
44+
}
45+
46+
args[2] = PyUnicode_FromString("bar");
47+
if (!args[2]) {
48+
goto leave;
49+
}
50+
51+
kwname = PyUnicode_InternFromString("baz");
52+
if (!kwname) {
53+
goto leave;
54+
}
55+
56+
kwnames = PyTuple_New(1);
57+
if (!kwnames) {
58+
goto leave;
59+
}
60+
61+
if (PyTuple_SetItem(kwnames, 0, kwname)) {
62+
goto leave;
63+
}
64+
65+
result = PyObject_Vectorcall(
66+
callable,
67+
args + 1,
68+
1 | PY_VECTORCALL_ARGUMENTS_OFFSET,
69+
kwnames
70+
);
71+
72+
leave:
73+
Py_XDECREF(args[1]);
74+
Py_XDECREF(args[2]);
75+
Py_XDECREF(kwnames);
76+
77+
return result;
78+
}
79+
80+
static PyObject *
81+
call_vectorcall_method(PyObject* self, PyObject *callable)
82+
{
83+
PyObject *args[3] = { NULL, NULL, NULL };
84+
PyObject *name = NULL, *kwname = NULL,
85+
*kwnames = NULL, *result = NULL;
86+
87+
name = PyUnicode_FromString("f");
88+
if (!name) {
89+
goto leave;
90+
}
91+
92+
args[0] = callable;
93+
args[1] = PyUnicode_FromString("foo");
94+
if (!args[1]) {
95+
goto leave;
96+
}
97+
98+
args[2] = PyUnicode_FromString("bar");
99+
if (!args[2]) {
100+
goto leave;
101+
}
102+
103+
kwname = PyUnicode_InternFromString("baz");
104+
if (!kwname) {
105+
goto leave;
106+
}
107+
108+
kwnames = PyTuple_New(1);
109+
if (!kwnames) {
110+
goto leave;
111+
}
112+
113+
if (PyTuple_SetItem(kwnames, 0, kwname)) {
114+
goto leave;
115+
}
116+
117+
118+
result = PyObject_VectorcallMethod(
119+
name,
120+
args,
121+
2 | PY_VECTORCALL_ARGUMENTS_OFFSET,
122+
kwnames
123+
);
124+
125+
leave:
126+
Py_XDECREF(name);
127+
Py_XDECREF(args[1]);
128+
Py_XDECREF(args[2]);
129+
Py_XDECREF(kwnames);
130+
131+
return result;
132+
}
133+
35134
static PyMemberDef LimitedVectorCallClass_members[] = {
36135
{"__vectorcalloffset__", T_PYSSIZET, sizeof(PyObject), READONLY},
37136
{NULL}
@@ -54,10 +153,8 @@ static PyType_Spec LimitedVectorCallClass_spec = {
54153
};
55154

56155
static PyMethodDef TestMethods[] = {
57-
/* Add module methods here.
58-
* (Empty list left here as template/example, since using
59-
* PyModule_AddFunctions isn't very common.)
60-
*/
156+
{"call_vectorcall", call_vectorcall, METH_O},
157+
{"call_vectorcall_method", call_vectorcall_method, METH_O},
61158
{NULL},
62159
};
63160

PC/python3dll.c

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)