Skip to content
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

Implement custom MRO for PyJType. #407

Merged
merged 2 commits into from
Jun 30, 2022
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
141 changes: 140 additions & 1 deletion src/main/c/Objects/pyjtype.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

#include "Jep.h"

static PyTypeObject PyJType_Type;

static PyTypeObject* pyjtype_get_cached(JNIEnv*, PyObject*, jclass);
static int addMethods(JNIEnv*, PyObject*, jclass);

Expand Down Expand Up @@ -346,11 +348,18 @@ static int addFields(JNIEnv* env, PyObject* dict, jclass clazz)
static PyTypeObject* pyjtype_get_new(JNIEnv *env, PyObject *fqnToPyType,
PyObject *typeName, jclass clazz)
{

if (!(*env)->IsAssignableFrom(env, clazz, JOBJECT_TYPE)) {
PyErr_Format(PyExc_TypeError, "Cannot create a pyjtype for primitive type: %s",
PyUnicode_AsUTF8(typeName));
return NULL;
}
if (!PyJType_Type.tp_base) {
PyJType_Type.tp_base = &PyType_Type;
}
if (PyType_Ready(&PyJType_Type) < 0) {
return NULL;
}

/* The Python types for the Java super class and any interfaces. */
PyObject* bases = getBaseTypes(env, fqnToPyType, clazz);
Expand Down Expand Up @@ -386,7 +395,7 @@ static PyTypeObject* pyjtype_get_new(JNIEnv *env, PyObject *fqnToPyType,
* type(shortName, bases, dict) in python.
* See https://docs.python.org/3/library/functions.html#type
*/
type = (PyTypeObject*) PyObject_CallFunctionObjArgs((PyObject*) &PyType_Type,
type = (PyTypeObject*) PyObject_CallFunctionObjArgs((PyObject*) &PyJType_Type,
shortName, bases, dict, NULL);
}
Py_DECREF(bases);
Expand Down Expand Up @@ -446,3 +455,133 @@ PyTypeObject* PyJType_Get(JNIEnv *env, jclass clazz)
Py_DECREF(fqnToPyType);
return result;
}

/*
* Given a base type for a class, merge the mro from the base into the mro list
* for a new type. The mro entries from the base type that are not in the list
* will be appended to the list.
*
* Returns 0 on success. Returns -1 and sets an exception if an error occurs.
*/
static int merge_mro(PyTypeObject* base, PyObject* mro_list)
{
PyObject* base_mro = base->tp_mro;
if (!base_mro || !PyTuple_Check(base_mro)) {
PyErr_SetString(PyExc_TypeError, "Invalid mro in base type.");
return -1;
}
Py_ssize_t n = PyTuple_Size(base_mro);
for (Py_ssize_t i = 0; i < n; i++) {
PyObject *next = PyTuple_GetItem(base_mro, i);
int contains = PySequence_Contains(mro_list, next);
if (contains < 0) {
return -1;
} else if (contains == 0) {
if (PyList_Append(mro_list, next)) {
return -1;
}
}
}
return 0;
}

/*
* Define a custom MRO for Python types that mirror Java classes. Java classes
* can define a class hierarchy that is not able to be resolved with the
* default Python MRO. Since Java does not support actual multiple inheritance
* a simplified MRO does not cause problems as long as the non-interface
* classes are in order. This simply merges all the mro's from the base types
* in order. The first base will be the non-interface super class so all
* non-interface inheritance takes precedence over interfaces.
*/
static PyObject* pyjtype_mro(PyObject* self, PyObject* unused)
{
PyTypeObject* type = (PyTypeObject*) self;
PyObject* mro_list = PyList_New(0);
if (!mro_list) {
return NULL;
}
if (PyList_Append(mro_list, self)) {
Py_DECREF(mro_list);
return NULL;
}

PyObject *bases = type->tp_bases;
Py_ssize_t n = PyTuple_Size(bases);
for (Py_ssize_t i = 0; i < n; i++) {
PyObject *base = PyTuple_GetItem(bases, i);
int contains = PySequence_Contains(mro_list, base);
if (contains < 0) {
Py_DECREF(mro_list);
return NULL;
} else if (contains == 0) {
if (PyList_Append(mro_list, base)) {
Py_DECREF(mro_list);
return NULL;
}
}
if (merge_mro(((PyTypeObject*) base), mro_list)) {
Py_DECREF(mro_list);
return NULL;
}
}
PyObject* mro_tuple = PySequence_Tuple(mro_list);
Py_DECREF(mro_list);
return mro_tuple;
}


static PyMethodDef pyjtype_methods[] = {
{
"mro",
pyjtype_mro,
METH_NOARGS,
"Implements a custom MRO algorithm compatible with all Java Class hierarchies"
},

{ NULL, NULL }
};


static PyTypeObject PyJType_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"PyJType", /* tp_name */
0, /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_TYPE_SUBCLASS, /* tp_flags */
0, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
pyjtype_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
NULL, /* tp_new */
};

49 changes: 49 additions & 0 deletions src/test/java/jep/test/TestPyJType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package jep.test;

public class TestPyJType {

public static interface Root {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be worth adding some default methods here, to make sure that doesn't break stuff (now or in the future)?

}

public static interface Child extends Root{
}

/**
* The default Python mro cannot create a consistent method resolution
* order (MRO) for this interface.
*/
public static interface ProblemInterface extends Root, Child {
}

/**
* The default Python mro cannot create a consistent method resolution
* order (MRO) for this class
*/
public static class ProblemClass implements Root, Child {
}

public static class InheritedProblemClass implements ProblemInterface {
}

public static interface InterfaceWithDefault {
public default String checkPrecedence() {
return "InterfaceWithDefault";
}
}

public static class ParentClassWithMethod {
public String checkPrecedence() {
return "ParentClassWithMethod";
}
}

/**
* In Java the parent class method will override interface default method. PyJType should match Java behavior.
*/
public static class ChildTestingMethodInheritance extends ParentClassWithMethod implements InterfaceWithDefault {
}

public static class ClassInheritingDefault implements InterfaceWithDefault {
}

}
8 changes: 8 additions & 0 deletions src/test/python/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ def test_static_access_to_nonstatic_field(self):
def test_close_with_threads(self):
jep_pipe(build_java_process_cmd('jep.test.TestCloseWithThreads'))

def test_pyjtype_mro(self):
# Both these throw errors with the default MRO
jep.findClass('jep.test.TestPyJType$ProblemClass')
jep.findClass('jep.test.TestPyJType$ProblemInterface')
ChildTestingMethodInheritance = jep.findClass('jep.test.TestPyJType$ChildTestingMethodInheritance')
self.assertEquals('ParentClassWithMethod', ChildTestingMethodInheritance().checkPrecedence())
ClassInheritingDefault = jep.findClass('jep.test.TestPyJType$ClassInheritingDefault')
self.assertEquals('InterfaceWithDefault', ClassInheritingDefault().checkPrecedence())