diff --git a/graalpython/com.oracle.graal.python.cext/src/call.c b/graalpython/com.oracle.graal.python.cext/src/call.c index 954f70eed0..b6693f5af5 100644 --- a/graalpython/com.oracle.graal.python.cext/src/call.c +++ b/graalpython/com.oracle.graal.python.cext/src/call.c @@ -929,6 +929,31 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args, assert(args != NULL); assert(PyVectorcall_NARGS(nargsf) >= 1); + /* GraalPy change: Perform the method lookup and call in a single upcall if the receiver is + managed, or if a native extension type inherited GraalPy's managed object.__getattribute__ + slot. If the lookup finds an unbound native method descriptor, the upcall returns the + descriptor instead of invoking it. This keeps native extension method calls in native code + without duplicating managed MRO and instance-dict lookup semantics here. */ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + if (points_to_py_handle_space(args[0]) || + (Py_TYPE(args[0])->tp_getattro == PyBaseObject_Type.tp_getattro && PyUnicode_CheckExact(name))) { + PyObject *result = NULL; + int native_method = GraalPyPrivate_Object_VectorcallMethod(name, args[0], args + 1, + nargs - 1, kwnames, &result); + if (native_method <= 0) { + return result; + } + + /* The receiver is still args[0], so the descriptor can use the original argument array. + Do not expose PY_VECTORCALL_ARGUMENTS_OFFSET: the native callee must not modify the + slot immediately preceding args. */ + nargsf &= ~PY_VECTORCALL_ARGUMENTS_OFFSET; + PyObject *call_result = _PyObject_VectorcallTstate(_PyThreadState_GET(), result, + args, nargsf, kwnames); + Py_DECREF(result); + return call_result; + } + PyThreadState *tstate = _PyThreadState_GET(); PyObject *callable = NULL; /* Use args[0] as "self" argument */ diff --git a/graalpython/com.oracle.graal.python.cext/src/object.c b/graalpython/com.oracle.graal.python.cext/src/object.c index 4f6d29b17d..2a67cf1cf6 100644 --- a/graalpython/com.oracle.graal.python.cext/src/object.c +++ b/graalpython/com.oracle.graal.python.cext/src/object.c @@ -1645,6 +1645,14 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) } } + /* GraalPy change: Native extension types inherit GraalPy's managed + object.__getattribute__ slot rather than PyObject_GenericGetAttr. + Do the LOAD_METHOD-style lookup on the managed side so that method + descriptors stay unbound. */ + if (tp->tp_getattro == PyBaseObject_Type.tp_getattro && PyUnicode_CheckExact(name)) { + return GraalPyPrivate_Object_GetMethod(obj, name, method); + } + if (tp->tp_getattro != PyObject_GenericGetAttr || !PyUnicode_CheckExact(name)) { *method = PyObject_GetAttr(obj, name); return 0; diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py index eedd1c10cd..26829283a7 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_functions.py @@ -147,6 +147,65 @@ class TypeWithAttrInDict: NativeTypeWithAttr.attr = "str" +class VectorcallMethodReceiver: + def combine(self, positional, *, keyword="default"): + return type(self).__name__, positional, keyword + + +NativeTypeWithAttr.combine = VectorcallMethodReceiver.combine + + +NativeVectorcallMethodReceiver = CPyExtHeapType( + "NativeVectorcallMethodReceiver", + code=''' + static PyObject* native_combine(PyObject* self, PyObject* const* args, + Py_ssize_t nargs, PyObject* kwnames) { + PyObject* positional = PyTuple_New(nargs); + if (positional == NULL) { + return NULL; + } + for (Py_ssize_t i = 0; i < nargs; i++) { + PyTuple_SET_ITEM(positional, i, Py_NewRef(args[i])); + } + PyObject* keywords = PyDict_New(); + if (keywords == NULL) { + Py_DECREF(positional); + return NULL; + } + if (kwnames != NULL) { + Py_ssize_t nkw = PyTuple_GET_SIZE(kwnames); + for (Py_ssize_t i = 0; i < nkw; i++) { + if (PyDict_SetItem(keywords, PyTuple_GET_ITEM(kwnames, i), args[nargs + i]) < 0) { + Py_DECREF(positional); + Py_DECREF(keywords); + return NULL; + } + } + } + PyObject* result = PyTuple_Pack(2, positional, keywords); + Py_DECREF(positional); + Py_DECREF(keywords); + return result; + } + + static PyMethodDef native_methods[] = { + {"combine", (PyCFunction)(void(*)(void))native_combine, METH_FASTCALL | METH_KEYWORDS, NULL}, + {NULL, NULL, 0, NULL} + }; + ''', + slots=['{Py_tp_methods, native_methods}'], +) + + +class NativeVectorcallMethodSubclass(NativeVectorcallMethodReceiver): + pass + + +native_vectorcall_method_shadowed = NativeVectorcallMethodSubclass() +native_vectorcall_method_shadowed.combine = lambda positional, *, keyword="default": ( + "shadowed", positional, keyword) + + class DelAttrObject: def __init__(self): self.a = 1 @@ -401,6 +460,43 @@ def __hash__(self): argspec="Oss", callfunction="PyObject_CallMethod" ) + test_PyObject_VectorcallMethod = CPyExtFunction( + lambda args: getattr(args[0], args[1])( + args[2], **({} if args[3] is None else {"keyword": args[3]})), + lambda: ( + (VectorcallMethodReceiver(), "combine", "managed-positional", "managed-keyword"), + (VectorcallMethodReceiver(), "combine", "managed-positional", None), + (NativeTypeWithAttr(), "combine", "native-positional", "native-keyword"), + (NativeTypeWithAttr(), "combine", "native-positional", None), + (NativeVectorcallMethodReceiver(), "combine", "native-method-positional", "native-method-keyword"), + (NativeVectorcallMethodReceiver(), "combine", "native-method-positional", None), + (native_vectorcall_method_shadowed, "combine", "shadowed-positional", "shadowed-keyword"), + ), + code=''' + static PyObject* wrap_PyObject_VectorcallMethod(PyObject* receiver, PyObject* name, + PyObject* positional, PyObject* keyword_value) { + PyObject* args[3] = {receiver, positional, keyword_value}; + PyObject* keyword_names = NULL; + if (keyword_value != Py_None) { + PyObject* keyword_name = PyUnicode_FromString("keyword"); + if (keyword_name == NULL) { + return NULL; + } + keyword_names = PyTuple_Pack(1, keyword_name); + Py_DECREF(keyword_name); + if (keyword_names == NULL) { + return NULL; + } + } + PyObject* result = PyObject_VectorcallMethod(name, args, 2, keyword_names); + Py_XDECREF(keyword_names); + return result; + } + ''', + arguments=["PyObject* receiver", "PyObject* name", "PyObject* positional", "PyObject* keyword_value"], + argspec="OOOO", + callfunction="wrap_PyObject_VectorcallMethod", + ) test_PyObject_Type = CPyExtFunction( type, lambda: ( diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_object.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_object.py index 8f05308339..43240045a9 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_object.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_object.py @@ -92,6 +92,135 @@ def __getattribute__(self, key): class TestObject(unittest.TestCase): + def test_get_method_keeps_method_descriptors_unbound(self): + module = compile_module_from_string( + """ + #include + + PyAPI_FUNC(int) _PyObject_GetMethod(PyObject *, PyObject *, PyObject **); + + static PyObject* native_descriptor_call(PyObject* self, PyObject* args, PyObject* kwargs) { + if (PyTuple_GET_SIZE(args) != 1 || (kwargs != NULL && PyDict_GET_SIZE(kwargs) != 0)) { + PyErr_SetString(PyExc_TypeError, "expected exactly one positional argument"); + return NULL; + } + return Py_NewRef(PyTuple_GET_ITEM(args, 0)); + } + + static PyObject* native_descriptor_get(PyObject* self, PyObject* obj, PyObject* type) { + if (obj == NULL || obj == Py_None) { + return Py_NewRef(self); + } + return PyMethod_New(self, obj); + } + + static PyTypeObject NativeMethodDescriptor_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "test_get_method.NativeMethodDescriptor", + .tp_basicsize = sizeof(PyObject), + .tp_call = native_descriptor_call, + .tp_descr_get = native_descriptor_get, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_METHOD_DESCRIPTOR, + }; + + static PyObject* get_method(PyObject* self, PyObject* args) { + PyObject* obj; + PyObject* name; + if (!PyArg_ParseTuple(args, "OO:get_method", &obj, &name)) { + return NULL; + } + PyObject* method = NULL; + int is_method = _PyObject_GetMethod(obj, name, &method); + if (method == NULL) { + return NULL; + } + PyObject* result = PyTuple_New(2); + if (result == NULL) { + Py_DECREF(method); + return NULL; + } + PyTuple_SET_ITEM(result, 0, PyBool_FromLong(is_method)); + PyTuple_SET_ITEM(result, 1, method); + return result; + } + + static PyMethodDef methods[] = { + {"get_method", get_method, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL} + }; + + static struct PyModuleDef module = { + PyModuleDef_HEAD_INIT, "test_get_method", NULL, -1, methods + }; + + PyMODINIT_FUNC PyInit_test_get_method(void) { + if (PyType_Ready(&NativeMethodDescriptor_Type) < 0) { + return NULL; + } + PyObject* m = PyModule_Create(&module); + if (m == NULL) { + return NULL; + } + PyObject* descriptor = NativeMethodDescriptor_Type.tp_alloc(&NativeMethodDescriptor_Type, 0); + if (descriptor == NULL || PyModule_AddObject(m, "native_method_descriptor", descriptor) < 0) { + Py_XDECREF(descriptor); + Py_DECREF(m); + return NULL; + } + return m; + } + """, + "test_get_method", + ) + + class C: + def method(self): + return self + + obj = C() + is_method, method = module.get_method(obj, "method") + assert is_method is True + assert method(obj) is obj + + Native = CPyExtType( + "GetMethodNative", + """ + static PyObject* get_self(PyObject* self, PyObject* unused) { + return Py_NewRef(self); + } + """, + tp_methods='{"get_self", (PyCFunction)get_self, METH_NOARGS, ""}', + ) + native_obj = Native() + is_method, method = module.get_method(native_obj, "get_self") + assert is_method is True + assert method(native_obj) is native_obj + + class WithNativeMethodDescriptor: + native_method_descriptor = module.native_method_descriptor + + native_descriptor_obj = WithNativeMethodDescriptor() + is_method, method = module.get_method(native_descriptor_obj, "native_method_descriptor") + assert is_method is True + assert method is module.native_method_descriptor + assert method(native_descriptor_obj) is native_descriptor_obj + assert native_descriptor_obj.native_method_descriptor() is native_descriptor_obj + + obj.method = lambda: "instance attribute" + is_method, method = module.get_method(obj, "method") + assert is_method is False + assert method() == "instance attribute" + + class WithGetAttribute: + def __getattribute__(self, name): + if name == "method": + return lambda: "custom getattribute" + return object.__getattribute__(self, name) + + is_method, method = module.get_method(WithGetAttribute(), "method") + assert is_method is False + assert method() == "custom getattribute" + def test_iter_dict_before_getattr_property(self): module = compile_module_from_string( """ diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_tp_slots.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_tp_slots.py index 2b6132af44..da45148006 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_tp_slots.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_tp_slots.py @@ -1670,6 +1670,57 @@ def __call__(self, *args, **kwargs): assert obj.__call__({'b': 2}, a=1) == {'a': 1, 'b': 2} +def test_native_vectorcall(): + NativeVectorcall = CPyExtType( + name='NativeVectorcall', + cmembers='vectorcallfunc vectorcall;', + code=r''' + static PyObject* native_vectorcall(PyObject* self, PyObject* const* args, size_t nargsf, PyObject* kwnames) { + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + Py_ssize_t nkwargs = kwnames ? PyTuple_GET_SIZE(kwnames) : 0; + PyObject* positional = PyTuple_New(nargs); + PyObject* keywords = PyDict_New(); + if (!positional || !keywords) { + Py_XDECREF(positional); + Py_XDECREF(keywords); + return NULL; + } + for (Py_ssize_t i = 0; i < nargs; i++) { + Py_INCREF(args[i]); + PyTuple_SET_ITEM(positional, i, args[i]); + } + for (Py_ssize_t i = 0; i < nkwargs; i++) { + if (PyDict_SetItem(keywords, PyTuple_GET_ITEM(kwnames, i), args[nargs + i]) < 0) { + Py_DECREF(positional); + Py_DECREF(keywords); + return NULL; + } + } + return Py_BuildValue("NN", positional, keywords); + } + + static PyObject* native_tp_call(PyObject* self, PyObject* args, PyObject* kwargs) { + return PyUnicode_FromString("tp_call"); + } + + static PyObject* native_vectorcall_new(PyTypeObject* type, PyObject* args, PyObject* kwargs) { + NativeVectorcallObject* self = (NativeVectorcallObject*)type->tp_alloc(type, 0); + if (self) { + self->vectorcall = native_vectorcall; + } + return (PyObject*)self; + } + ''', + tp_flags='Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_VECTORCALL', + tp_vectorcall_offset='offsetof(NativeVectorcallObject, vectorcall)', + tp_call='native_tp_call', + tp_new='native_vectorcall_new', + ) + + callable_obj = NativeVectorcall() + assert callable_obj(1, 2, keyword=3) == ((1, 2), {'keyword': 3}) + + def test_richcmp(): MyNativeIntSubType = CPyExtType("MyNativeIntSubTypeForRichCmpTest", ready_code = "MyNativeIntSubTypeForRichCmpTestType.tp_new = PyLong_Type.tp_new;", diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java index 398655635e..a847405177 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java @@ -50,6 +50,7 @@ import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Int; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Pointer; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObject; +import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectAsTruffleString; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectConstPtr; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectPtr; import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectRawPointer; @@ -65,12 +66,14 @@ import static com.oracle.graal.python.nodes.SpecialMethodNames.T___BYTES__; import static com.oracle.graal.python.nodes.StringLiterals.T_JAVA; import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.NULLPTR; +import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.POINTER_SIZE; import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.readPtrArrayElement; import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.writePtr; import static com.oracle.graal.python.util.PythonUtils.TS_ENCODING; import java.io.PrintWriter; import java.lang.ref.Reference; +import java.util.logging.Level; import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.builtins.PythonBuiltinClassType; @@ -88,6 +91,8 @@ import com.oracle.graal.python.builtins.objects.bytes.BytesNodes; import com.oracle.graal.python.builtins.objects.bytes.BytesUtils; import com.oracle.graal.python.builtins.objects.bytes.PBytes; +import com.oracle.graal.python.builtins.objects.cext.PythonAbstractNativeObject; +import com.oracle.graal.python.builtins.objects.cext.capi.CApiContext; import com.oracle.graal.python.builtins.objects.cext.capi.CExtNodes.EnsurePythonObjectNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTiming; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions; @@ -119,6 +124,8 @@ import com.oracle.graal.python.lib.PyObjectGetAttr; import com.oracle.graal.python.lib.PyObjectGetAttrO; import com.oracle.graal.python.lib.PyObjectGetIter; +import com.oracle.graal.python.lib.PyObjectGetMethod; +import com.oracle.graal.python.lib.PyObjectGetMethodNodeGen; import com.oracle.graal.python.lib.PyObjectHashNode; import com.oracle.graal.python.lib.PyObjectIsInstanceNode; import com.oracle.graal.python.lib.PyObjectIsSubclassNode; @@ -136,6 +143,8 @@ import com.oracle.graal.python.nodes.StringLiterals; import com.oracle.graal.python.nodes.argument.keywords.ExpandKeywordStarargsNode; import com.oracle.graal.python.nodes.builtins.TupleNodes.GetTupleStorage; +import com.oracle.graal.python.nodes.builtins.TupleNodesFactory.GetTupleStorageNodeGen; +import com.oracle.graal.python.nodes.call.BoundDescriptor; import com.oracle.graal.python.nodes.call.CallNode; import com.oracle.graal.python.nodes.call.special.CallUnaryMethodNode; import com.oracle.graal.python.nodes.call.special.LookupSpecialMethodNode; @@ -156,6 +165,7 @@ import com.oracle.graal.python.runtime.sequence.storage.SequenceStorage; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; @@ -287,6 +297,61 @@ static Object doMethod(Object receiver, Object methodName, long argsArray, long } } + @CApiBuiltin(ret = Int, args = {PyObjectAsTruffleString, PyObject, PyObjectConstPtr, Py_ssize_t, PyObject, PyObjectPtr}, call = Ignored) + static int GraalPyPrivate_Object_VectorcallMethod(long methodNamePtr, long receiverPtr, long argsArray, long nargs, long kwnamesPtr, long resultPointer) { + TruffleString methodName = CastToTruffleStringNode.castKnownStringUncached(NativeToPythonInternalNode.executeUncached(methodNamePtr, false)); + Object receiver = NativeToPythonInternalNode.executeUncached(receiverPtr, false); + long resultPointerValue = NULLPTR; + try { + Object callable = PyObjectGetMethodNodeGen.getUncached().execute(null, null, receiver, methodName); + boolean isBound = callable instanceof BoundDescriptor; + if (!isBound && callable instanceof PythonAbstractNativeObject) { + resultPointerValue = PythonToNativeInternalNode.executeNewRefUncached(callable); + return 1; + } + Object[] args = readArguments(argsArray, (int) nargs, receiver, isBound); + if (isBound) { + callable = ((BoundDescriptor) callable).descriptor; + } + + PKeyword[] keywords; + if (kwnamesPtr == NULLPTR) { + keywords = PKeyword.EMPTY_KEYWORDS; + } else { + Object keywordNames = NativeToPythonInternalNode.executeUncached(kwnamesPtr, false); + SequenceStorage storage = GetTupleStorageNodeGen.getUncached().execute(null, keywordNames); + int kwcount = storage.length(); + Object[] kwValues = CStructAccess.ReadObjectNode.getUncached().readPyObjectArray(argsArray, kwcount, (int) nargs); + keywords = new PKeyword[kwcount]; + for (int i = 0; i < kwcount; i++) { + TruffleString name = CastToTruffleStringNode.executeUncached(SequenceStorageNodes.GetItemScalarNode.executeUncached(storage, i)); + keywords[i] = new PKeyword(name, kwValues[i]); + } + } + Object result = CallNode.executeUncached(callable, args, keywords); + resultPointerValue = PythonToNativeInternalNode.executeNewRefUncached(result); + return 0; + } catch (CannotCastException e) { + // PyVectorcall_NARGS cannot exceed Py_ssize_t, and GraalPy arrays are int-sized. + throw CompilerDirectives.shouldNotReachHere(e); + } finally { + writePtr(resultPointer, resultPointerValue); + } + } + + private static Object[] readArguments(long argsArray, int nargs, + Object receiver, boolean isBound) { + if (isBound) { + return CStructAccess.ReadObjectNode.getUncached().readPyObjectArray(argsArray, nargs); + } + Object[] args = new Object[nargs + 1]; + args[0] = receiver; + for (int i = 0; i < nargs; i++) { + args[i + 1] = CStructAccess.ReadObjectNode.getUncached().read(argsArray, i * POINTER_SIZE); + } + return args; + } + @CApiBuiltin(ret = PyObjectTransfer, args = {PyObject, ConstCharPtrAsTruffleString, PyObject, Int}, call = Ignored) abstract static class GraalPyPrivate_Object_CallMethod1 extends CApiQuaternaryBuiltinNode { @Specialization @@ -309,6 +374,8 @@ static Object doGeneric(Object receiver, TruffleString methodName, Object argsOb @CApiBuiltin(ret = PyObjectTransfer, args = {PyThreadState, PyObject, PyObjectConstPtr, Py_ssize_t, PyObject}, call = Direct) abstract static class _PyObject_MakeTpCall extends CApi5BuiltinNode { + private static final TruffleLogger LOGGER = CApiContext.getLogger(_PyObject_MakeTpCall.class); + @Specialization static Object doGeneric(@SuppressWarnings("unused") long threadState, Object callable, long argsArray, long nargs, Object kwargs, @Cached CStructAccess.ReadObjectNode readNode, @@ -321,6 +388,9 @@ static Object doGeneric(@SuppressWarnings("unused") long threadState, Object cal @Cached PyTupleCheckNode tupleCheckNode, @Cached CastToTruffleStringNode castToTruffleStringNode) { try { + if (LOGGER.isLoggable(Level.FINE)) { + logCallableType(callable); + } Object[] args = readNode.readPyObjectArray(argsArray, (int) nargs); PKeyword[] keywords; @@ -349,6 +419,12 @@ static Object doGeneric(@SuppressWarnings("unused") long threadState, Object cal throw CompilerDirectives.shouldNotReachHere(e); } } + + @TruffleBoundary + private static void logCallableType(Object callable) { + Object type = GetClassNode.executeUncached(callable); + LOGGER.fine(TypeNodes.GetNameNode.executeUncached(type).toJavaStringUncached()); + } } @CApiBuiltin(ret = PyObjectTransfer, args = {PyObject}, call = Direct) @@ -487,6 +563,32 @@ static int getOptionalAttr(Object obj, Object attr, long resultPointer, } } + @CApiBuiltin(ret = Int, args = {PyObject, PyObjectAsTruffleString, PyObjectPtr}, call = Ignored) + abstract static class GraalPyPrivate_Object_GetMethod extends CApiTernaryBuiltinNode { + @Specialization + static int getMethod(Object receiver, TruffleString name, long resultPointer, + @Bind Node inliningTarget, + @Bind PythonContext context, + @Cached PyObjectGetMethod getMethodNode, + @Cached EnsurePythonObjectNode ensureNode, + @Cached PythonToNativeInternalNode toNativeNode, + @Cached InlinedConditionProfile isBoundProfile) { + long resultPointerValue = NULLPTR; + try { + Object result = getMethodNode.execute(null, inliningTarget, receiver, name); + boolean isBound = isBoundProfile.profile(inliningTarget, result instanceof BoundDescriptor); + if (isBound) { + result = ((BoundDescriptor) result).descriptor; + } + resultPointerValue = toNativeNode.executeNewRef(inliningTarget, + ensureNode.execute(context, result, false)); + return isBound ? 0 : 1; + } finally { + writePtr(resultPointer, resultPointerValue); + } + } + } + @CApiBuiltin(ret = Int, args = {PyObject, ConstCharPtr, PyObjectPtr}, call = Ignored) abstract static class GraalPyPrivate_Object_GetOptionalAttrString extends CApiTernaryBuiltinNode { @Specialization diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionNodes.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionNodes.java index 4d05162c15..f3f18b0b54 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionNodes.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionNodes.java @@ -2374,6 +2374,7 @@ static long doGeneric(PythonContext context, Object[] args, * instances. */ @GenerateInline(false) + @GenerateUncached public abstract static class CreateNativeKwNamesTupleNode extends Node { public abstract long execute(PythonContext context, Object[] args); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java index 53d9624e82..25fa86bf80 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java @@ -254,7 +254,9 @@ public enum ExternalFunctionSignature implements NativeCExtSymbol { // TODO(fa): should be an implicit signature GCCOLLECT(false, Py_ssize_t, Int), - GETDICTPTRFUN(true, Pointer, PyObject); + GETDICTPTRFUN(true, Pointer, PyObject), + // typedef PyObject *(*vectorcallfunc)(PyObject *, PyObject *const *, size_t, PyObject *); + VECTORCALL(false, PyObjectReturn, PyObject, Pointer, Py_ssize_t, PyObject); public final ArgDescriptor returnValue; public final ArgDescriptor[] arguments; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/slots/TpSlotVarargs.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/slots/TpSlotVarargs.java index 5fdd80f32a..5b307050fc 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/slots/TpSlotVarargs.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/slots/TpSlotVarargs.java @@ -41,11 +41,16 @@ package com.oracle.graal.python.builtins.objects.type.slots; import static com.oracle.graal.python.builtins.objects.PNone.NO_VALUE; +import static com.oracle.graal.python.builtins.objects.cext.structs.CFields.PyObject__ob_type; +import static com.oracle.graal.python.builtins.objects.cext.structs.CFields.PyTypeObject__tp_flags; +import static com.oracle.graal.python.builtins.objects.cext.structs.CFields.PyTypeObject__tp_vectorcall_offset; +import static com.oracle.graal.python.builtins.objects.type.TypeFlags.HAVE_VECTORCALL; import static com.oracle.graal.python.nodes.SpecialMethodNames.J___NEW__; import static com.oracle.graal.python.nodes.SpecialMethodNames.T___CALL__; import static com.oracle.graal.python.nodes.SpecialMethodNames.T___INIT__; import static com.oracle.graal.python.nodes.SpecialMethodNames.T___NEW__; import static com.oracle.graal.python.runtime.exception.PythonErrorType.TypeError; +import static com.oracle.graal.python.runtime.nativeaccess.NativeMemory.NULLPTR; import java.lang.ref.Reference; @@ -55,9 +60,11 @@ import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.PythonBuiltins; import com.oracle.graal.python.builtins.objects.PNone; +import com.oracle.graal.python.builtins.objects.cext.PythonNativeObject; import com.oracle.graal.python.builtins.objects.cext.capi.CExtNodes.EnsurePythonObjectNode; import com.oracle.graal.python.builtins.objects.cext.capi.ExternalFunctionInvoker; import com.oracle.graal.python.builtins.objects.cext.capi.ExternalFunctionNodes.CreateNativeArgsTupleNode; +import com.oracle.graal.python.builtins.objects.cext.capi.ExternalFunctionNodes.CreateNativeKwNamesTupleNode; import com.oracle.graal.python.builtins.objects.cext.capi.ExternalFunctionNodes.EagerTupleState; import com.oracle.graal.python.builtins.objects.cext.capi.ExternalFunctionNodes.PExternalFunctionWrapper; import com.oracle.graal.python.builtins.objects.cext.capi.ExternalFunctionNodes.PyObjectCheckFunctionResultNode; @@ -66,6 +73,8 @@ import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonInternalNode; import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonToNativeInternalNode; import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.TransformExceptionFromNativeNode; +import com.oracle.graal.python.builtins.objects.cext.structs.CStructAccess; +import com.oracle.graal.python.builtins.objects.function.PArguments; import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction; import com.oracle.graal.python.builtins.objects.function.PFunction; import com.oracle.graal.python.builtins.objects.function.PKeyword; @@ -99,13 +108,17 @@ import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.PythonVarargsBuiltinNode; import com.oracle.graal.python.nodes.object.GetClassNode; +import com.oracle.graal.python.runtime.ExecutionContext.BoundaryCallContext; +import com.oracle.graal.python.runtime.GilNode; import com.oracle.graal.python.runtime.IndirectCallData.BoundaryCallData; import com.oracle.graal.python.runtime.PythonContext; import com.oracle.graal.python.runtime.PythonContext.GetThreadStateNode; import com.oracle.graal.python.runtime.PythonContext.PythonThreadState; +import com.oracle.graal.python.runtime.nativeaccess.NativeMemory; import com.oracle.graal.python.runtime.object.PFactory; import com.oracle.graal.python.util.PythonUtils; import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.HostCompilerDirectives.InliningCutoff; import com.oracle.truffle.api.RootCallTarget; @@ -124,7 +137,9 @@ import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.LoopNode; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.profiles.InlinedConditionProfile; import com.oracle.truffle.api.strings.TruffleString; public final class TpSlotVarargs { @@ -314,16 +329,109 @@ static Object callPython(VirtualFrame frame, Node inliningTarget, TpSlotPythonSi } } - private static final CApiTiming C_API_TIMING = CApiTiming.create(true, ""); + private static final CApiTiming C_API_TIMING_TP_CALL = CApiTiming.create(true, "tp_call"); + private static final CApiTiming C_API_TIMING_TP_INIT = CApiTiming.create(true, "tp_init"); + private static final CApiTiming C_API_TIMING_TP_NEW = CApiTiming.create(true, "tp_new"); + private static final CApiTiming C_API_TIMING_VECTORCALL = CApiTiming.create(true, "vectorcall"); + + @GenerateInline(false) + @GenerateUncached + abstract static class CallNativeVectorcallNode extends Node { + + abstract Object execute(VirtualFrame frame, Object callable, long vectorcallPointer, Object[] args, PKeyword[] keywords); + + @Specialization + static Object callNative(VirtualFrame frame, Object callable, long vectorcallPointer, Object[] args, PKeyword[] keywords, + @Bind Node inliningTarget, + @Bind PythonContext context, + @Cached GetThreadStateNode getThreadStateNode, + @Cached EnsurePythonObjectNode ensurePythonObjectNode, + @Cached PythonToNativeInternalNode toNativeNode, + @Cached CreateNativeKwNamesTupleNode createNativeKwNamesTupleNode, + @Cached ReleaseNativeArgsTupleNode releaseNativeKwNamesTupleNode, + @Cached NativeToPythonInternalNode toPythonNode, + @Cached PyObjectCheckFunctionResultNode checkResultNode, + @Cached("createFor($node)") BoundaryCallData boundaryCallData) { + PythonThreadState threadState = getThreadStateNode.execute(inliningTarget, context); + Object promotedCallable = ensurePythonObjectNode.execute(context, callable, false); + Object[] fastcallArgs = new Object[args.length + keywords.length]; + for (int i = 0; i < args.length; i++) { + fastcallArgs[i] = ensurePythonObjectNode.execute(context, args[i], false); + } + LoopNode.reportLoopCount(inliningTarget, args.length); + + long nativeArgs = NULLPTR; + Object[] keywordNames = null; + long keywordNamesPtr = NULLPTR; + try { + if (keywords.length > 0) { + keywordNames = new Object[keywords.length]; + for (int i = 0; i < keywords.length; i++) { + keywordNames[i] = keywords[i].getName(); + fastcallArgs[args.length + i] = ensurePythonObjectNode.execute(context, keywords[i].getValue(), false); + } + LoopNode.reportLoopCount(inliningTarget, keywordNames.length); + keywordNamesPtr = createNativeKwNamesTupleNode.execute(context, keywordNames); + } + nativeArgs = createNativeArguments(fastcallArgs, toNativeNode, inliningTarget); + + long nativeResult = invokeVectorcall(frame, vectorcallPointer, context, boundaryCallData, threadState, + toNativeNode.execute(inliningTarget, promotedCallable), nativeArgs, args.length, keywordNamesPtr); + return checkResultNode.execute(threadState, T___CALL__, toPythonNode.executeTransferAndRelease(inliningTarget, nativeResult)); + } finally { + if (nativeArgs != NULLPTR) { + NativeMemory.free(nativeArgs); + } + if (keywordNames != null) { + releaseNativeKwNamesTupleNode.execute(keywordNamesPtr, keywordNames); + } + Reference.reachabilityFence(promotedCallable); + Reference.reachabilityFence(fastcallArgs); + Reference.reachabilityFence(keywordNames); + } + } + + private static long createNativeArguments(Object[] args, PythonToNativeInternalNode toNativeNode, Node inliningTarget) { + if (args.length == 0) { + return NULLPTR; + } + long pointer = NativeMemory.mallocPtrArray(args.length); + for (int i = 0; i < args.length; i++) { + assert EnsurePythonObjectNode.doesNotNeedPromotion(args[i]); + NativeMemory.writePtrArrayElement(pointer, i, toNativeNode.execute(inliningTarget, args[i])); + } + LoopNode.reportLoopCount(inliningTarget, args.length); + return pointer; + } + + private static long invokeVectorcall(VirtualFrame frame, long vectorcallPointer, PythonContext context, BoundaryCallData boundaryCallData, PythonThreadState threadState, + long callable, long args, long nargs, long keywordNames) { + Object state = BoundaryCallContext.enter(frame, threadState, boundaryCallData); + CApiTiming.enter(); + try { + return ExternalFunctionInvoker.invokeVECTORCALL(vectorcallPointer, callable, args, nargs, keywordNames); + } catch (Throwable exception) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + GilNode.uncachedAcquire(); + throw CompilerDirectives.shouldNotReachHere(exception); + } finally { + CApiTiming.exit(C_API_TIMING_VECTORCALL); + if (frame != null && threadState.getCaughtException() != null) { + PArguments.setException(frame, threadState.getCaughtException()); + } + BoundaryCallContext.exit(frame, threadState, state); + } + } + } @GenerateInline(false) @GenerateUncached abstract static class CallNativeTernaryfuncNode extends Node { - abstract Object execute(VirtualFrame frame, TpSlotCExtNative slot, Object self, Object[] args, PKeyword[] keywords, TruffleString name); + abstract Object execute(VirtualFrame frame, TpSlotCExtNative slot, Object self, Object[] args, PKeyword[] keywords, TruffleString name, CApiTiming timing); @Specialization - static Object callNative(VirtualFrame frame, TpSlotCExtNative slot, Object self, Object[] args, PKeyword[] keywords, TruffleString name, + static Object callNative(VirtualFrame frame, TpSlotCExtNative slot, Object self, Object[] args, PKeyword[] keywords, TruffleString name, CApiTiming timing, @Bind Node inliningTarget, @Bind PythonContext context, @Cached GetThreadStateNode getThreadStateNode, @@ -351,7 +459,7 @@ static Object callNative(VirtualFrame frame, TpSlotCExtNative slot, Object self, Object kwargsDict = keywords.length > 0 ? PFactory.createDict(language, keywords) : NO_VALUE; assert EnsurePythonObjectNode.doesNotNeedPromotion(kwargsDict); try { - long nativeResult = ExternalFunctionInvoker.invokeTERNARYFUNC(frame, C_API_TIMING, context.ensureNativeContext(), boundaryCallData, state, slot.callable, + long nativeResult = ExternalFunctionInvoker.invokeTERNARYFUNC(frame, timing, context.ensureNativeContext(), boundaryCallData, state, slot.callable, toNativeNode.execute(inliningTarget, promotedSelf), argsTuplePtr, toNativeNode.execute(inliningTarget, kwargsDict)); @@ -419,7 +527,7 @@ static Object callNative(VirtualFrame frame, Node inliningTarget, TpSlotCExtNati Object kwargsDict = keywords.length > 0 ? PFactory.createDict(language, keywords) : NO_VALUE; assert EnsurePythonObjectNode.doesNotNeedPromotion(kwargsDict); try { - int nativeResult = ExternalFunctionInvoker.invokeINITPROC(frame, C_API_TIMING, context.ensureNativeContext(), boundaryCallData, state, slot.callable, + int nativeResult = ExternalFunctionInvoker.invokeINITPROC(frame, C_API_TIMING_TP_INIT, context.ensureNativeContext(), boundaryCallData, state, slot.callable, toNativeNode.execute(inliningTarget, promotedSelf), argsTuplePtr, toNativeNode.execute(inliningTarget, kwargsDict)); @@ -512,7 +620,7 @@ static boolean hasDefaultMetatype(Node inliningTarget, Object self, GetClassNode static Object callNative(VirtualFrame frame, TpSlotCExtNative slot, Object self, Object[] args, PKeyword[] keywords, @Cached CallNativeTernaryfuncNode callNode) { // 'tp_new' slot's C type is actually 'newfunc' but that is compatible to 'ternaryfunc'. - return callNode.execute(frame, slot, self, args, keywords, T___NEW__); + return callNode.execute(frame, slot, self, args, keywords, T___NEW__, C_API_TIMING_TP_NEW); } } @@ -535,9 +643,29 @@ static Object callPython(VirtualFrame frame, Node inliningTarget, TpSlotPythonSi @Specialization @InliningCutoff - static Object callNative(VirtualFrame frame, TpSlotCExtNative slot, Object self, Object[] args, PKeyword[] keywords, + static Object callNative(VirtualFrame frame, Node inliningTarget, TpSlotCExtNative slot, Object self, Object[] args, PKeyword[] keywords, + @Cached InlinedConditionProfile vectorcallProfile, + @Cached CallNativeVectorcallNode callVectorcallNode, @Cached CallNativeTernaryfuncNode callNode) { - return callNode.execute(frame, slot, self, args, keywords, T___CALL__); + if (self instanceof PythonNativeObject nativeObject) { + long vectorcallPointer = getVectorcallPointer(nativeObject); + if (vectorcallProfile.profile(inliningTarget, vectorcallPointer != NULLPTR)) { + return callVectorcallNode.execute(frame, self, vectorcallPointer, args, keywords); + } + } + return callNode.execute(frame, slot, self, args, keywords, T___CALL__, C_API_TIMING_TP_CALL); + } + + /** Reads the vectorcall pointer without resolving any native object or its type. */ + private static long getVectorcallPointer(PythonNativeObject callable) { + long callablePtr = callable.getPtr(); + long obType = CStructAccess.readPtrField(callablePtr, PyObject__ob_type); + if ((CStructAccess.readLongField(obType, PyTypeObject__tp_flags) & HAVE_VECTORCALL) == 0) { + return NULLPTR; + } + long vectorcallOffset = CStructAccess.readLongField(obType, PyTypeObject__tp_vectorcall_offset); + assert vectorcallOffset > 0; + return NativeMemory.readPtr(callablePtr + vectorcallOffset); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyObjectGetMethod.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyObjectGetMethod.java index 3c38a181ec..0735ebd7fa 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyObjectGetMethod.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyObjectGetMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -43,10 +43,13 @@ import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.objects.PNone; import com.oracle.graal.python.builtins.objects.PythonAbstractObject; +import com.oracle.graal.python.builtins.objects.cext.PythonAbstractNativeObject; import com.oracle.graal.python.builtins.objects.object.ObjectBuiltins; import com.oracle.graal.python.builtins.objects.type.TpSlots; import com.oracle.graal.python.builtins.objects.type.TpSlots.GetCachedTpSlotsNode; import com.oracle.graal.python.builtins.objects.type.TpSlots.GetObjectSlotsNode; +import com.oracle.graal.python.builtins.objects.type.TypeFlags; +import com.oracle.graal.python.builtins.objects.type.TypeNodes.GetTypeFlagsNode; import com.oracle.graal.python.builtins.objects.type.slots.TpSlot; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.CallSlotDescrGet; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrSet; @@ -110,6 +113,7 @@ static Object getAttr(VirtualFrame frame, Node inliningTarget, Object receiver, @Cached GetCachedTpSlotsNode getTypeSlotsNode, @Bind("getClass.execute(inliningTarget, receiver)") Object lazyClass, @Cached LookupAttributeInMRONode.Dynamic lookupNode, + @Cached(inline = false) GetTypeFlagsNode getTypeFlagsNode, @Cached GetObjectSlotsNode getSlotsNode, @Cached CallSlotDescrGet callGetNode, @Cached ReadAttributeFromObjectNode readAttr, @@ -124,7 +128,7 @@ static Object getAttr(VirtualFrame frame, Node inliningTarget, Object receiver, TpSlot getMethod = null; if (descr != PNone.NO_VALUE) { hasDescr.enter(inliningTarget); - if (MaybeBindDescriptorNode.isMethodDescriptor(descr)) { + if (isMethodDescriptor(inliningTarget, descr, getClass, getTypeFlagsNode)) { methodFound = true; } else { // lookupGet acts as branch profile for this branch @@ -159,6 +163,18 @@ static Object getAttr(VirtualFrame frame, Node inliningTarget, Object receiver, throw raiseNode.raiseAttributeError(inliningTarget, ErrorMessages.OBJ_P_HAS_NO_ATTR_S, receiver, name); } + private static boolean isMethodDescriptor(Node inliningTarget, Object descriptor, GetClassNode getClassNode, + GetTypeFlagsNode getTypeFlagsNode) { + if (MaybeBindDescriptorNode.isMethodDescriptor(descriptor)) { + return true; + } + if (descriptor instanceof PythonAbstractNativeObject) { + Object descriptorType = getClassNode.execute(inliningTarget, descriptor); + return (getTypeFlagsNode.execute(descriptorType) & TypeFlags.METHOD_DESCRIPTOR) != 0; + } + return false; + } + @Specialization(guards = "isForeignObject(inliningTarget, isForeignObjectNode, receiver)", limit = "1") @InliningCutoff static Object getForeignMethod(VirtualFrame frame, Node inliningTarget, Object receiver, TruffleString name, diff --git a/graalpython/lib-graalpython/patches/cython-3.2.4.patch b/graalpython/lib-graalpython/patches/cython-3.2.4.patch index 2fa5ffd2a9..1307d88a10 100644 --- a/graalpython/lib-graalpython/patches/cython-3.2.4.patch +++ b/graalpython/lib-graalpython/patches/cython-3.2.4.patch @@ -256,6 +256,24 @@ index 8131c86..aa67eda 100644 #endif ((__pyx_CyFunctionObject *) func)->flags, ((__pyx_CyFunctionObject *) func)->func_qualname, +diff --git a/Cython/Utility/ModuleSetupCode.c b/Cython/Utility/ModuleSetupCode.c +index ddaffbd..c8e7962 100644 +--- a/Cython/Utility/ModuleSetupCode.c ++++ b/Cython/Utility/ModuleSetupCode.c +@@ -96,9 +96,11 @@ + #undef CYTHON_FAST_GIL + #define CYTHON_FAST_GIL 0 + #undef CYTHON_METH_FASTCALL +- #define CYTHON_METH_FASTCALL 0 ++ #define CYTHON_METH_FASTCALL 1 + #undef CYTHON_FAST_PYCALL +- #define CYTHON_FAST_PYCALL 0 ++ #define CYTHON_FAST_PYCALL 1 ++ #undef CYTHON_FAST_PYCCALL ++ #define CYTHON_FAST_PYCCALL 1 + #ifndef CYTHON_PEP487_INIT_SUBCLASS + #define CYTHON_PEP487_INIT_SUBCLASS 1 + #endif diff --git a/Cython/Utility/Optimize.c b/Cython/Utility/Optimize.c index b731f74..300758b 100644 --- a/Cython/Utility/Optimize.c