Skip to content

Commit

Permalink
Clean up special handling of PyCFunction and CyFunction (GH-5739)
Browse files Browse the repository at this point in the history
This avoids crashes in debug mode where CPython's access functions assert a type check (which fails for CyFunction).
Avoid excluding CyFunction from the fast-call fast-paths for 0/1 args in Python debug mode. Also include the 1-arg (METH_O) case in the optimisation - I cannot see a reason why only 0-args should be special-cased for CyFunction.
Fix a bug in __Pyx_TryUnpackUnboundCMethod() where we checked for "defined(CYTHON_COMPILING_IN_PYPY)" (which is always true) instead of the defined 0/1 value.

It's probably ok to apply the unpacking of unbound methods to any PyCFunction, including subclasses.
Due to a bug, we weren't doing the type check at all before, which meant that PyCFunction_GET_SELF() could actually fail and we'd ignore the error.
  • Loading branch information
scoder committed Oct 4, 2023
1 parent 18d3e8e commit ed2933b
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 38 deletions.
13 changes: 3 additions & 10 deletions Cython/Compiler/Nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4964,19 +4964,12 @@ def generate_execution_code(self, code):
# need to get attribute manually--scope would return cdef method
code.globalstate.use_utility_code(
UtilityCode.load_cached("PyObjectGetAttrStr", "ObjectHandling.c"))
err = code.error_goto_if_null(func_node_temp, self.pos)
code.putln("%s = __Pyx_PyObject_GetAttrStr(%s, %s); %s" % (
func_node_temp, self_arg, interned_attr_cname, err))
func_node_temp, self_arg, interned_attr_cname,
code.error_goto_if_null(func_node_temp, self.pos)))
code.put_gotref(func_node_temp, py_object_type)

is_overridden = "(PyCFunction_GET_FUNCTION(%s) != (PyCFunction)(void*)%s)" % (
func_node_temp, method_entry.func_cname)
code.putln("#ifdef __Pyx_CyFunction_USED")
code.putln("if (!__Pyx_IsCyOrPyCFunction(%s)" % func_node_temp)
code.putln("#else")
code.putln("if (!PyCFunction_Check(%s)" % func_node_temp)
code.putln("#endif")
code.putln(" || %s) {" % is_overridden)
code.putln("if (!__Pyx_IsSameCFunction(%s, (void*) %s)) {" % (func_node_temp, method_entry.func_cname))
self.body.generate_execution_code(code)
code.putln("}")

Expand Down
21 changes: 20 additions & 1 deletion Cython/Utility/CythonFunction.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ typedef struct {
PyObject *func_is_coroutine;
} __pyx_CyFunctionObject;

#undef __Pyx_CyOrPyCFunction_Check
#define __Pyx_CyFunction_Check(obj) __Pyx_TypeCheck(obj, __pyx_CyFunctionType)
#define __Pyx_IsCyOrPyCFunction(obj) __Pyx_TypeCheck2(obj, __pyx_CyFunctionType, &PyCFunction_Type)
#define __Pyx_CyOrPyCFunction_Check(obj) __Pyx_TypeCheck2(obj, __pyx_CyFunctionType, &PyCFunction_Type)
#define __Pyx_CyFunction_CheckExact(obj) __Pyx_IS_TYPE(obj, __pyx_CyFunctionType)
static CYTHON_INLINE int __Pyx__IsSameCyOrCFunction(PyObject *func, void *cfunc);/*proto*/
#undef __Pyx_IsSameCFunction
#define __Pyx_IsSameCFunction(func, cfunc) __Pyx__IsSameCyOrCFunction(func, cfunc)

static PyObject *__Pyx_CyFunction_Init(__pyx_CyFunctionObject* op, PyMethodDef *ml,
int flags, PyObject* qualname,
Expand Down Expand Up @@ -115,6 +119,21 @@ static PyObject * __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS_METHOD(PyObject
//@requires: ModuleSetupCode.c::IncludeStructmemberH
//@requires: ObjectHandling.c::PyObjectGetAttrStr

#if CYTHON_COMPILING_IN_LIMITED_API
static CYTHON_INLINE int __Pyx__IsSameCyOrCFunction(PyObject *func, void *cfunc) {
if (__Pyx_CyFunction_Check(func)) {
return PyCFunction_GetFunction(((__pyx_CyFunctionObject*)func)->func) == (PyCFunction) cfunc;
} else if (PyCFunction_Check(func)) {
return PyCFunction_GetFunction(func) == (PyCFunction) cfunc;
}
return 0;
}
#else
static CYTHON_INLINE int __Pyx__IsSameCyOrCFunction(PyObject *func, void *cfunc) {
return __Pyx_CyOrPyCFunction_Check(func) && __Pyx_CyOrPyCFunction_GET_FUNCTION(func) == (PyCFunction) cfunc;
}
#endif

static CYTHON_INLINE void __Pyx__CyFunction_SetClassObj(__pyx_CyFunctionObject* f, PyObject* classobj) {
#if PY_VERSION_HEX < 0x030900B1 || CYTHON_COMPILING_IN_LIMITED_API
__Pyx_Py_XDECREF_SET(
Expand Down
33 changes: 33 additions & 0 deletions Cython/Utility/ModuleSetupCode.c
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,39 @@ class __Pyx_FakeReference {
#define __Pyx_PyVectorcall_NARGS(n) ((Py_ssize_t)(n))
#endif

// These PyCFunction related macros get redefined in CythonFunction.c.
// We need our own copies because the inline functions in CPython have a type-check assert
// that breaks with a CyFunction in debug mode.
#if PY_MAJOR_VERSION >= 0x030900B1
#define __Pyx_PyCFunction_CheckExact(func) PyCFunction_CheckExact(func)
#else
#define __Pyx_PyCFunction_CheckExact(func) PyCFunction_Check(func)
#endif
#define __Pyx_CyOrPyCFunction_Check(func) PyCFunction_Check(func)

#if CYTHON_COMPILING_IN_CPYTHON
#define __Pyx_CyOrPyCFunction_GET_FUNCTION(func) (((PyCFunctionObject*)(func))->m_ml->ml_meth)
#elif !CYTHON_COMPILING_IN_LIMITED_API
// It's probably easier for non-CPythons to support PyCFunction_GET_FUNCTION() than the object struct layout.
#define __Pyx_CyOrPyCFunction_GET_FUNCTION(func) PyCFunction_GET_FUNCTION(func)
// Unused in CYTHON_COMPILING_IN_LIMITED_API.
#endif
#if CYTHON_COMPILING_IN_CPYTHON
#define __Pyx_CyOrPyCFunction_GET_FLAGS(func) (((PyCFunctionObject*)(func))->m_ml->ml_flags)
static CYTHON_INLINE PyObject* __Pyx_CyOrPyCFunction_GET_SELF(PyObject *func) {
return (__Pyx_CyOrPyCFunction_GET_FLAGS(func) & METH_STATIC) ? NULL : ((PyCFunctionObject*)func)->m_self;
}
// Only used if CYTHON_COMPILING_IN_CPYTHON.
#endif
static CYTHON_INLINE int __Pyx__IsSameCFunction(PyObject *func, void *cfunc) {
#if CYTHON_COMPILING_IN_LIMITED_API
return PyCFunction_Check(func) && PyCFunction_GetFunction(func) == (PyCFunction) cfunc;
#else
return PyCFunction_Check(func) && PyCFunction_GET_FUNCTION(func) == (PyCFunction) cfunc;
#endif
}
#define __Pyx_IsSameCFunction(func, cfunc) __Pyx__IsSameCFunction(func, cfunc)

// PEP-573: PyCFunction holds reference to defining class (PyCMethodObject)
#if __PYX_LIMITED_VERSION_HEX < 0x030900B1
#define __Pyx_PyType_FromModuleAndSpec(m, s, b) ((void)m, PyType_FromSpecWithBases(s, b))
Expand Down
36 changes: 9 additions & 27 deletions Cython/Utility/ObjectHandling.c
Original file line number Diff line number Diff line change
Expand Up @@ -2017,7 +2017,7 @@ static int __Pyx_TryUnpackUnboundCMethod(__Pyx_CachedCFunction* target) {
// method descriptor type isn't exported in Py2.x, cannot easily check the type there.
// Therefore, reverse the check to the most likely alternative
// (which is returned for class methods)
if (likely(!PyCFunction_Check(method)))
if (likely(!__Pyx_CyOrPyCFunction_Check(method)))
#endif
{
PyMethodDescrObject *descr = (PyMethodDescrObject*) method;
Expand All @@ -2026,11 +2026,8 @@ static int __Pyx_TryUnpackUnboundCMethod(__Pyx_CachedCFunction* target) {
} else
#endif
// bound classmethods need special treatment
#if defined(CYTHON_COMPILING_IN_PYPY)
// In PyPy functions are regular methods, so just do
// the self check
#elif PY_VERSION_HEX >= 0x03090000
if (PyCFunction_CheckExact(method))
#if CYTHON_COMPILING_IN_PYPY
// In PyPy, functions are regular methods, so just do the self check.
#else
if (PyCFunction_Check(method))
#endif
Expand Down Expand Up @@ -2284,27 +2281,12 @@ static CYTHON_INLINE PyObject* __Pyx_PyObject_FastCallDict(PyObject *func, PyObj
Py_ssize_t nargs = __Pyx_PyVectorcall_NARGS(_nargs);
#if CYTHON_COMPILING_IN_CPYTHON
if (nargs == 0 && kwargs == NULL) {
#if defined(__Pyx_CyFunction_USED) && defined(NDEBUG)
// TODO PyCFunction_GET_FLAGS has a type-check assert that breaks with a CyFunction
// in debug mode. There is likely to be a better way of avoiding tripping this
// check that doesn't involve disabling the optimized path.
if (__Pyx_IsCyOrPyCFunction(func))
#else
if (PyCFunction_Check(func))
#endif
{
if (likely(PyCFunction_GET_FLAGS(func) & METH_NOARGS)) {
return __Pyx_PyObject_CallMethO(func, NULL);
}
}
if (__Pyx_CyOrPyCFunction_Check(func) && likely( __Pyx_CyOrPyCFunction_GET_FLAGS(func) & METH_NOARGS))
return __Pyx_PyObject_CallMethO(func, NULL);
}
else if (nargs == 1 && kwargs == NULL) {
if (PyCFunction_Check(func))
{
if (likely(PyCFunction_GET_FLAGS(func) & METH_O)) {
return __Pyx_PyObject_CallMethO(func, args[0]);
}
}
if (__Pyx_CyOrPyCFunction_Check(func) && likely( __Pyx_CyOrPyCFunction_GET_FLAGS(func) & METH_O))
return __Pyx_PyObject_CallMethO(func, args[0]);
}
#endif

Expand Down Expand Up @@ -2465,8 +2447,8 @@ static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethO(PyObject *func, PyObject
static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethO(PyObject *func, PyObject *arg) {
PyObject *self, *result;
PyCFunction cfunc;
cfunc = PyCFunction_GET_FUNCTION(func);
self = PyCFunction_GET_SELF(func);
cfunc = __Pyx_CyOrPyCFunction_GET_FUNCTION(func);
self = __Pyx_CyOrPyCFunction_GET_SELF(func);

if (unlikely(Py_EnterRecursiveCall((char*)" while calling a Python object")))
return NULL;
Expand Down

0 comments on commit ed2933b

Please sign in to comment.