diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst index 56817acb7..dd61866c7 100644 --- a/doc/CHANGELOG.rst +++ b/doc/CHANGELOG.rst @@ -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. diff --git a/jpype/_core.py b/jpype/_core.py index 91d225fd9..e4f547247 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -120,6 +120,7 @@ def _hasClassPath(args) -> bool: def _handleClassPath( classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, + ascii: bool = True ) -> typing.Sequence[str]: """ Return a classpath which represents the given tuple of classpath specifications @@ -151,7 +152,9 @@ def _handleClassPath( out.extend(glob.glob(pth + '.jar')) else: out.append(pth) - return out + if ascii: + return [i for i in out if i.isascii()] + return [i for i in out if not i.isascii()] _JVM_started = False @@ -237,6 +240,17 @@ def startJVM( # Not specified at all, use the default classpath. classpath = _classpath.getClassPath() + # Handle strings and list of strings. + extra_jvm_args = [] + if classpath: + cp = _classpath._SEP.join(_handleClassPath(classpath)) + extra_jvm_args += ['-Djava.class.path=%s'%cp ] + + 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] + try: import locale # Gather a list of locale settings that Java may override (excluding LC_ALL) @@ -244,7 +258,7 @@ def startJVM( # Keep the current locale settings, else Java will replace them. prior = [locale.getlocale(i) for i in categories] # Start the JVM - _jpype.startup(jvmpath, jvmargs, + _jpype.startup(jvmpath, jvmargs + tuple(extra_jvm_args), ignoreUnrecognized, convertStrings, interrupt) # Collect required resources for operation initializeResources() @@ -276,7 +290,7 @@ def startJVM( To resolve this issue we add the classpath after initialization since jpype itself supports unicode class paths. """ - for cp in _handleClassPath(classpath): + for cp in _handleClassPath(classpath, False): addClassPath(Path.cwd() / Path(cp).resolve()) diff --git a/native/common/include/jp_context.h b/native/common/include/jp_context.h index d5cd4c4b9..0b357f786 100644 --- a/native/common/include/jp_context.h +++ b/native/common/include/jp_context.h @@ -220,7 +220,6 @@ class JPContext public: JPClassRef m_ContextClass; JPClassRef m_RuntimeException; - JPClassRef m_NoSuchMethodError; private: JPClassRef m_Array; diff --git a/native/common/include/jp_exception.h b/native/common/include/jp_exception.h index ad9596366..f23b9dea6 100644 --- a/native/common/include/jp_exception.h +++ b/native/common/include/jp_exception.h @@ -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 @@ -160,4 +159,4 @@ class JPypeException : std::runtime_error JPThrowableRef m_Throwable; }; -#endif \ No newline at end of file +#endif diff --git a/native/common/include/jpype.h b/native/common/include/jpype.h index 861831040..c88e57ad3 100644 --- a/native/common/include/jpype.h +++ b/native/common/include/jpype.h @@ -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 diff --git a/native/common/jp_classloader.cpp b/native/common/jp_classloader.cpp index 9e754fb8d..0dbfa750d 100644 --- a/native/common/jp_classloader.cpp +++ b/native/common/jp_classloader.cpp @@ -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, "", "(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); -} - JPClassLoader::JPClassLoader(JPJavaFrame& frame) { JP_TRACE_IN("JPClassLoader::JPClassLoader"); @@ -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, "", "([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, "", "(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 } diff --git a/native/common/jp_context.cpp b/native/common/jp_context.cpp index fc2baa8d2..d6e25cb2f 100644 --- a/native/common/jp_context.cpp +++ b/native/common/jp_context.cpp @@ -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"); diff --git a/native/common/jp_exception.cpp b/native/common/jp_exception.cpp index 339df1a9c..a3e3ef902 100644 --- a/native/common/jp_exception.cpp +++ b/native/common/jp_exception.cpp @@ -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) @@ -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; diff --git a/native/common/jp_proxy.cpp b/native/common/jp_proxy.cpp index 8ad3f464d..432554e2b 100644 --- a/native/common/jp_proxy.cpp +++ b/native/common/jp_proxy.cpp @@ -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); @@ -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; // Find the return type auto* returnClass = (JPClass*) returnTypePtr; diff --git a/native/java/manifest.txt b/native/java/manifest.txt new file mode 100644 index 000000000..0b556c29d --- /dev/null +++ b/native/java/manifest.txt @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Premain-Class: org.jpype.agent.JPypeAgent + diff --git a/native/java/org/jpype/agent/JPypeAgent.java b/native/java/org/jpype/agent/JPypeAgent.java new file mode 100644 index 000000000..c15ac7334 --- /dev/null +++ b/native/java/org/jpype/agent/JPypeAgent.java @@ -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 + } +} diff --git a/native/java/org/jpype/proxy/JPypeProxy.java b/native/java/org/jpype/proxy/JPypeProxy.java index f2c8dd445..836deb74a 100644 --- a/native/java/org/jpype/proxy/JPypeProxy.java +++ b/native/java/org/jpype/proxy/JPypeProxy.java @@ -15,9 +15,15 @@ **************************************************************************** */ package org.jpype.proxy; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.logging.Level; +import java.util.logging.Logger; import org.jpype.JPypeContext; import org.jpype.manager.TypeManager; import org.jpype.ref.JPypeReferenceQueue; @@ -29,12 +35,34 @@ public class JPypeProxy implements InvocationHandler { + private final static Constructor constructor; private final static JPypeReferenceQueue referenceQueue = JPypeReferenceQueue.getInstance(); JPypeContext context; public long instance; public long cleanup; Class[] interfaces; ClassLoader cl = ClassLoader.getSystemClassLoader(); + public static Object missing = new Object(); + + // See following link for Java 8 default access implementation + // https://blog.jooq.org/correct-reflective-access-to-interface-default-methods-in-java-8-9-10/ + static + { + Constructor c = null; + if (System.getProperty("java.version").startsWith("1.")) + { + try + { + c = Lookup.class + .getDeclaredConstructor(Class.class); + c.setAccessible(true); + } catch (NoSuchMethodException | SecurityException ex) + { + Logger.getLogger(JPypeProxy.class.getName()).log(Level.SEVERE, null, ex); + } + } + constructor = c; + } public static JPypeProxy newProxy(JPypeContext context, long instance, @@ -69,35 +97,69 @@ public Object newInstance() public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - try + + 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) { -// 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) + 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]); } + } + + // 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; - return hostInvoke(context.getContext(), method.getName(), instance, returnType, parameterTypes, args); - } finally + // If it is a default method in the interface then we have to invoke it using special reflection. + if (method.isDefault()) { -// context.decrementProxy(); + try + { + Class cls = method.getDeclaringClass(); + + // Java 8 + if (constructor != null) + { + return constructor.newInstance(cls) + .findSpecial(cls, + method.getName(), + MethodType.methodType(method.getReturnType()), + cls) + .bindTo(proxy) + .invokeWithArguments(args); + } + + return MethodHandles.lookup() + .findSpecial(cls, + method.getName(), + MethodType.methodType(method.getReturnType()), + cls) + .bindTo(proxy) + .invokeWithArguments(args); + } catch (java.lang.IllegalAccessException ex) + { + throw new RuntimeException(ex); + } } + + // 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); } diff --git a/project/jpype_java/nbproject/project.properties b/project/jpype_java/nbproject/project.properties index 76c7151ac..3ec6b403a 100755 --- a/project/jpype_java/nbproject/project.properties +++ b/project/jpype_java/nbproject/project.properties @@ -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/java/manifest.txt diff --git a/setupext/build_ext.py b/setupext/build_ext.py index 80514bb93..00cc0a72d 100644 --- a/setupext/build_ext.py +++ b/setupext/build_ext.py @@ -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 if file.endswith(".java") or os.path.isdir(file): continue p = os.path.join(build_dir, os.path.relpath(file, "native/java")) @@ -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) diff --git a/test/harness/jpype/proxy/TestInterface1.java b/test/harness/jpype/proxy/TestInterface1.java index d9ed797ea..00985e31c 100644 --- a/test/harness/jpype/proxy/TestInterface1.java +++ b/test/harness/jpype/proxy/TestInterface1.java @@ -19,4 +19,6 @@ public interface TestInterface1 { int testMethod1(); + + default int testDefault() { return 1234; } } diff --git a/test/jpypetest/test_proxy.py b/test/jpypetest/test_proxy.py index 0cba57b52..87dbe67bd 100644 --- a/test/jpypetest/test_proxy.py +++ b/test/jpypetest/test_proxy.py @@ -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 @@ -560,3 +588,5 @@ def run(self): startJVM() assert isinstance(MyImpl(), MyImpl) + +