Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions graalpython/com.oracle.graal.python.cext/src/call.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
8 changes: 8 additions & 0 deletions graalpython/com.oracle.graal.python.cext/src/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Python.h>

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(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;",
Expand Down
Loading
Loading