From 4344295721a3fafda60ca7143d9c8f69f9ad2562 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 12:43:33 -0800 Subject: [PATCH] Rework to use reflector class --- jpype/_classpath.py | 6 +- jpype/_core.py | 2 +- native/common/jp_classloader.cpp | 2 +- native/java/manifest.txt | 2 +- ...ClassLoader.java => JPypeClassLoader.java} | 108 ++++++++---------- native/java/org/jpype/JPypeContext.java | 33 +++--- native/java/org/jpype/JPypeReflector.java | 27 +++++ native/java/org/jpype/html/Html.java | 4 +- native/java/org/jpype/pkg/JPypePackage.java | 6 +- native/java0/org/jpype/Reflector0.java | 38 ++++++ test/jpypetest/test_startup.py | 6 +- 11 files changed, 147 insertions(+), 87 deletions(-) rename native/java/org/jpype/{classloader/DynamicClassLoader.java => JPypeClassLoader.java} (79%) create mode 100644 native/java/org/jpype/JPypeReflector.java create mode 100644 native/java0/org/jpype/Reflector0.java diff --git a/jpype/_classpath.py b/jpype/_classpath.py index 2d8c919eb..160017005 100644 --- a/jpype/_classpath.py +++ b/jpype/_classpath.py @@ -52,7 +52,7 @@ def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None: path1 = path2.joinpath(path1) # If the JVM is already started then we will have to load the paths - # immediately into the DynamicClassLoader + # immediately into the JPypeClassLoader if _jpype.isStarted(): Paths = _jpype.JClass('java.nio.file.Paths') JContext = _jpype.JClass('org.jpype.JPypeContext') @@ -62,9 +62,9 @@ def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None: if len(paths) == 0: return for path in paths: - classLoader.addFile(Paths.get(str(path))) + classLoader.addPath(Paths.get(str(path))) else: - classLoader.addFile(Paths.get(str(path1))) + classLoader.addPath(Paths.get(str(path1))) _CLASSPATHS.append(path1) diff --git a/jpype/_core.py b/jpype/_core.py index c025ea4ae..f547f7376 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -305,7 +305,7 @@ def startJVM( # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past extra_jvm_args += [ - '-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader', + '-Djava.system.class.loader=org.jpype.JPypeClassLoader', '-Djava.class.path=%s'%support_lib, '-Djpype.class.path=%s'%java_class_path, '-Xshare:off' diff --git a/native/common/jp_classloader.cpp b/native/common/jp_classloader.cpp index 454d6b623..cac86282a 100644 --- a/native/common/jp_classloader.cpp +++ b/native/common/jp_classloader.cpp @@ -38,7 +38,7 @@ JPClassLoader::JPClassLoader(JPJavaFrame& frame) m_SystemClassLoader = JPObjectRef(frame, frame.CallStaticObjectMethodA(classLoaderClass, getSystemClassLoader, nullptr)); - jclass dynamicLoaderClass = frame.getEnv()->FindClass("org/jpype/classloader/DynamicClassLoader"); + jclass dynamicLoaderClass = frame.getEnv()->FindClass("org/jpype/JPypeClassLoader"); if (dynamicLoaderClass != nullptr) { // Use the one in place already diff --git a/native/java/manifest.txt b/native/java/manifest.txt index 0b556c29d..81c97d3eb 100644 --- a/native/java/manifest.txt +++ b/native/java/manifest.txt @@ -1,3 +1,3 @@ Manifest-Version: 1.0 Premain-Class: org.jpype.agent.JPypeAgent - +Multi-Release: true diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/JPypeClassLoader.java similarity index 79% rename from native/java/org/jpype/classloader/DynamicClassLoader.java rename to native/java/org/jpype/JPypeClassLoader.java index 1021217c3..003284ecd 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/JPypeClassLoader.java @@ -1,4 +1,4 @@ -package org.jpype.classloader; +package org.jpype; import java.io.ByteArrayOutputStream; import java.io.File; @@ -7,6 +7,7 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; @@ -22,21 +23,24 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; -public class DynamicClassLoader extends URLClassLoader +public class JPypeClassLoader extends URLClassLoader { - List loaders = new LinkedList<>(); HashMap> map = new HashMap<>(); + int code = 0; - public DynamicClassLoader(ClassLoader parent) + public JPypeClassLoader(ClassLoader parent) { - super(launch(), parent); + super(initial(), parent); + } + + public int getCode() + { + return code; } /** @@ -48,7 +52,7 @@ public DynamicClassLoader(ClassLoader parent) * * @return */ - private static URL[] launch() + private static URL[] initial() { String cp = System.getProperty("jpype.class.path"); if (cp == null) @@ -91,11 +95,6 @@ private void appendToClassPathForInstrumentation(String path) throws Throwable addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); } - public int getCode() - { - return loaders.hashCode(); - } - /** * Add a set of jars to the classpath. * @@ -103,11 +102,10 @@ public int getCode() * @param glob * @throws IOException */ - public void addFiles(Path root, String glob) throws IOException + public void addPaths(Path root, String glob) throws IOException { final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(glob); - List urls = new LinkedList<>(); Files.walkFileTree(root, new SimpleFileVisitor() { @@ -118,7 +116,7 @@ public FileVisitResult visitFile(Path path, if (pathMatcher.matches(root.relativize(path))) { URL url = path.toUri().toURL(); - urls.add(url); + addURL(url); } return FileVisitResult.CONTINUE; } @@ -131,23 +129,15 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) } }); - loaders.add(new URLClassLoader(urls.toArray(new URL[0]))); } - public void addFile(Path path) throws FileNotFoundException + public void addPath(Path path) throws FileNotFoundException { try { if (!Files.exists(path)) throw new FileNotFoundException(path.toString()); - URL[] urls = new URL[] - { - path.toUri().toURL() - }; - loaders.add(new URLClassLoader(urls)); - - // Scan the file for directory entries - this.scanJar(path); + this.addURL(path.toUri().toURL()); } catch (MalformedURLException ex) { // This should never happen @@ -186,7 +176,7 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr buffer.flush(); byte[] data = buffer.toByteArray(); - return defineClass(name, data, 0, data.length); + return defineClass(null, data, 0, data.length); } } catch (IOException ex) { @@ -194,18 +184,6 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr throw new ClassNotFoundException(name); } - @Override - public URL getResource(String name) - { - // Search our parent - URL url = this.getParent().getResource(name); - if (url != null) - return url; - - // Otherwise search locally - return findResource(name); - } - @Override public URL findResource(String name) { @@ -214,31 +192,23 @@ public URL findResource(String name) if (url != null) return url; - // Use one of the subs - for (URLClassLoader cl : this.loaders) - { - url = cl.findResource(name); - if (url != null) - return url; - } // Both with and without / should generate the same result if (name.endsWith("/")) name = name.substring(0, name.length() - 1); if (map.containsKey(name)) return map.get(name).get(0); + + // We have some resource which must be sourced to a particular class loader + if (name.startsWith("org/jpype/")) + return getResource("META-INF/versions/0/" + name); return null; } @Override - public Enumeration getResources(String name) throws IOException + public Enumeration findResources(String name) throws IOException { ArrayList out = new ArrayList<>(); - out.addAll(Collections.list(getParent().getResources(name))); - out.addAll(Collections.list(super.getResources(name))); - for (URLClassLoader cl : this.loaders) - { - out.addAll(Collections.list(cl.findResources(name))); - } + out.addAll(Collections.list(super.findResources(name))); // Both with and without / should generate the same result if (name.endsWith("/")) name = name.substring(0, name.length() - 1); @@ -254,6 +224,25 @@ public void addResource(String name, URL url) this.map.get(name).add(url); } + @Override + public void addURL(URL url) + { + // Mark our cache as dirty + code = code * 98745623 + url.hashCode(); + super.addURL(url); + Path path; + try + { + path = Paths.get(url.toURI()); + } catch (URISyntaxException ex) + { + return; + } + + // Scan for missing resources + scanJar(path); + } + /** * Recreate missing directory entries for Jars that lack indexing. * @@ -261,18 +250,19 @@ public void addResource(String name, URL url) * properly importing their contents. This procedure scans a jar file when * loaded to build missing directories. * - * @param p1 + * @param path */ - public void scanJar(Path p1) + void scanJar(Path path) { - if (!Files.exists(p1)) + if (!Files.exists(path)) return; - if (Files.isDirectory(p1)) + if (Files.isDirectory(path)) return; - try (JarFile jf = new JarFile(p1.toFile())) + + try (JarFile jf = new JarFile(path.toFile())) { Enumeration entries = jf.entries(); - URI abs = p1.toAbsolutePath().toUri(); + URI abs = path.toAbsolutePath().toUri(); Set urls = new java.util.HashSet(); while (entries.hasMoreElements()) { diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java index db85cf37c..d4fcc00be 100644 --- a/native/java/org/jpype/JPypeContext.java +++ b/native/java/org/jpype/JPypeContext.java @@ -27,7 +27,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import org.jpype.classloader.DynamicClassLoader; +import java.util.logging.Level; +import java.util.logging.Logger; import org.jpype.manager.TypeFactory; import org.jpype.manager.TypeFactoryNative; import org.jpype.manager.TypeManager; @@ -77,11 +78,12 @@ public class JPypeContext private long context; private TypeFactory typeFactory; private TypeManager typeManager; - private DynamicClassLoader classLoader; + private JPypeClassLoader classLoader; private final AtomicInteger shutdownFlag = new AtomicInteger(); private final List shutdownHooks = new ArrayList<>(); private final List postHooks = new ArrayList<>(); public static boolean freeResources = true; + public JPypeReflector reflector = null; static public JPypeContext getInstance() { @@ -92,20 +94,32 @@ static public JPypeContext getInstance() * Start the JPype system. * * @param context is the C++ portion of the context. - * @param bootLoader is the classloader holding JPype resources. + * @param loader is the classloader holding JPype resources. * @return the created context. */ - static JPypeContext createContext(long context, ClassLoader bootLoader, String nativeLib, boolean interrupt) + static JPypeContext createContext(long context, ClassLoader loader, String nativeLib, boolean interrupt) { if (nativeLib != null) { System.load(nativeLib); } INSTANCE.context = context; - INSTANCE.classLoader = (DynamicClassLoader) bootLoader; + INSTANCE.classLoader = (JPypeClassLoader) loader; INSTANCE.typeFactory = new TypeFactoryNative(); INSTANCE.typeManager = new TypeManager(context, INSTANCE.typeFactory); INSTANCE.initialize(interrupt); + + try + { + INSTANCE.reflector = (JPypeReflector) Class.forName("org.jpype.Reflector0", true, loader) + .getConstructor() + .newInstance(); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException + | InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException ex) + { + System.err.println("Unable to create reflector "+ ex); + } scanExistingJars(); return INSTANCE; @@ -460,15 +474,6 @@ public boolean isShutdown() return shutdownFlag.get() > 0; } -// public void incrementProxy() -// { -// proxyCount.incrementAndGet(); -// } -// -// public void decrementProxy() -// { -// proxyCount.decrementAndGet(); -// } /** * Clear the current interrupt. * diff --git a/native/java/org/jpype/JPypeReflector.java b/native/java/org/jpype/JPypeReflector.java new file mode 100644 index 000000000..f92d6de0e --- /dev/null +++ b/native/java/org/jpype/JPypeReflector.java @@ -0,0 +1,27 @@ +package org.jpype; + +import java.lang.reflect.Method; + +/** + * + * @author nelson85 + */ +public interface JPypeReflector +{ + /** + * Call a method using reflection. + * + * This method creates a stackframe so that caller sensitive methods will + * execute properly. + * + * @param method is the method to call. + * @param obj is the object to operate on, it will be null if the method is + * static. + * @param args the arguments to method. + * @return the object that results form the invocation. + * @throws java.lang.Throwable throws whatever type the called method + * produces. + */ + public Object callMethod(Method method, Object obj, Object[] args) + throws Throwable; +} diff --git a/native/java/org/jpype/html/Html.java b/native/java/org/jpype/html/Html.java index 35739ee48..e97a02af3 100644 --- a/native/java/org/jpype/html/Html.java +++ b/native/java/org/jpype/html/Html.java @@ -72,8 +72,8 @@ public static List parseAttributes(Document doc, String str) static { - try (InputStream is = JPypeContext.getInstance().getClass().getClassLoader() - .getResourceAsStream("org/jpype/html/entities.txt"); + ClassLoader cl = ClassLoader.getSystemClassLoader(); + try (InputStream is = cl.getResourceAsStream("org/jpype/html/entities.txt"); InputStreamReader isr = new InputStreamReader(is); BufferedReader rd = new BufferedReader(isr)) { diff --git a/native/java/org/jpype/pkg/JPypePackage.java b/native/java/org/jpype/pkg/JPypePackage.java index 1541ccea4..1321b40d9 100644 --- a/native/java/org/jpype/pkg/JPypePackage.java +++ b/native/java/org/jpype/pkg/JPypePackage.java @@ -25,9 +25,9 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Map; +import org.jpype.JPypeClassLoader; import org.jpype.JPypeContext; import org.jpype.JPypeKeywords; -import org.jpype.classloader.DynamicClassLoader; /** * Representation of a JPackage in Java. @@ -45,13 +45,13 @@ public class JPypePackage // A mapping from Python names into Paths into the module/jar file system. Map contents; int code; - private final DynamicClassLoader classLoader; + private final JPypeClassLoader classLoader; public JPypePackage(String pkg) { this.pkg = pkg; this.contents = JPypePackageManager.getContentMap(pkg); - this.classLoader = ((DynamicClassLoader)(JPypeContext.getInstance().getClassLoader())); + this.classLoader = ((JPypeClassLoader)(JPypeContext.getInstance().getClassLoader())); this.code = classLoader.getCode(); } diff --git a/native/java0/org/jpype/Reflector0.java b/native/java0/org/jpype/Reflector0.java new file mode 100644 index 000000000..f4ff1891f --- /dev/null +++ b/native/java0/org/jpype/Reflector0.java @@ -0,0 +1,38 @@ +package org.jpype; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Reflector0 implements JPypeReflector +{ + + public Reflector0() + {} + + /** + * Call a method using reflection. + * + * This method creates a stackframe so that caller sensitive methods will + * execute properly. + * + * @param method is the method to call. + * @param obj is the object to operate on, it will be null if the method is + * static. + * @param args the arguments to method. + * @return the object that results form the invocation. + * @throws java.lang.Throwable throws whatever type the called method + * produces. + */ + public Object callMethod(Method method, Object obj, Object[] args) + throws Throwable + { + try + { + return method.invoke(obj, args); + } catch (InvocationTargetException ex) + { +// ex.printStackTrace(); + throw ex.getCause(); + } + } +} diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index 0a3eadc00..d5a4c86c4 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -152,7 +152,7 @@ def testNonASCIIPath(self): """ jpype.startJVM(jvmpath=Path(self.jvmpath), classpath="test/jar/unicode_à😎/sample_package.jar") cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.JPypeClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] @@ -162,7 +162,7 @@ def testOldStyleNonASCIIPath(self): """ jpype.startJVM("-Djava.class.path=test/jar/unicode_à😎/sample_package.jar", jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.JPypeClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] def testNonASCIIPathWithSystemClassLoader(self): @@ -207,7 +207,7 @@ def testDefaultSystemClassLoader(self): # we introduce no behavior change unless absolutely necessary jpype.startJVM(jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) + self.assertNotEqual(type(cl), jpype.JClass("org.jpype.JPypeClassLoader")) def testServiceWithNonASCIIPath(self): jpype.startJVM(