Skip to content

Commit aacc77f

Browse files
jdemeyermarkshannon
authored andcommitted
bpo-36974: implement PEP 590 (GH-13185)
Co-authored-by: Jeroen Demeyer <J.Demeyer@UGent.be> Co-authored-by: Mark Shannon <mark@hotpy.org>
1 parent d30da5d commit aacc77f

22 files changed

+404
-233
lines changed

Include/classobject.h

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ typedef struct {
1414
PyObject *im_func; /* The callable object implementing the method */
1515
PyObject *im_self; /* The instance it is bound to */
1616
PyObject *im_weakreflist; /* List of weak references */
17+
vectorcallfunc vectorcall;
1718
} PyMethodObject;
1819

1920
PyAPI_DATA(PyTypeObject) PyMethod_Type;

Include/cpython/abstract.h

+83-34
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ PyAPI_FUNC(int) _PyStack_UnpackDict(
4747
/* Suggested size (number of positional arguments) for arrays of PyObject*
4848
allocated on a C stack to avoid allocating memory on the heap memory. Such
4949
array is used to pass positional arguments to call functions of the
50-
_PyObject_FastCall() family.
50+
_PyObject_Vectorcall() family.
5151
5252
The size is chosen to not abuse the C stack and so limit the risk of stack
5353
overflow. The size is also chosen to allow using the small stack for most
@@ -56,50 +56,103 @@ PyAPI_FUNC(int) _PyStack_UnpackDict(
5656
#define _PY_FASTCALL_SMALL_STACK 5
5757

5858
/* Return 1 if callable supports FASTCALL calling convention for positional
59-
arguments: see _PyObject_FastCallDict() and _PyObject_FastCallKeywords() */
59+
arguments: see _PyObject_Vectorcall() and _PyObject_FastCallDict() */
6060
PyAPI_FUNC(int) _PyObject_HasFastCall(PyObject *callable);
6161

62-
/* Call the callable object 'callable' with the "fast call" calling convention:
63-
args is a C array for positional arguments (nargs is the number of
64-
positional arguments), kwargs is a dictionary for keyword arguments.
62+
PyAPI_FUNC(PyObject *) _Py_CheckFunctionResult(PyObject *callable,
63+
PyObject *result,
64+
const char *where);
6565

66-
If nargs is equal to zero, args can be NULL. kwargs can be NULL.
67-
nargs must be greater or equal to zero.
66+
/* === Vectorcall protocol (PEP 590) ============================= */
6867

69-
Return the result on success. Raise an exception and return NULL on
70-
error. */
71-
PyAPI_FUNC(PyObject *) _PyObject_FastCallDict(
68+
/* Call callable using tp_call. Arguments are like _PyObject_Vectorcall()
69+
or _PyObject_FastCallDict() (both forms are supported),
70+
except that nargs is plainly the number of arguments without flags. */
71+
PyAPI_FUNC(PyObject *) _PyObject_MakeTpCall(
7272
PyObject *callable,
73-
PyObject *const *args,
74-
Py_ssize_t nargs,
75-
PyObject *kwargs);
73+
PyObject *const *args, Py_ssize_t nargs,
74+
PyObject *keywords);
7675

77-
/* Call the callable object 'callable' with the "fast call" calling convention:
78-
args is a C array for positional arguments followed by values of
79-
keyword arguments. Keys of keyword arguments are stored as a tuple
80-
of strings in kwnames. nargs is the number of positional parameters at
81-
the beginning of stack. The size of kwnames gives the number of keyword
82-
values in the stack after positional arguments.
76+
#define PY_VECTORCALL_ARGUMENTS_OFFSET ((size_t)1 << (8 * sizeof(size_t) - 1))
8377

84-
kwnames must only contains str strings, no subclass, and all keys must
85-
be unique.
78+
static inline Py_ssize_t
79+
PyVectorcall_NARGS(size_t n)
80+
{
81+
return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET;
82+
}
83+
84+
static inline vectorcallfunc
85+
_PyVectorcall_Function(PyObject *callable)
86+
{
87+
PyTypeObject *tp = Py_TYPE(callable);
88+
if (!PyType_HasFeature(tp, _Py_TPFLAGS_HAVE_VECTORCALL)) {
89+
return NULL;
90+
}
91+
assert(PyCallable_Check(callable));
92+
Py_ssize_t offset = tp->tp_vectorcall_offset;
93+
assert(offset > 0);
94+
vectorcallfunc *ptr = (vectorcallfunc *)(((char *)callable) + offset);
95+
return *ptr;
96+
}
97+
98+
/* Call the callable object 'callable' with the "vectorcall" calling
99+
convention.
100+
101+
args is a C array for positional arguments.
102+
103+
nargsf is the number of positional arguments plus optionally the flag
104+
PY_VECTORCALL_ARGUMENTS_OFFSET which means that the caller is allowed to
105+
modify args[-1].
86106
87-
If nargs is equal to zero and there is no keyword argument (kwnames is
88-
NULL or its size is zero), args can be NULL.
107+
kwnames is a tuple of keyword names. The values of the keyword arguments
108+
are stored in "args" after the positional arguments (note that the number
109+
of keyword arguments does not change nargsf). kwnames can also be NULL if
110+
there are no keyword arguments.
111+
112+
keywords must only contains str strings (no subclass), and all keys must
113+
be unique.
89114
90115
Return the result on success. Raise an exception and return NULL on
91116
error. */
92-
PyAPI_FUNC(PyObject *) _PyObject_FastCallKeywords(
117+
static inline PyObject *
118+
_PyObject_Vectorcall(PyObject *callable, PyObject *const *args,
119+
size_t nargsf, PyObject *kwnames)
120+
{
121+
assert(kwnames == NULL || PyTuple_Check(kwnames));
122+
assert(args != NULL || PyVectorcall_NARGS(nargsf) == 0);
123+
vectorcallfunc func = _PyVectorcall_Function(callable);
124+
if (func == NULL) {
125+
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
126+
return _PyObject_MakeTpCall(callable, args, nargs, kwnames);
127+
}
128+
PyObject *res = func(callable, args, nargsf, kwnames);
129+
return _Py_CheckFunctionResult(callable, res, NULL);
130+
}
131+
132+
/* Same as _PyObject_Vectorcall except that keyword arguments are passed as
133+
dict, which may be NULL if there are no keyword arguments. */
134+
PyAPI_FUNC(PyObject *) _PyObject_FastCallDict(
93135
PyObject *callable,
94136
PyObject *const *args,
95-
Py_ssize_t nargs,
96-
PyObject *kwnames);
137+
size_t nargsf,
138+
PyObject *kwargs);
97139

98-
#define _PyObject_FastCall(func, args, nargs) \
99-
_PyObject_FastCallDict((func), (args), (nargs), NULL)
140+
/* Call "callable" (which must support vectorcall) with positional arguments
141+
"tuple" and keyword arguments "dict". "dict" may also be NULL */
142+
PyAPI_FUNC(PyObject *) PyVectorcall_Call(PyObject *callable, PyObject *tuple, PyObject *dict);
100143

101-
#define _PyObject_CallNoArg(func) \
102-
_PyObject_FastCallDict((func), NULL, 0, NULL)
144+
/* Same as _PyObject_Vectorcall except without keyword arguments */
145+
static inline PyObject *
146+
_PyObject_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs)
147+
{
148+
return _PyObject_Vectorcall(func, args, (size_t)nargs, NULL);
149+
}
150+
151+
/* Call a callable without any arguments */
152+
static inline PyObject *
153+
_PyObject_CallNoArg(PyObject *func) {
154+
return _PyObject_Vectorcall(func, NULL, 0, NULL);
155+
}
103156

104157
PyAPI_FUNC(PyObject *) _PyObject_Call_Prepend(
105158
PyObject *callable,
@@ -113,10 +166,6 @@ PyAPI_FUNC(PyObject *) _PyObject_FastCall_Prepend(
113166
PyObject *const *args,
114167
Py_ssize_t nargs);
115168

116-
PyAPI_FUNC(PyObject *) _Py_CheckFunctionResult(PyObject *callable,
117-
PyObject *result,
118-
const char *where);
119-
120169
/* Like PyObject_CallMethod(), but expect a _Py_Identifier*
121170
as the method name. */
122171
PyAPI_FUNC(PyObject *) _PyObject_CallMethodId(PyObject *obj,

Include/cpython/object.h

+8-7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ typedef struct bufferinfo {
5555
typedef int (*getbufferproc)(PyObject *, Py_buffer *, int);
5656
typedef void (*releasebufferproc)(PyObject *, Py_buffer *);
5757

58+
typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args,
59+
size_t nargsf, PyObject *kwnames);
60+
5861
/* Maximum number of dimensions */
5962
#define PyBUF_MAX_NDIM 64
6063

@@ -167,12 +170,9 @@ typedef struct {
167170
releasebufferproc bf_releasebuffer;
168171
} PyBufferProcs;
169172

170-
/* We can't provide a full compile-time check that limited-API
171-
users won't implement tp_print. However, not defining printfunc
172-
and making tp_print of a different function pointer type
173-
if Py_LIMITED_API is set should at least cause a warning
174-
in most cases. */
175-
typedef int (*printfunc)(PyObject *, FILE *, int);
173+
/* Allow printfunc in the tp_vectorcall_offset slot for
174+
* backwards-compatibility */
175+
typedef Py_ssize_t printfunc;
176176

177177
typedef struct _typeobject {
178178
PyObject_VAR_HEAD
@@ -182,7 +182,7 @@ typedef struct _typeobject {
182182
/* Methods to implement standard operations */
183183

184184
destructor tp_dealloc;
185-
printfunc tp_print;
185+
Py_ssize_t tp_vectorcall_offset;
186186
getattrfunc tp_getattr;
187187
setattrfunc tp_setattr;
188188
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
@@ -254,6 +254,7 @@ typedef struct _typeobject {
254254
unsigned int tp_version_tag;
255255

256256
destructor tp_finalize;
257+
vectorcallfunc tp_vectorcall;
257258

258259
#ifdef COUNT_ALLOCS
259260
/* these must be last and never explicitly initialized */

Include/descrobject.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ typedef struct {
5353
typedef struct {
5454
PyDescr_COMMON;
5555
PyMethodDef *d_method;
56+
vectorcallfunc vectorcall;
5657
} PyMethodDescrObject;
5758

5859
typedef struct {
@@ -92,7 +93,7 @@ PyAPI_FUNC(PyObject *) PyDescr_NewGetSet(PyTypeObject *,
9293
#ifndef Py_LIMITED_API
9394

9495
PyAPI_FUNC(PyObject *) _PyMethodDescr_FastCallKeywords(
95-
PyObject *descrobj, PyObject *const *stack, Py_ssize_t nargs, PyObject *kwnames);
96+
PyObject *descrobj, PyObject *const *args, size_t nargsf, PyObject *kwnames);
9697
PyAPI_FUNC(PyObject *) PyDescr_NewWrapper(PyTypeObject *,
9798
struct wrapperbase *, void *);
9899
#define PyDescr_IsData(d) (Py_TYPE(d)->tp_descr_set != NULL)

Include/funcobject.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ typedef struct {
3232
PyObject *func_module; /* The __module__ attribute, can be anything */
3333
PyObject *func_annotations; /* Annotations, a dict or NULL */
3434
PyObject *func_qualname; /* The qualified name */
35+
vectorcallfunc vectorcall;
3536

3637
/* Invariant:
3738
* func_closure contains the bindings for func_code->co_freevars, so
@@ -68,7 +69,7 @@ PyAPI_FUNC(PyObject *) _PyFunction_FastCallDict(
6869
PyAPI_FUNC(PyObject *) _PyFunction_FastCallKeywords(
6970
PyObject *func,
7071
PyObject *const *stack,
71-
Py_ssize_t nargs,
72+
size_t nargsf,
7273
PyObject *kwnames);
7374
#endif
7475

Include/methodobject.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ PyAPI_FUNC(PyObject *) _PyCFunction_FastCallDict(PyObject *func,
4949

5050
PyAPI_FUNC(PyObject *) _PyCFunction_FastCallKeywords(PyObject *func,
5151
PyObject *const *stack,
52-
Py_ssize_t nargs,
52+
size_t nargsf,
5353
PyObject *kwnames);
5454
#endif
5555

@@ -105,6 +105,7 @@ typedef struct {
105105
PyObject *m_self; /* Passed as 'self' arg to the C func, can be NULL */
106106
PyObject *m_module; /* The __module__ attribute, can be anything */
107107
PyObject *m_weakreflist; /* List of weak references */
108+
vectorcallfunc vectorcall;
108109
} PyCFunctionObject;
109110

110111
PyAPI_FUNC(PyObject *) _PyMethodDef_RawFastCallDict(

Include/object.h

+5
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ given type object has a specified feature.
291291
/* Set if the type allows subclassing */
292292
#define Py_TPFLAGS_BASETYPE (1UL << 10)
293293

294+
/* Set if the type implements the vectorcall protocol (PEP 590) */
295+
#ifndef Py_LIMITED_API
296+
#define _Py_TPFLAGS_HAVE_VECTORCALL (1UL << 11)
297+
#endif
298+
294299
/* Set if the type is 'ready' -- fully initialized */
295300
#define Py_TPFLAGS_READY (1UL << 12)
296301

Lib/test/test_call.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ def test_fastcall(self):
402402
result = _testcapi.pyobject_fastcall(func, None)
403403
self.check_result(result, expected)
404404

405-
def test_fastcall_dict(self):
405+
def test_vectorcall_dict(self):
406406
# Test _PyObject_FastCallDict()
407407

408408
for func, args, expected in self.CALLS_POSARGS:
@@ -429,33 +429,33 @@ def test_fastcall_dict(self):
429429
result = _testcapi.pyobject_fastcalldict(func, args, kwargs)
430430
self.check_result(result, expected)
431431

432-
def test_fastcall_keywords(self):
433-
# Test _PyObject_FastCallKeywords()
432+
def test_vectorcall(self):
433+
# Test _PyObject_Vectorcall()
434434

435435
for func, args, expected in self.CALLS_POSARGS:
436436
with self.subTest(func=func, args=args):
437437
# kwnames=NULL
438-
result = _testcapi.pyobject_fastcallkeywords(func, args, None)
438+
result = _testcapi.pyobject_vectorcall(func, args, None)
439439
self.check_result(result, expected)
440440

441441
# kwnames=()
442-
result = _testcapi.pyobject_fastcallkeywords(func, args, ())
442+
result = _testcapi.pyobject_vectorcall(func, args, ())
443443
self.check_result(result, expected)
444444

445445
if not args:
446446
# kwnames=NULL
447-
result = _testcapi.pyobject_fastcallkeywords(func, None, None)
447+
result = _testcapi.pyobject_vectorcall(func, None, None)
448448
self.check_result(result, expected)
449449

450450
# kwnames=()
451-
result = _testcapi.pyobject_fastcallkeywords(func, None, ())
451+
result = _testcapi.pyobject_vectorcall(func, None, ())
452452
self.check_result(result, expected)
453453

454454
for func, args, kwargs, expected in self.CALLS_KWARGS:
455455
with self.subTest(func=func, args=args, kwargs=kwargs):
456456
kwnames = tuple(kwargs.keys())
457457
args = args + tuple(kwargs.values())
458-
result = _testcapi.pyobject_fastcallkeywords(func, args, kwnames)
458+
result = _testcapi.pyobject_vectorcall(func, args, kwnames)
459459
self.check_result(result, expected)
460460

461461
def test_fastcall_clearing_dict(self):

Lib/test/test_capi.py

+47
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ def testfunction(self):
3434
"""some doc"""
3535
return self
3636

37+
def testfunction_kw(self, *, kw):
38+
"""some doc"""
39+
return self
40+
41+
3742
class InstanceMethod:
3843
id = _testcapi.instancemethod(id)
3944
testfunction = _testcapi.instancemethod(testfunction)
@@ -479,6 +484,48 @@ class MethodDescriptorHeap(_testcapi.MethodDescriptorBase):
479484
pass
480485
self.assertFalse(MethodDescriptorHeap.__flags__ & Py_TPFLAGS_METHOD_DESCRIPTOR)
481486

487+
def test_vectorcall(self):
488+
# Test a bunch of different ways to call objects:
489+
# 1. normal call
490+
# 2. vectorcall using _PyObject_Vectorcall()
491+
# 3. vectorcall using PyVectorcall_Call()
492+
# 4. call as bound method
493+
# 5. call using functools.partial
494+
495+
# A list of (function, args, kwargs, result) calls to test
496+
calls = [(len, (range(42),), {}, 42),
497+
(list.append, ([], 0), {}, None),
498+
([].append, (0,), {}, None),
499+
(sum, ([36],), {"start":6}, 42),
500+
(testfunction, (42,), {}, 42),
501+
(testfunction_kw, (42,), {"kw":None}, 42)]
502+
503+
from _testcapi import pyobject_vectorcall, pyvectorcall_call
504+
from types import MethodType
505+
from functools import partial
506+
507+
def vectorcall(func, args, kwargs):
508+
args = *args, *kwargs.values()
509+
kwnames = tuple(kwargs)
510+
return pyobject_vectorcall(func, args, kwnames)
511+
512+
for (func, args, kwargs, expected) in calls:
513+
with self.subTest(str(func)):
514+
args1 = args[1:]
515+
meth = MethodType(func, args[0])
516+
wrapped = partial(func)
517+
if not kwargs:
518+
self.assertEqual(expected, func(*args))
519+
self.assertEqual(expected, pyobject_vectorcall(func, args, None))
520+
self.assertEqual(expected, pyvectorcall_call(func, args))
521+
self.assertEqual(expected, meth(*args1))
522+
self.assertEqual(expected, wrapped(*args))
523+
self.assertEqual(expected, func(*args, **kwargs))
524+
self.assertEqual(expected, vectorcall(func, args, kwargs))
525+
self.assertEqual(expected, pyvectorcall_call(func, args, kwargs))
526+
self.assertEqual(expected, meth(*args1, **kwargs))
527+
self.assertEqual(expected, wrapped(*args, **kwargs))
528+
482529

483530
class SubinterpreterTest(unittest.TestCase):
484531

0 commit comments

Comments
 (0)