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

Access to default methods in @JImplements #1182

Merged
merged 16 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions doc/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Latest Changes:

- **1.5.1_dev0 - 2023-12-15**

- Allow access to default methods implemented in interfaces when using ``@JImplements``.

- Added support for typing ``JArray`` (Java type only), e.g. ``JArray[java.lang.Object]`` ``"JArray[java.lang.Object]"``

- Fixed uncaught exception while setting traceback causing issues in Python 3.11/3.12.
Expand Down
9 changes: 9 additions & 0 deletions jpype/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,15 @@
# Not specified at all, use the default classpath.
classpath = _classpath.getClassPath()

# Handle strings and list of strings.
if classpath:
extra_jvm_args += (f'-Djava.class.path={_handleClassPath(classpath)}', )

supportLib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar")
if not os.path.exists(supportLib):
raise RuntimeError("Unable to find org.jpype.jar support library at " + supportLib)
extra_jvm_args += ('-javaagent:' + supportLib,)
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved

try:
import locale
# Gather a list of locale settings that Java may override (excluding LC_ALL)
Expand Down
1 change: 0 additions & 1 deletion native/common/include/jp_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ class JPContext
public:
JPClassRef m_ContextClass;
JPClassRef m_RuntimeException;
JPClassRef m_NoSuchMethodError;

private:
JPClassRef m_Array;
Expand Down
3 changes: 1 addition & 2 deletions native/common/include/jp_exception.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ _python_error,
_python_exc,
_os_error_unix,
_os_error_windows,
_method_not_found,
};

// Create a stackinfo for a particular location in the code that can then
Expand Down Expand Up @@ -160,4 +159,4 @@ class JPypeException : std::runtime_error
JPThrowableRef m_Throwable;
};

#endif
#endif
1 change: 0 additions & 1 deletion native/common/include/jpype.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ class JPResource
#define JP_RAISE_PYTHON() { throw JPypeException(JPError::_python_error, nullptr, JP_STACKINFO()); }
#define JP_RAISE_OS_ERROR_UNIX(err, msg) { throw JPypeException(JPError::_os_error_unix, msg, err, JP_STACKINFO()); }
#define JP_RAISE_OS_ERROR_WINDOWS(err, msg) { throw JPypeException(JPError::_os_error_windows, msg, err, JP_STACKINFO()); }
#define JP_RAISE_METHOD_NOT_FOUND(msg) { throw JPypeException(JPError::_method_not_found, nullptr, msg, JP_STACKINFO()); }
#define JP_RAISE(type, msg) { throw JPypeException(JPError::_python_exc, type, msg, JP_STACKINFO()); }

#ifndef PyObject_HEAD
Expand Down
57 changes: 2 additions & 55 deletions native/common/jp_classloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,6 @@ jobject JPClassLoader::getBootLoader()
return m_BootLoader.get();
}

static jobject toURL(JPJavaFrame &frame, const string& path)
{
// file = new File("org.jpype.jar");
jclass fileClass = frame.FindClass("java/io/File");
jmethodID newFile = frame.GetMethodID(fileClass, "<init>", "(Ljava/lang/String;)V");
jvalue v[3];
v[0].l = frame.NewStringUTF(path.c_str());
jobject file = frame.NewObjectA(fileClass, newFile, v);

// url = file.toURI().toURL();
jmethodID toURI = frame.GetMethodID(fileClass, "toURI", "()Ljava/net/URI;");
jobject uri = frame.CallObjectMethodA(file, toURI, nullptr);
jclass uriClass = frame.GetObjectClass(uri);
jmethodID toURL = frame.GetMethodID(uriClass, "toURL", "()Ljava/net/URL;");
return frame.CallObjectMethodA(uri, toURL, nullptr);
}

marscher marked this conversation as resolved.
Show resolved Hide resolved
JPClassLoader::JPClassLoader(JPJavaFrame& frame)
{
JP_TRACE_IN("JPClassLoader::JPClassLoader");
Expand Down Expand Up @@ -68,44 +51,8 @@ JPClassLoader::JPClassLoader(JPJavaFrame& frame)
}
frame.ExceptionClear();

// Harder, we need to find the _jpype module and use __file__ to obtain a
// path.
JPPyObject pypath = JPPyObject::call(PyObject_GetAttrString(PyJPModule, "__file__"));
string path = JPPyString::asStringUTF8(pypath.get());
string::size_type i = path.find_last_of('\\');
if (i == string::npos)
i = path.find_last_of('/');
if (i == string::npos)
JP_RAISE(PyExc_RuntimeError, "Can't find jar path");
path = path.substr(0, i + 1);
jobject url1 = toURL(frame, path + "org.jpype.jar");
// jobject url2 = toURL(frame, path + "lib/asm-8.0.1.jar");

// urlArray = new URL[]{url};
jclass urlClass = frame.GetObjectClass(url1);
jobjectArray urlArray = frame.NewObjectArray(1, urlClass, nullptr);
frame.SetObjectArrayElement(urlArray, 0, url1);
// frame.SetObjectArrayElement(urlArray, 1, url2);

// cl = new URLClassLoader(urlArray);
jclass urlLoaderClass = frame.FindClass("java/net/URLClassLoader");
jmethodID newURLClassLoader = frame.GetMethodID(urlLoaderClass, "<init>", "([Ljava/net/URL;Ljava/lang/ClassLoader;)V");
jvalue v[3];
v[0].l = (jobject) urlArray;
v[1].l = (jobject) m_SystemClassLoader.get();
jobject cl = frame.NewObjectA(urlLoaderClass, newURLClassLoader, v);

// Class dycl = Class.forName("org.jpype.classloader.DynamicClassLoader", true, cl);
v[0].l = frame.NewStringUTF("org.jpype.classloader.DynamicClassLoader");
v[1].z = true;
v[2].l = cl;
auto dyClass = (jclass) frame.CallStaticObjectMethodA(m_ClassClass.get(), m_ForNameID, v);

// dycl.newInstance(systemClassLoader);
jmethodID newDyLoader = frame.GetMethodID(dyClass, "<init>", "(Ljava/lang/ClassLoader;)V");
v[0].l = cl;
m_BootLoader = JPObjectRef(frame, frame.NewObjectA(dyClass, newDyLoader, v));

// org.jpype was not loaded already so we can't proceed
JP_RAISE(PyExc_RuntimeError, "Can't find org.jpype.jar support library");
JP_TRACE_OUT; // GCOVR_EXCL_LINE
}

Expand Down
1 change: 0 additions & 1 deletion native/common/jp_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ void JPContext::initializeResources(JNIEnv* env, bool interrupt)
m_Object_HashCodeID = frame.GetMethodID(objectClass, "hashCode", "()I");
m_Object_GetClassID = frame.GetMethodID(objectClass, "getClass", "()Ljava/lang/Class;");

m_NoSuchMethodError = JPClassRef(frame, (jclass) frame.FindClass("java/lang/NoSuchMethodError"));
m_RuntimeException = JPClassRef(frame, (jclass) frame.FindClass("java/lang/RuntimeException"));

jclass stringClass = frame.FindClass("java/lang/String");
Expand Down
12 changes: 0 additions & 12 deletions native/common/jp_exception.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,6 @@ void JPypeException::toPython()
} else if (m_Type == JPError::_python_error)
{
// Already on the stack
} else if (m_Type == JPError::_method_not_found)
{
// This is hit when a proxy fails to implement a required
// method. Only older style proxies should be able hit this.
JP_TRACE("Runtime error");
PyErr_SetString(PyExc_RuntimeError, mesg);
}// This section is only reachable during startup of the JVM.
// GCOVR_EXCL_START
else if (m_Type == JPError::_os_error_unix)
Expand Down Expand Up @@ -429,12 +423,6 @@ void JPypeException::toJava(JPContext *context)
return;
}

if (m_Type == JPError::_method_not_found)
{
frame.ThrowNew(context->m_NoSuchMethodError.get(), mesg);
return;
}

if (m_Type == JPError::_python_error)
{
JPPyCallAcquire callback;
Expand Down
9 changes: 3 additions & 6 deletions native/common/jp_proxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ extern "C" JNIEXPORT jobject JNICALL Java_org_jpype_proxy_JPypeProxy_hostInvoke(
jlong hostObj,
jlong returnTypePtr,
jlongArray parameterTypePtrs,
jobjectArray args)
jobjectArray args,
jobject missing)
{
auto* context = (JPContext*) contextPtr;
JPJavaFrame frame = JPJavaFrame::external(context, env);
Expand Down Expand Up @@ -84,11 +85,7 @@ extern "C" JNIEXPORT jobject JNICALL Java_org_jpype_proxy_JPypeProxy_hostInvoke(

// If method can't be called, throw an exception
if (callable.isNull() || callable.get() == Py_None)
{
JP_TRACE("Callable not found");
JP_RAISE_METHOD_NOT_FOUND(cname);
return nullptr;
}
return missing;
marscher marked this conversation as resolved.
Show resolved Hide resolved

// Find the return type
auto* returnClass = (JPClass*) returnTypePtr;
Expand Down
2 changes: 2 additions & 0 deletions native/java/manifest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Premain-Class: org.jpype.agent.JPypeAgent
11 changes: 11 additions & 0 deletions native/java/org/jpype/agent/JPypeAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.jpype.agent;

import java.lang.instrument.Instrumentation;

public class JPypeAgent
{
public static void premain(String agentArgs, Instrumentation inst) {
// This doesn't have to do anything.
// We just need to be an agent to load elevated privileges
}
}
marscher marked this conversation as resolved.
Show resolved Hide resolved
57 changes: 35 additions & 22 deletions native/java/org/jpype/proxy/JPypeProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
**************************************************************************** */
package org.jpype.proxy;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
Expand All @@ -35,6 +36,7 @@ public class JPypeProxy implements InvocationHandler
public long cleanup;
Class<?>[] interfaces;
ClassLoader cl = ClassLoader.getSystemClassLoader();
public static Object missing = new Object();
marscher marked this conversation as resolved.
Show resolved Hide resolved

public static JPypeProxy newProxy(JPypeContext context,
long instance,
Expand Down Expand Up @@ -69,35 +71,46 @@ public Object newInstance()
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
try
{
// context.incrementProxy();
if (context.isShutdown())
throw new RuntimeException("Proxy called during shutdown");

// We can save a lot of effort on the C++ side by doing all the
// type lookup work here.
TypeManager typeManager = context.getTypeManager();
long returnType;
long[] parameterTypes;
synchronized (typeManager)
if (context.isShutdown())
throw new RuntimeException("Proxy called during shutdown");

// We can save a lot of effort on the C++ side by doing all the
// type lookup work here.
TypeManager typeManager = context.getTypeManager();
long returnType;
long[] parameterTypes;
synchronized (typeManager)
{
returnType = typeManager.findClass(method.getReturnType());
Class<?>[] types = method.getParameterTypes();
parameterTypes = new long[types.length];
for (int i = 0; i < types.length; ++i)
{
returnType = typeManager.findClass(method.getReturnType());
Class<?>[] types = method.getParameterTypes();
parameterTypes = new long[types.length];
for (int i = 0; i < types.length; ++i)
{
parameterTypes[i] = typeManager.findClass(types[i]);
}
parameterTypes[i] = typeManager.findClass(types[i]);
}
}

return hostInvoke(context.getContext(), method.getName(), instance, returnType, parameterTypes, args);
} finally
// Check first to see if Python has implementated it
Object result = hostInvoke(context.getContext(), method.getName(), instance, returnType, parameterTypes, args, missing);

// If we get a good result than return it
if (result != missing)
return result;

// If it is a default method in the interface then we have to invoke it using special reflection.
if (method.isDefault())
{
// context.decrementProxy();
return MethodHandles.lookup()
.unreflectSpecial(method, method.getDeclaringClass())
.bindTo(proxy)
.invokeWithArguments(args);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the heart of the change as we need to call these privileged methods to invoke a special method. Of course given the method we are trying to invoke is nothing more than a public method implemented in the interface there is absolutely nothing privileged about the call. The issue is simply that the API through which we are accessing a public method can also be used to invoke private methods.

Copy link
Contributor

@astrelsky astrelsky Sep 12, 2024

Choose a reason for hiding this comment

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

Wouldn't using MethodHandles.publicLookup be better here since it can only access public fields/methods thus removing the privileged issue?

I'm sure there are some possible benefits to having a java agent, I just wanted to point this out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can certainly try that. Though we will still need the agent if we want to make sure that it never changes behavior based on how JPype is started. (Something that plagues the development.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The other reason for requiring privilege is if we ever want to be able to "extend" a class in Python. In that case we will need to get to protected methods, and protected fields.

Copy link
Contributor

@astrelsky astrelsky Sep 12, 2024

Choose a reason for hiding this comment

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

The other reason for requiring privilege is if we ever want to be able to "extend" a class in Python. In that case we will need to get to protected methods, and protected fields.

I actually have a prototype using JDK22 ClassFile API preview feature, as an independent python package, where I managed this. I designed it around the impression that if you want to extend a Java class from Python, then you don't care about the added overhead 😅.

I basically created delegate methods that start with "-" which could only be accessed by reflection or it would be a syntax error. It's gross but it worked.

I was considering integrating and contributing it since it would not introduce GPL code. Was just waiting on it to exit preview in a LTS JDK and then for the free time.

Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to call a default method from jni using CallNonvirtualObjectMethodA if you use the interface with the default method as the claz parameter.

I figured this out while working on super() support for extension classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CallNonvirtualObjectMethodA does not work. I ran tests on it. While it does allow the default method to be called, it ALWAYS calls the default even when overwritten. I thus went with this approach as there are no methods in the JNI interface that gave proper behavior.

Copy link
Contributor

@astrelsky astrelsky Sep 30, 2024

Choose a reason for hiding this comment

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

CallNonvirtualObjectMethodA does not work. I ran tests on it. While it does allow the default method to be called, it ALWAYS calls the default even when overwritten. I thus went with this approach as there are no methods in the JNI interface that gave proper behavior.

Yes that is what the NonVirtual does, it's invoke exactly the specified method. Thinking about it, I'm not sure why the regular CallVirtual doesn't work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason it doesn't is that for interfaces "ALL" virtual method route to the proxy dispatch. As default methods are virtual that means they effectively are not reachable other than by the "NonVirtual" method. We could in principle your the virtual call request to our method, then call to Python with a reference handle find out that it was really supposed to go to default, then reroute back in the JNI method for a NonVirtual call. That would mean unpacking and repacking arguments at C level and keeping track of the default level in C. Of course we would have to know to try the nonvirtual path (trying to call it when it doesn't exist is a segfault). So that means everything needs to be handed to the the C method. I attempted that path but found it distasteful as it required making reflection API calls prior to launching to C.

It is "doable" but means more routing on the primary path to check for and passing the default method down to C. That would make this already slow path potentially even slower for a feature that not many people will likely use. Better to reroute back to Java and try to invoke the handle there.

}

// Else throw... (this should never happen as proxies are checked when created.)
throw new NoSuchMethodError(method.getName());
}

private static native Object hostInvoke(long context, String name, long pyObject,
long returnType, long[] argsTypes, Object[] args);
long returnType, long[] argsTypes, Object[] args, Object bad);
}
1 change: 1 addition & 0 deletions project/jpype_java/nbproject/project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,4 @@ source.encoding=UTF-8
src.java.dir=${file.reference.native-java}
test.harness.dir=${file.reference.test-harness}
test.src.dir=test
manifest.file=../../native/jara/manifest.txt
6 changes: 5 additions & 1 deletion setupext/build_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,12 @@ def build_java_ext(self, ext):
os.makedirs("build/classes", exist_ok=True)
self.announce(" %s" % " ".join(cmd1), level=distutils.log.INFO)
subprocess.check_call(cmd1)
manifest = None
try:
for file in glob.iglob("native/java/**/*.*", recursive=True):
if file.endswith("manifest.txt"):
manifest = file
continue
marscher marked this conversation as resolved.
Show resolved Hide resolved
if file.endswith(".java") or os.path.isdir(file):
continue
p = os.path.join(build_dir, os.path.relpath(file, "native/java"))
Expand All @@ -326,7 +330,7 @@ def build_java_ext(self, ext):
print("FAIL", ex)
pass
cmd3 = shlex.split(
'%s cvf "%s" -C "%s" .' % (jar, jarFile, build_dir))
'%s cvfm "%s" "%s" -C "%s" .' % (jar, jarFile, manifest, build_dir))
self.announce(" %s" % " ".join(cmd3), level=distutils.log.INFO)
subprocess.check_call(cmd3)

Expand Down
2 changes: 2 additions & 0 deletions test/harness/jpype/proxy/TestInterface1.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public interface TestInterface1
{

int testMethod1();

default int testDefault() { return 1234; }
}
30 changes: 30 additions & 0 deletions test/jpypetest/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,34 @@ class MyImpl(object):
def testMethod1(self):
pass

def testDefault1(self):
itf1 = self.package.TestInterface1

@JImplements(itf1)
class MyImpl(object):
@JOverride
def testMethod1(self):
pass

obj = itf1@MyImpl()
self.assertEqual(obj.testDefault(), 1234)

def testDefault2(self):
itf1 = self.package.TestInterface1

@JImplements(itf1)
class MyImpl(object):
@JOverride
def testMethod1(self):
pass

@JOverride
def testDefault(self):
return 5678

obj = itf1@MyImpl()
self.assertEqual(obj.testDefault(), 5678)

def testProxyImplementsForm2(self):
itf1 = self.package.TestInterface1
itf2 = self.package.TestInterface2
Expand Down Expand Up @@ -560,3 +588,5 @@ def run(self):

startJVM()
assert isinstance(MyImpl(), MyImpl)


Loading