Skip to content

bpo-36974: PEP 590 #13185

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

Merged
merged 9 commits into from
May 29, 2019
1 change: 1 addition & 0 deletions Include/classobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ typedef struct {
PyObject *im_func; /* The callable object implementing the method */
PyObject *im_self; /* The instance it is bound to */
PyObject *im_weakreflist; /* List of weak references */
vectorcallfunc vectorcall;
} PyMethodObject;

PyAPI_DATA(PyTypeObject) PyMethod_Type;
Expand Down
117 changes: 83 additions & 34 deletions Include/cpython/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ PyAPI_FUNC(int) _PyStack_UnpackDict(
/* Suggested size (number of positional arguments) for arrays of PyObject*
allocated on a C stack to avoid allocating memory on the heap memory. Such
array is used to pass positional arguments to call functions of the
_PyObject_FastCall() family.
_PyObject_Vectorcall() family.

The size is chosen to not abuse the C stack and so limit the risk of stack
overflow. The size is also chosen to allow using the small stack for most
Expand All @@ -56,50 +56,103 @@ PyAPI_FUNC(int) _PyStack_UnpackDict(
#define _PY_FASTCALL_SMALL_STACK 5

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

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

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

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

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

kwnames must only contains str strings, no subclass, and all keys must
be unique.
static inline Py_ssize_t
PyVectorcall_NARGS(size_t n)
{
return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET;
}

static inline vectorcallfunc
_PyVectorcall_Function(PyObject *callable)
{
PyTypeObject *tp = Py_TYPE(callable);
if (!PyType_HasFeature(tp, _Py_TPFLAGS_HAVE_VECTORCALL)) {
return NULL;
}
assert(PyCallable_Check(callable));
Py_ssize_t offset = tp->tp_vectorcall_offset;
assert(offset > 0);
vectorcallfunc *ptr = (vectorcallfunc *)(((char *)callable) + offset);
return *ptr;
}

/* Call the callable object 'callable' with the "vectorcall" calling
convention.

args is a C array for positional arguments.

nargsf is the number of positional arguments plus optionally the flag
PY_VECTORCALL_ARGUMENTS_OFFSET which means that the caller is allowed to
modify args[-1].

If nargs is equal to zero and there is no keyword argument (kwnames is
NULL or its size is zero), args can be NULL.
kwnames is a tuple of keyword names. The values of the keyword arguments
are stored in "args" after the positional arguments (note that the number
of keyword arguments does not change nargsf). kwnames can also be NULL if
there are no keyword arguments.

keywords must only contains str strings (no subclass), and all keys must
be unique.

Return the result on success. Raise an exception and return NULL on
error. */
PyAPI_FUNC(PyObject *) _PyObject_FastCallKeywords(
static inline PyObject *
_PyObject_Vectorcall(PyObject *callable, PyObject *const *args,
size_t nargsf, PyObject *kwnames)
{
assert(kwnames == NULL || PyTuple_Check(kwnames));
assert(args != NULL || PyVectorcall_NARGS(nargsf) == 0);
vectorcallfunc func = _PyVectorcall_Function(callable);
if (func == NULL) {
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
return _PyObject_MakeTpCall(callable, args, nargs, kwnames);
}
PyObject *res = func(callable, args, nargsf, kwnames);
return _Py_CheckFunctionResult(callable, res, NULL);
}

/* Same as _PyObject_Vectorcall except that keyword arguments are passed as
dict, which may be NULL if there are no keyword arguments. */
PyAPI_FUNC(PyObject *) _PyObject_FastCallDict(
PyObject *callable,
PyObject *const *args,
Py_ssize_t nargs,
PyObject *kwnames);
size_t nargsf,
PyObject *kwargs);

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

#define _PyObject_CallNoArg(func) \
_PyObject_FastCallDict((func), NULL, 0, NULL)
/* Same as _PyObject_Vectorcall except without keyword arguments */
static inline PyObject *
_PyObject_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs)
{
return _PyObject_Vectorcall(func, args, (size_t)nargs, NULL);
}

/* Call a callable without any arguments */
static inline PyObject *
_PyObject_CallNoArg(PyObject *func) {
return _PyObject_Vectorcall(func, NULL, 0, NULL);
}

PyAPI_FUNC(PyObject *) _PyObject_Call_Prepend(
PyObject *callable,
Expand All @@ -113,10 +166,6 @@ PyAPI_FUNC(PyObject *) _PyObject_FastCall_Prepend(
PyObject *const *args,
Py_ssize_t nargs);

PyAPI_FUNC(PyObject *) _Py_CheckFunctionResult(PyObject *callable,
PyObject *result,
const char *where);

/* Like PyObject_CallMethod(), but expect a _Py_Identifier*
as the method name. */
PyAPI_FUNC(PyObject *) _PyObject_CallMethodId(PyObject *obj,
Expand Down
15 changes: 8 additions & 7 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ typedef struct bufferinfo {
typedef int (*getbufferproc)(PyObject *, Py_buffer *, int);
typedef void (*releasebufferproc)(PyObject *, Py_buffer *);

typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args,
size_t nargsf, PyObject *kwnames);

/* Maximum number of dimensions */
#define PyBUF_MAX_NDIM 64

Expand Down Expand Up @@ -167,12 +170,9 @@ typedef struct {
releasebufferproc bf_releasebuffer;
} PyBufferProcs;

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

typedef struct _typeobject {
PyObject_VAR_HEAD
Expand All @@ -182,7 +182,7 @@ typedef struct _typeobject {
/* Methods to implement standard operations */

destructor tp_dealloc;
printfunc tp_print;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
Expand Down Expand Up @@ -254,6 +254,7 @@ typedef struct _typeobject {
unsigned int tp_version_tag;

destructor tp_finalize;
vectorcallfunc tp_vectorcall;

#ifdef COUNT_ALLOCS
/* these must be last and never explicitly initialized */
Expand Down
3 changes: 2 additions & 1 deletion Include/descrobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ typedef struct {
typedef struct {
PyDescr_COMMON;
PyMethodDef *d_method;
vectorcallfunc vectorcall;
} PyMethodDescrObject;

typedef struct {
Expand Down Expand Up @@ -92,7 +93,7 @@ PyAPI_FUNC(PyObject *) PyDescr_NewGetSet(PyTypeObject *,
#ifndef Py_LIMITED_API

PyAPI_FUNC(PyObject *) _PyMethodDescr_FastCallKeywords(
PyObject *descrobj, PyObject *const *stack, Py_ssize_t nargs, PyObject *kwnames);
PyObject *descrobj, PyObject *const *args, size_t nargsf, PyObject *kwnames);
PyAPI_FUNC(PyObject *) PyDescr_NewWrapper(PyTypeObject *,
struct wrapperbase *, void *);
#define PyDescr_IsData(d) (Py_TYPE(d)->tp_descr_set != NULL)
Expand Down
3 changes: 2 additions & 1 deletion Include/funcobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ typedef struct {
PyObject *func_module; /* The __module__ attribute, can be anything */
PyObject *func_annotations; /* Annotations, a dict or NULL */
PyObject *func_qualname; /* The qualified name */
vectorcallfunc vectorcall;

/* Invariant:
* func_closure contains the bindings for func_code->co_freevars, so
Expand Down Expand Up @@ -68,7 +69,7 @@ PyAPI_FUNC(PyObject *) _PyFunction_FastCallDict(
PyAPI_FUNC(PyObject *) _PyFunction_FastCallKeywords(
PyObject *func,
PyObject *const *stack,
Py_ssize_t nargs,
size_t nargsf,
PyObject *kwnames);
#endif

Expand Down
3 changes: 2 additions & 1 deletion Include/methodobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ PyAPI_FUNC(PyObject *) _PyCFunction_FastCallDict(PyObject *func,

PyAPI_FUNC(PyObject *) _PyCFunction_FastCallKeywords(PyObject *func,
PyObject *const *stack,
Py_ssize_t nargs,
size_t nargsf,
PyObject *kwnames);
#endif

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

PyAPI_FUNC(PyObject *) _PyMethodDef_RawFastCallDict(
Expand Down
5 changes: 5 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ given type object has a specified feature.
/* Set if the type allows subclassing */
#define Py_TPFLAGS_BASETYPE (1UL << 10)

/* Set if the type implements the vectorcall protocol (PEP 590) */
#ifndef Py_LIMITED_API
#define _Py_TPFLAGS_HAVE_VECTORCALL (1UL << 11)
#endif

/* Set if the type is 'ready' -- fully initialized */
#define Py_TPFLAGS_READY (1UL << 12)

Expand Down
16 changes: 8 additions & 8 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def test_fastcall(self):
result = _testcapi.pyobject_fastcall(func, None)
self.check_result(result, expected)

def test_fastcall_dict(self):
def test_vectorcall_dict(self):
# Test _PyObject_FastCallDict()

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

def test_fastcall_keywords(self):
# Test _PyObject_FastCallKeywords()
def test_vectorcall(self):
# Test _PyObject_Vectorcall()

for func, args, expected in self.CALLS_POSARGS:
with self.subTest(func=func, args=args):
# kwnames=NULL
result = _testcapi.pyobject_fastcallkeywords(func, args, None)
result = _testcapi.pyobject_vectorcall(func, args, None)
self.check_result(result, expected)

# kwnames=()
result = _testcapi.pyobject_fastcallkeywords(func, args, ())
result = _testcapi.pyobject_vectorcall(func, args, ())
self.check_result(result, expected)

if not args:
# kwnames=NULL
result = _testcapi.pyobject_fastcallkeywords(func, None, None)
result = _testcapi.pyobject_vectorcall(func, None, None)
self.check_result(result, expected)

# kwnames=()
result = _testcapi.pyobject_fastcallkeywords(func, None, ())
result = _testcapi.pyobject_vectorcall(func, None, ())
self.check_result(result, expected)

for func, args, kwargs, expected in self.CALLS_KWARGS:
with self.subTest(func=func, args=args, kwargs=kwargs):
kwnames = tuple(kwargs.keys())
args = args + tuple(kwargs.values())
result = _testcapi.pyobject_fastcallkeywords(func, args, kwnames)
result = _testcapi.pyobject_vectorcall(func, args, kwnames)
self.check_result(result, expected)

def test_fastcall_clearing_dict(self):
Expand Down
47 changes: 47 additions & 0 deletions Lib/test/test_capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ def testfunction(self):
"""some doc"""
return self

def testfunction_kw(self, *, kw):
"""some doc"""
return self


class InstanceMethod:
id = _testcapi.instancemethod(id)
testfunction = _testcapi.instancemethod(testfunction)
Expand Down Expand Up @@ -479,6 +484,48 @@ class MethodDescriptorHeap(_testcapi.MethodDescriptorBase):
pass
self.assertFalse(MethodDescriptorHeap.__flags__ & Py_TPFLAGS_METHOD_DESCRIPTOR)

def test_vectorcall(self):
# Test a bunch of different ways to call objects:
# 1. normal call
# 2. vectorcall using _PyObject_Vectorcall()
# 3. vectorcall using PyVectorcall_Call()
# 4. call as bound method
# 5. call using functools.partial

# A list of (function, args, kwargs, result) calls to test
calls = [(len, (range(42),), {}, 42),
(list.append, ([], 0), {}, None),
([].append, (0,), {}, None),
(sum, ([36],), {"start":6}, 42),
(testfunction, (42,), {}, 42),
(testfunction_kw, (42,), {"kw":None}, 42)]

from _testcapi import pyobject_vectorcall, pyvectorcall_call
from types import MethodType
from functools import partial

def vectorcall(func, args, kwargs):
args = *args, *kwargs.values()
kwnames = tuple(kwargs)
return pyobject_vectorcall(func, args, kwnames)

for (func, args, kwargs, expected) in calls:
with self.subTest(str(func)):
args1 = args[1:]
meth = MethodType(func, args[0])
wrapped = partial(func)
if not kwargs:
self.assertEqual(expected, func(*args))
self.assertEqual(expected, pyobject_vectorcall(func, args, None))
self.assertEqual(expected, pyvectorcall_call(func, args))
self.assertEqual(expected, meth(*args1))
self.assertEqual(expected, wrapped(*args))
self.assertEqual(expected, func(*args, **kwargs))
self.assertEqual(expected, vectorcall(func, args, kwargs))
self.assertEqual(expected, pyvectorcall_call(func, args, kwargs))
self.assertEqual(expected, meth(*args1, **kwargs))
self.assertEqual(expected, wrapped(*args, **kwargs))


class SubinterpreterTest(unittest.TestCase):

Expand Down
Loading