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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions doc/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ This changelog *only* contains changes from the *first* pypi release (0.5.4.3) o
Latest Changes:

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

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

- Fixed uncaught exception while setting traceback causing issues in Python 3.11/3.12.

- Use PEP-518 and PEP-660 configuration for the package, allowing editable and
configurable builds using modern Python packaging tooling.
Where before ``python setup.py --enable-tracing develop``, now can be done with
Expand Down
5 changes: 5 additions & 0 deletions jpype/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ def startJVM(
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,)

Comment on lines +240 to +244
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 code changes the way that the jar gets loaded to place in a privileged enough position to call reflect. Prior to Java 9 this was the norm, but after 9 there was a new module based security policy and org.jpype is not organized into a module.

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
3 changes: 1 addition & 2 deletions 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 Expand Up @@ -196,4 +195,4 @@ using PyObject = _object;
// Primitives classes
#include "jp_primitivetype.h"

#endif // _JPYPE_H_
#endif // _JPYPE_H_
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);
}

Comment on lines -26 to -42
Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the Jar loaded as an agent we don't need to worry about loading it late in the process so all this code can be removed.

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;
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 method to fail is moved to the Java code so the corresponding JNI code to trigger has been removed.


// 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
}
}
Comment on lines +1 to +11
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 aren't going to do anything with the agent status except use the load path. So we don't need to have a body.

We can use the agent to do things like change byte code or access low level memory statistics for the JVM which may be handy. We can also late load a jar file to the same level which is required for things like DB services.

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();
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 static object will be passed back to indicate that Python did not implement the method. There is nothing special about it other than being an object that the user is unlikely to ever return from a valid proxy. The object was left public so that I can pass it during testing to trigger the not implemented logic if needed.


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);
Comment on lines +104 to +107
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.

}

// 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
Comment on lines +321 to +323
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 another requirement for an agent. We must add contents to the manifest and include it in the jar file.

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