From 64e4ce83a55a8e66b24258c764064ccd1ce70cac Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Mon, 26 Aug 2024 19:52:11 +0800 Subject: [PATCH 1/4] fix: try to load class from multiple class-loaders --- .../luajava/jsr223/LuaScriptEngine.java | 2 +- .../jsr223/LuaScriptEngineFactory.java | 2 +- .../java/party/iroiro/luajava/JuaAPI.java | 12 +-- .../party/iroiro/luajava/util/ClassUtils.java | 74 +++++++++++++++---- 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngine.java b/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngine.java index da27ff7f..caf9dc5b 100644 --- a/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngine.java +++ b/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngine.java @@ -25,7 +25,7 @@ public final class LuaScriptEngine extends AbstractScriptEngine implements Scrip private Lua getLua() throws ScriptException { try { - Lua L = (Lua) ClassUtils.forName(luaClass, null).newInstance(); + Lua L = (Lua) ClassUtils.forName(luaClass).newInstance(); L.setExternalLoader(new ClassPathLoader()); L.openLibraries(); return L; diff --git a/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngineFactory.java b/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngineFactory.java index 60a1d5c9..c8c9ae99 100644 --- a/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngineFactory.java +++ b/jsr223/src/main/java/party/iroiro/luajava/jsr223/LuaScriptEngineFactory.java @@ -183,7 +183,7 @@ private static String[] findAvailableEngine() { } for (String[] engine : ENGINES) { try { - ClassUtils.forName(engine[2], null); + ClassUtils.forName(engine[2]); return engine; } catch (ClassNotFoundException ignored) { } diff --git a/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java b/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java index 2728164e..10004aea 100644 --- a/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java +++ b/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java @@ -129,7 +129,7 @@ public static int loadModule(int id, String module) { public static int loadLib(int id, String className, String methodName) { AbstractLua L = Jua.get(id); try { - Class clazz = ClassUtils.forName(className, null); + Class clazz = ClassUtils.forName(className); Method method = clazz.getDeclaredMethod(methodName, Lua.class); if (method.getReturnType() == int.class) { //noinspection Convert2Lambda @@ -199,7 +199,7 @@ public static int proxy(int id) { String name = L.toString(i); if (name != null) { try { - return ClassUtils.forName(name, null); + return ClassUtils.forName(name); } catch (ClassNotFoundException e) { return null; } @@ -224,7 +224,7 @@ public static int proxy(int id) { public static int javaImport(int id, String className) { Lua L = Jua.get(id); try { - L.pushJavaClass(ClassUtils.forName(className, null)); + L.pushJavaClass(ClassUtils.forName(className)); return 1; } catch (ClassNotFoundException e) { return L.error(e); @@ -362,7 +362,7 @@ public static int objectInvoke(int index, Object obj, String name, String iClass = name.substring(0, colon); String method = name.substring(colon + 1); try { - return methodInvoke(index, ClassUtils.forName(iClass, null), obj, method, + return methodInvoke(index, ClassUtils.forName(iClass), obj, method, notSignature, paramCount); } catch (ClassNotFoundException e) { return Jua.get(index).error(e); @@ -460,7 +460,7 @@ public static int classIndex(int index, Class clazz, String name) { return 1; } else { try { - L.pushJavaClass(ClassUtils.forName(clazz.getName() + '$' + name, null)); + L.pushJavaClass(ClassUtils.forName(clazz.getName() + '$' + name)); return 1; } catch (ClassNotFoundException e) { return i; @@ -1028,7 +1028,7 @@ public static Class[] getClasses(String notSignature) { Class[] classes = new Class[names.length]; for (int i = 0; i < names.length; i++) { try { - classes[i] = ClassUtils.forName(names[i], null); + classes[i] = ClassUtils.forName(names[i]); } catch (ClassNotFoundException e) { classes[i] = null; } diff --git a/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java b/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java index c0d230c2..3ca94065 100644 --- a/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java +++ b/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java @@ -1,7 +1,7 @@ /* * Copied from Spring Framework: org/springframework/util/ClassUtils.java * - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,19 +23,19 @@ import java.io.Closeable; import java.io.Externalizable; import java.io.Serializable; -import java.lang.reflect.Array; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.lang.reflect.*; import java.util.*; /** * Miscellaneous {@code java.lang.Class} utility methods. - * Mainly for internal use within the framework. + * + *

Mainly for internal use within the framework. * * @author Juergen Hoeller * @author Keith Donald * @author Rob Harrop * @author Sam Brannen + * @author Sebastien Deleuze * @since 1.1 */ public abstract class ClassUtils { @@ -73,7 +73,7 @@ public abstract class ClassUtils { /** * Map with primitive type name as key and corresponding primitive - * type as value, for example: "int" -> "int.class". + * type as value, for example: {@code "int" -> int.class}. */ private static final Map> primitiveTypeNameMap = new HashMap<>(32); @@ -114,8 +114,7 @@ public abstract class ClassUtils { registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, Error.class, StackTraceElement.class, StackTraceElement[].class); registerCommonClasses(Enum.class, Iterable.class, Iterator.class, Enumeration.class, - Collection.class, List.class, Set.class, Map.class, Map.Entry.class, - optionalClass()); + Collection.class, List.class, Set.class, Map.class, Map.Entry.class, optionalClass()); Class[] javaLanguageInterfaceArray = {Serializable.class, Externalizable.class, Closeable.class, AutoCloseable.class, Cloneable.class, Comparable.class}; @@ -140,12 +139,21 @@ public abstract class ClassUtils { */ private static void registerCommonClasses(Class... commonClasses) { for (Class clazz : commonClasses) { - if (clazz != null) { - commonClassCache.put(clazz.getName(), clazz); - } + commonClassCache.put(clazz.getName(), clazz); } } + /** + * Reference to an external class loader + * + *

+ * This library defaults to using the class loader of this class. + * If this causes problems, you may try to override the default + * by setting this field to a suitable class loader instance. + *

+ */ + public static volatile ClassLoader DEFAULT_CLASS_LOADER = null; + /** * Return the default ClassLoader to use: typically the thread context * ClassLoader, if available; the ClassLoader that loaded the ClassUtils @@ -163,7 +171,10 @@ private static void registerCommonClasses(Class... commonClasses) { */ @Nullable public static ClassLoader getDefaultClassLoader() { - ClassLoader cl = null; + ClassLoader cl = DEFAULT_CLASS_LOADER; + if (cl != null) { + return cl; + } try { cl = Thread.currentThread().getContextClassLoader(); } catch (Throwable ex) { @@ -192,13 +203,13 @@ public static ClassLoader getDefaultClassLoader() { * * @param name the name of the Class * @param classLoader the class loader to use - * (which may be {@code null}, which indicates the default class loader) + * (can be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws ClassNotFoundException if the class was not found * @throws LinkageError if the class file could not be loaded * @see Class#forName(String, boolean, ClassLoader) */ - public static Class forName(String name, @Nullable ClassLoader classLoader) + private static Class forName(String name, @Nullable ClassLoader classLoader) throws ClassNotFoundException, LinkageError { Class clazz = resolvePrimitiveClassName(name); if (clazz == null) { @@ -237,7 +248,8 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) return Class.forName(name, false, clToUse); } catch (ClassNotFoundException ex) { int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); - if (lastDotIndex != -1) { + int previousDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR, lastDotIndex - 1); + if (lastDotIndex != -1 && previousDotIndex != -1 && Character.isUpperCase(name.charAt(previousDotIndex + 1))) { String nestedClassName = name.substring(0, lastDotIndex) + NESTED_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); try { @@ -273,6 +285,38 @@ public static Class resolvePrimitiveClassName(@Nullable String name) { return result; } + /** + * Try to use {@link #forName(String, ClassLoader)} with multiple class loaders + *

+ * {@link #getDefaultClassLoader()} prioritizes {@link Thread#getContextClassLoader()} + * over {@link Class#getClassLoader()}. However, in some scenarios, the package is + * loaded over a isolated class-loader with an inappropriate {@link Thread#getContextClassLoader()}, + * making class look-up fail. This method is an attempt to fix that. + *

+ * + * @param name the name of the Class + * @return a class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see #forName(String, ClassLoader) + */ + public static Class forName(String name) throws ClassNotFoundException { + try { + return forName(name, null); + } catch (ClassNotFoundException ex) { + try { + return forName(name, ClassUtils.class.getClassLoader()); + } catch (ClassNotFoundException ex2) { + try { + return forName(name, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException ex3) { + // Swallow - let original exception get through + } + } + throw ex; + } + } + private final static Set OBJECT_METHODS; static { From a971b18f23b999168d3033fd10080ca58e125ccd Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Tue, 27 Aug 2024 03:10:42 +0800 Subject: [PATCH 2/4] feat: cache reflection results --- example/build.gradle | 16 ++++ .../luajava/jmh/MethodCallBenchmark.java | 52 ++++++++++++ .../iroiro/luajava/jmh/SimpleBenchmark.java | 53 ++++++++++++ example/src/jmh/resources/binary-trees.lua | 80 ++++++++++++++++++ .../java/party/iroiro/luajava/JuaAPI.java | 84 ++++++++++++++++++- .../party/iroiro/luajava/util/LRUCache.java | 68 +++++++++++++++ 6 files changed, 349 insertions(+), 4 deletions(-) create mode 100644 example/src/jmh/java/party/iroiro/luajava/jmh/MethodCallBenchmark.java create mode 100644 example/src/jmh/java/party/iroiro/luajava/jmh/SimpleBenchmark.java create mode 100644 example/src/jmh/resources/binary-trees.lua create mode 100644 luajava/src/main/java/party/iroiro/luajava/util/LRUCache.java diff --git a/example/build.gradle b/example/build.gradle index 3e8f94ca..2eb7ed6d 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -3,6 +3,7 @@ plugins { id 'application' id 'jacoco' id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'me.champeau.jmh' version '0.7.2' } repositories { @@ -24,6 +25,12 @@ java { } dependencies { + jmh project(':luajava') + jmh project(':luaj') + jmh project(':lua54') + jmh project(':luajit') + jmh project(path: ':lua54', configuration: 'desktopNatives') + jmh project(path: ':luajit', configuration: 'desktopNatives') implementation project(':luajava') implementation project(':lua51') implementation project(':lua52') @@ -50,6 +57,15 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jUnitVersion" } +jmh { + benchmarkMode = ['avgt'] + fork = 1 + iterations = 3 + profilers = ['perfasm'] + warmupIterations = 2 + timeUnit = 'us' +} + test { useJUnitPlatform() diff --git a/example/src/jmh/java/party/iroiro/luajava/jmh/MethodCallBenchmark.java b/example/src/jmh/java/party/iroiro/luajava/jmh/MethodCallBenchmark.java new file mode 100644 index 00000000..3a469768 --- /dev/null +++ b/example/src/jmh/java/party/iroiro/luajava/jmh/MethodCallBenchmark.java @@ -0,0 +1,52 @@ +package party.iroiro.luajava.jmh; + +import org.openjdk.jmh.annotations.*; +import party.iroiro.luajava.Lua; +import party.iroiro.luajava.lua54.Lua54; +import party.iroiro.luajava.luaj.LuaJ; +import party.iroiro.luajava.luajit.LuaJit; + +import java.math.BigInteger; + +@State(Scope.Benchmark) +public class MethodCallBenchmark { + + @Param({"Lua 5.4", "LuaJIT", "LuaJ"}) + public String lua; + + private Lua L; + + @Setup + public void setup() { + switch (lua) { + case "Lua 5.4": + L = new Lua54(); + break; + case "LuaJIT": + L = new LuaJit(); + break; + case "LuaJ": + L = new LuaJ(); + break; + default: + throw new IllegalStateException(); + } + L.set("big_int", BigInteger.valueOf(1024)); + L.run("int_value = java.method(big_int, 'intValue', '')"); + } + + @Benchmark + public void benchmarkObjectMethodCall() { + L.run("assert(big_int:intValue() == 1024)"); + } + + @Benchmark + public void benchmarkModuleMethodCall() { + L.run("assert(int_value() == 1024)"); + } + + @TearDown + public void tearDown() { + L.close(); + } +} diff --git a/example/src/jmh/java/party/iroiro/luajava/jmh/SimpleBenchmark.java b/example/src/jmh/java/party/iroiro/luajava/jmh/SimpleBenchmark.java new file mode 100644 index 00000000..de0c8f35 --- /dev/null +++ b/example/src/jmh/java/party/iroiro/luajava/jmh/SimpleBenchmark.java @@ -0,0 +1,53 @@ +package party.iroiro.luajava.jmh; + +import org.openjdk.jmh.annotations.*; +import party.iroiro.luajava.ClassPathLoader; +import party.iroiro.luajava.Lua; +import party.iroiro.luajava.lua54.Lua54; +import party.iroiro.luajava.luaj.LuaJ; +import party.iroiro.luajava.luajit.LuaJit; + +@State(Scope.Benchmark) +public class SimpleBenchmark { + + private void setupLua(Lua L) { + L.openLibraries(); + L.run("io.write = function(s) assert(string.find(s, 'tree', 1, true)) end"); + L.setExternalLoader(new ClassPathLoader()); + L.loadExternal("binary-trees"); + L.setGlobal("benchmark"); + } + + @Param({"Lua 5.4", "LuaJIT", "LuaJ"}) + public String lua; + + private Lua L; + + @Setup + public void setup() { + switch (lua) { + case "Lua 5.4": + L = new Lua54(); + break; + case "LuaJIT": + L = new LuaJit(); + break; + case "LuaJ": + L = new LuaJ(); + break; + default: + throw new IllegalStateException(); + } + setupLua(L); + } + + @Benchmark + public void benchmarkBinaryTrees() { + L.run("benchmark()"); + } + + @TearDown + public void tearDown() { + L.close(); + } +} diff --git a/example/src/jmh/resources/binary-trees.lua b/example/src/jmh/resources/binary-trees.lua new file mode 100644 index 00000000..78498e41 --- /dev/null +++ b/example/src/jmh/resources/binary-trees.lua @@ -0,0 +1,80 @@ +-- Copyright © 2004-2008 Brent Fulgham, 2005-2024 Isaac Gouy +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are met: +-- +-- 1. Redistributions of source code must retain the above copyright notice, +-- this list of conditions and the following disclaimer. +-- +-- 2. Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- +-- 3. Neither the name "The Computer Language Benchmarks Game" nor the name "The +-- Benchmarks Game" nor the name "The Computer Language Shootout Benchmarks" nor +-- the names of its contributors may be used to endorse or promote products +-- derived from this software without specific prior written permission. +-- + +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +-- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +-- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +-- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +-- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +-- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +-- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +-- POSSIBILITY OF SUCH DAMAGE. + +-- The Computer Language Benchmarks Game +-- https://salsa.debian.org/benchmarksgame-team/benchmarksgame/ +-- contributed by Mike Pall +-- *reset* + +local function BottomUpTree(depth) + if depth > 0 then + depth = depth - 1 + local left, right = BottomUpTree(depth), BottomUpTree(depth) + return { left, right } + else + return { } + end +end + +local function ItemCheck(tree) + if tree[1] then + return 1 + ItemCheck(tree[1]) + ItemCheck(tree[2]) + else + return 1 + end +end + +local N = tonumber(arg and arg[1]) or 0 +local mindepth = 4 +local maxdepth = mindepth + 2 +if maxdepth < N then maxdepth = N end + +do + local stretchdepth = maxdepth + 1 + local stretchtree = BottomUpTree(stretchdepth) + io.write(string.format("stretch tree of depth %d\t check: %d\n", + stretchdepth, ItemCheck(stretchtree))) +end + +local longlivedtree = BottomUpTree(maxdepth) + +for depth=mindepth,maxdepth,2 do + local iterations = 2 ^ (maxdepth - depth + mindepth) + local check = 0 + for i=1,iterations do + check = check + ItemCheck(BottomUpTree(depth)) + end + io.write(string.format("%d\t trees of depth %d\t check: %d\n", + iterations, depth, check)) +end + +io.write(string.format("long lived tree of depth %d\t check: %d\n", + maxdepth, ItemCheck(longlivedtree))) diff --git a/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java b/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java index 10004aea..d8f51c51 100644 --- a/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java +++ b/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import party.iroiro.luajava.util.ClassUtils; +import party.iroiro.luajava.util.LRUCache; import party.iroiro.luajava.value.LuaValue; import java.lang.reflect.*; @@ -607,6 +608,12 @@ public static int arrayLength(Object obj) { } } + private final static LRUCache, String, Method[]> MEMBER_METHOD_CACHE = new LRUCache<>( + 25, + 10, + 4 + ); + /** * Calls the given method {obj}.{name}(... params from stack) *

@@ -628,7 +635,18 @@ public static int methodInvoke(int index, Class clazz, @Nullable Object obj, Lua L = Jua.get(index); /* Storage of converted params */ Object[] objects = new Object[paramCount]; - Method method = matchMethod(L, clazz.getMethods(), name, objects); + Method[] methods = MEMBER_METHOD_CACHE.get(clazz, name); + if (methods == null) { + List namedMethods = new ArrayList<>(); + for (Method method : clazz.getMethods()) { + if (method.getName().equals(name)) { + namedMethods.add(method); + } + } + methods = namedMethods.toArray(new Method[0]); + MEMBER_METHOD_CACHE.put(clazz, name, methods); + } + Method method = matchMethod(L, methods, name, objects); if (method == null) { L.push("no matching method found"); return -1; @@ -752,6 +770,21 @@ private static Object[] transformVarArgs(Executable executable, Object[] objects } */ + private final static class OptionalField { + @Nullable + public final Field field; + + private OptionalField(@Nullable Field field) { + this.field = field; + } + } + + private final static LRUCache, String, OptionalField> OBJECT_FIELD_CACHE = new LRUCache<>( + 25, + 10, + 4 + ); + /** * Tries to fetch field from an object *

@@ -766,11 +799,22 @@ private static Object[] transformVarArgs(Executable executable, Object[] objects */ public static int fieldIndex(Lua L, Class clazz, @Nullable Object object, String name) { try { - Field field = clazz.getField(name); + OptionalField optionalField = OBJECT_FIELD_CACHE.get(clazz, name); + Field field; + if (optionalField == null) { + field = clazz.getField(name); + OBJECT_FIELD_CACHE.put(clazz, name, new OptionalField(field)); + } else { + field = optionalField.field; + if (field == null) { + return 2; + } + } Object obj = field.get(object); L.push(obj, Lua.Conversion.SEMI); return 1; } catch (NoSuchFieldException | IllegalAccessException | NullPointerException ignored) { + OBJECT_FIELD_CACHE.put(clazz, name, new OptionalField(null)); return 2; } } @@ -787,12 +831,23 @@ public static int fieldIndex(Lua L, Class clazz, @Nullable Object object, Str private static int fieldNewIndex(int index, Class clazz, Object object, String name) { Lua L = Jua.get(index); try { - Field field = clazz.getField(name); + OptionalField optionalField = OBJECT_FIELD_CACHE.get(clazz, name); + Field field; + if (optionalField == null) { + field = clazz.getField(name); + OBJECT_FIELD_CACHE.put(clazz, name, new OptionalField(field)); + } else { + field = optionalField.field; + if (field == null) { + return L.error(new NoSuchFieldException(name)); + } + } Class type = field.getType(); Object o = convertFromLua(L, type, 3); field.set(object, o); return 0; } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) { + OBJECT_FIELD_CACHE.put(clazz, name, new OptionalField(null)); return L.error(e); } } @@ -865,6 +920,18 @@ private static T matchMethod(Lua L, T[] methods, return null; } + private final static LRUCache, String, Constructor> CONSTRUCTOR_CACHE = new LRUCache<>( + 25, + 5, + 4 + ); + + private final static LRUCache, String, Method> METHOD_CACHE = new LRUCache<>( + 25, + 50, + 4 + ); + /** * Find a certain constructor * @@ -874,6 +941,10 @@ private static T matchMethod(Lua L, T[] methods, */ @Nullable private static Constructor matchMethod(Class clazz, String notSignature) { + Constructor cached = CONSTRUCTOR_CACHE.get(clazz, notSignature); + if (cached != null) { + return cached; + } Class[] classes = getClasses(notSignature); try { return clazz.getConstructor(classes); @@ -892,6 +963,10 @@ private static Constructor matchMethod(Class clazz, String notSignature) { */ @Nullable private static Method matchMethod(Class clazz, String name, String notSignature) { + Method cached = METHOD_CACHE.get(clazz, name + ",," + notSignature); + if (cached != null) { + return cached; + } Class[] classes = getClasses(notSignature); try { return clazz.getMethod(name, classes); @@ -1041,7 +1116,8 @@ public static Class[] getClasses(String notSignature) { * since {@code Executable} is not introduced until Java 8. */ interface ExecutableWrapper { - @Nullable String getName(T executable); + @Nullable + String getName(T executable); Class[] getParameterTypes(T executable); } diff --git a/luajava/src/main/java/party/iroiro/luajava/util/LRUCache.java b/luajava/src/main/java/party/iroiro/luajava/util/LRUCache.java new file mode 100644 index 00000000..cc62dce9 --- /dev/null +++ b/luajava/src/main/java/party/iroiro/luajava/util/LRUCache.java @@ -0,0 +1,68 @@ +package party.iroiro.luajava.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * An LRU-cache based on {@link LinkedHashMap} + * + *

+ * Basically, this class is intended for method cache with usage like + * {@code LRUCache, String, Method>}. + *

+ */ +public final class LRUCache { + + private final int innerSize; + private final List>> cacheShards; + + public LRUCache(int level1Size, int level2Size, int shards) { + this.innerSize = level2Size; + ArrayList>> shardList = new ArrayList<>(shards); + for (int i = 0; i < shards; i++) { + shardList.add(Collections.synchronizedMap(new Cache<>(level1Size))); + } + this.cacheShards = Collections.unmodifiableList(shardList); + } + + @Nullable + public V get(K1 k1, K2 k2) { + Map inner = getInnerCache(k1); + return inner.get(k2); + } + + private Map getInnerCache(K1 k1) { + int shard = k1.hashCode() % cacheShards.size(); + Map> cache = cacheShards.get(shard); + Map inner = cache.get(k1); + if (inner == null) { + inner = Collections.synchronizedMap(new Cache<>(innerSize)); + Map prev = cache.putIfAbsent(k1, inner); + if (prev != null) { + inner = prev; + } + } + return inner; + } + + public void put(K1 k1, K2 k2, V v) { + Map inner = getInnerCache(k1); + inner.putIfAbsent(k2, v); + } + + private final static class Cache extends LinkedHashMap { + private final int maxEntries; + + private Cache(int maxEntries) { + super(maxEntries + 1, 0.75F, true); + this.maxEntries = maxEntries; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxEntries; + } + } + +} From 32d4aea0971fb841da42fa632cdea35945f542cd Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Tue, 27 Aug 2024 14:39:58 +0800 Subject: [PATCH 3/4] feat: allow using different classloader for ClassPathLoader --- docs/troubleshooting.md | 26 ++++++++++++++ .../party/iroiro/luajava/ClassPathLoader.java | 35 ++++++++++++++++++- .../party/iroiro/luajava/util/ClassUtils.java | 12 +++---- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 72c7e222..034fb58d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,31 @@ # Troubleshooting +## Class (or Resource) Not Found + +### Is a wrong classloader used? + +By default, LuaJava tries the following classloaders for class loading, +and chooses the first non-null one: + +1. `Thread.currentThread().getContextClassLoader()` +2. `party.iroiro.luajava.util.ClassUtils.class.getClassLoader()` +3. `ClassLoader.getSystemClassLoader()` + +This might not be optimal if your class loading environment is not set up in this hierarchical way. +You may override `party.iroiro.luajava.util.ClassUtils#DEFAULT_CLASS_LOADER` to use a different class loader. +(It is not documented in the Javadoc, since it is quite internal and subject to changes.) + +### Are you (mistakenly) using Java 9 modules? + +If you package a fat JAR with, for example, [shadow](https://github.com/GradleUp/shadow), +please note that older versions of the plugins may not prune the `module-info.class` from some of your dependencies, +potentially making the whole JAR a large module. + +It should be fine if the fat JAR is the only external JAR you load into the JVM. +But, if you plan to use this JAR as part of another application (e.g., as a plugin), +this can cause problems because the module system can restrict reflective access. +Try moving all `**/*/module-info.class` from your fat JAR. + ## JVM Crashed The crash is often followed by the following error message: diff --git a/luajava/src/main/java/party/iroiro/luajava/ClassPathLoader.java b/luajava/src/main/java/party/iroiro/luajava/ClassPathLoader.java index b5a22d4c..9dd3b455 100644 --- a/luajava/src/main/java/party/iroiro/luajava/ClassPathLoader.java +++ b/luajava/src/main/java/party/iroiro/luajava/ClassPathLoader.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import party.iroiro.luajava.util.ClassUtils; import java.io.ByteArrayOutputStream; import java.io.InputStream; @@ -32,12 +33,37 @@ import java.nio.ByteBuffer; import java.util.Objects; +/** + * An {@link ExternalLoader} that loads modules from classpath + * + *

+ * The path to the resource is converted from module name with + * {@link #getPath(String)}. For example, loading a {@code abc.def} module + * will load a Lua file at {@code classpath://abc/def.lua}. + *

+ */ public class ClassPathLoader implements ExternalLoader { + protected final ClassLoader classLoader; + + /** + * Use {@link ClassUtils#getDefaultClassLoader()} for resource loading + */ + public ClassPathLoader() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * @param classLoader the classloader for resource loading + */ + public ClassPathLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + @Override public @Nullable Buffer load(String module, Lua ignored) { try (InputStream resource = Objects.requireNonNull( // We use the class loader to load resources support loading from other Java modules. - getClass().getClassLoader().getResourceAsStream(getPath(module)) + classLoader.getResourceAsStream(getPath(module)) )) { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] bytes = new byte[4096]; @@ -57,10 +83,17 @@ public class ClassPathLoader implements ExternalLoader { } } + /** + * @param module dot separated module path + * @return module path with {@code .} replaced by {@code /}, appended with {@code .lua} + */ protected String getPath(String module) { return module.replace('.', '/') + ".lua"; } + /** + * An output stream used to convert a {@link ByteArrayOutputStream} to a {@link ByteBuffer} + */ public static class BufferOutputStream extends OutputStream { private final ByteBuffer buffer; diff --git a/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java b/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java index 3ca94065..10bf378f 100644 --- a/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java +++ b/luajava/src/main/java/party/iroiro/luajava/util/ClassUtils.java @@ -114,7 +114,8 @@ public abstract class ClassUtils { registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, Error.class, StackTraceElement.class, StackTraceElement[].class); registerCommonClasses(Enum.class, Iterable.class, Iterator.class, Enumeration.class, - Collection.class, List.class, Set.class, Map.class, Map.Entry.class, optionalClass()); + Collection.class, List.class, Set.class, Map.class, Map.Entry.class); + registerOptionalClasses(); Class[] javaLanguageInterfaceArray = {Serializable.class, Externalizable.class, Closeable.class, AutoCloseable.class, Cloneable.class, Comparable.class}; @@ -123,14 +124,11 @@ public abstract class ClassUtils { /** * {@code Class requires API level 24 (current min is 19): java.util.Optional} - * - * @return the class */ - private static @Nullable Class optionalClass() { + private static void registerOptionalClasses() { try { - return Class.forName("java.util.Optional"); - } catch (ClassNotFoundException e) { - return null; + registerCommonClasses(Class.forName("java.util.Optional")); + } catch (ClassNotFoundException ignored) { } } From 6e0dd11723cd5f3279c8c75e85aa10b73f892e8c Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Tue, 27 Aug 2024 17:43:01 +0800 Subject: [PATCH 4/4] test: test cached methods --- .../src/main/resources/suite/apiTest.lua | 13 +++ .../java/party/iroiro/luajava/JuaAPI.java | 85 +++++++------------ 2 files changed, 44 insertions(+), 54 deletions(-) diff --git a/example/suite/src/main/resources/suite/apiTest.lua b/example/suite/src/main/resources/suite/apiTest.lua index affdcd45..e4b578de 100644 --- a/example/suite/src/main/resources/suite/apiTest.lua +++ b/example/suite/src/main/resources/suite/apiTest.lua @@ -105,3 +105,16 @@ assert(currentThread ~= nil) -- Injected by the runner assertThrows('unable to detach a main state', java.detach, currentThread) subThread = coroutine.create(function() end) java.detach(subThread) + +--[[ + java.method + ]]-- +-- The following ensures coverage of method caching +BigInteger = java.import('java.math.BigInteger') +Constructor = java.method(BigInteger, 'new', 'java.lang.String') +integer1 = Constructor('100') +integer2 = Constructor('100') +assert(integer1:equals(integer2)) +added = java.method(integer1, 'add', 'java.math.BigInteger')(integer2) +added = java.method(added, 'add', 'java.math.BigInteger')(added) +assert(integer:intValue() == 400) diff --git a/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java b/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java index d8f51c51..e9ecd996 100644 --- a/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java +++ b/luajava/src/main/java/party/iroiro/luajava/JuaAPI.java @@ -384,6 +384,12 @@ public static int objectNewIndex(int index, Object obj, String name) { return fieldNewIndex(index, obj.getClass(), obj, name); } + private final static LRUCache, Boolean, Constructor[]> CONSTRUCTORS_CACHE = new LRUCache<>( + 25, + 1, + 4 + ); + /** * Constructs an instance of a class * @@ -411,7 +417,12 @@ public static int classNew(int index, Object oClazz, int paramCount) { } } Object[] objects = new Object[paramCount]; - Constructor constructor = matchMethod(L, clazz.getConstructors(), objects); + Constructor[] constructors = CONSTRUCTORS_CACHE.get(clazz, Boolean.TRUE); + if (constructors == null) { + constructors = clazz.getConstructors(); + CONSTRUCTORS_CACHE.put(clazz, Boolean.TRUE, constructors); + } + Constructor constructor = matchMethod(L, constructors, CONSTRUCTOR_WRAPPER, objects); if (constructor != null) { return construct(L, objects, constructor); } @@ -646,7 +657,7 @@ public static int methodInvoke(int index, Class clazz, @Nullable Object obj, methods = namedMethods.toArray(new Method[0]); MEMBER_METHOD_CACHE.put(clazz, name, methods); } - Method method = matchMethod(L, methods, name, objects); + Method method = matchMethod(L, methods, METHOD_WRAPPER, objects); if (method == null) { L.push("no matching method found"); return -1; @@ -674,7 +685,7 @@ public static int methodInvoke(int index, Class clazz, @Nullable Object obj, Constructor constructor = matchMethod(clazz, notSignature); if (constructor != null) { Object[] objects = new Object[paramCount]; - if (matchMethod(L, new Constructor[]{constructor}, objects) != null) { + if (matchMethod(L, new Constructor[]{constructor}, CONSTRUCTOR_WRAPPER, objects) != null) { return construct(L, objects, constructor); } } @@ -687,7 +698,7 @@ public static int methodInvoke(int index, Class clazz, @Nullable Object obj, Method method = matchMethod(clazz, name, notSignature); if (method != null) { Object[] objects = new Object[paramCount]; - if (matchMethod(L, new Method[]{method}, name, objects) != null) { + if (matchMethod(L, new Method[]{method}, METHOD_WRAPPER, objects) != null) { if (clazz.isInterface()) { return specialInvoke(L, method, obj, objects); } else { @@ -852,42 +863,10 @@ private static int fieldNewIndex(int index, Class clazz, Object object, Strin } } - /** - * See {@link #matchMethod(Lua, Object[], ExecutableWrapper, String, Object[])} - * - * @param L the lua state - * @param methods all the constructors - * @param params an array to store converted parameters - * @return a match method - */ - @Nullable - private static Constructor matchMethod(Lua L, Constructor[] methods, Object[] params) { - return matchMethod(L, methods, CONSTRUCTOR_WRAPPER, null, params); - } - - /** - * See {@link #matchMethod(Lua, Object[], ExecutableWrapper, String, Object[])} - * - * @param L the lua state - * @param methods all the constructors - * @param name the method name - * @param params an array to store converted parameters - * @return a match method - */ - @Nullable - private static Method matchMethod(Lua L, Method[] methods, - @Nullable String name, Object[] params) { - return matchMethod(L, methods, - METHOD_WRAPPER, - name, params); - } - /** * Matches methods against values on stack - * * @param L the lua state - * @param methods all the methods - * @param name the method name + * @param methods filtered methods that only differ in their parameters * @param params an array to store converted parameters * @param either {@link Method} or {@link Constructor} * @return a match method @@ -895,26 +874,24 @@ private static Method matchMethod(Lua L, Method[] methods, @Nullable private static T matchMethod(Lua L, T[] methods, ExecutableWrapper wrapper, - @Nullable String name, Object[] params) { + Object[] params) { for (T method : methods) { - if (name == null || name.equals(wrapper.getName(method))) { - /* - * This is costly since it clones the internal array. - * However, getParameterCount() is not available on Android 4.4 - * - * {@code Call requires API level 24 (current min is 19): java.lang.reflect.Method#isDefault} - */ - Class[] classes = wrapper.getParameterTypes(method); - if (classes.length == params.length) { - try { - for (int i = 0; i != params.length; ++i) { - params[i] = convertFromLua(L, classes[i], -params.length + i); - } - } catch (IllegalArgumentException e) { - continue; + /* + * This is costly since it clones the internal array. + * However, getParameterCount() is not available on Android 4.4 + * + * {@code Call requires API level 24 (current min is 19): java.lang.reflect.Method#isDefault} + */ + Class[] classes = wrapper.getParameterTypes(method); + if (classes.length == params.length) { + try { + for (int i = 0; i != params.length; ++i) { + params[i] = convertFromLua(L, classes[i], -params.length + i); } - return method; + } catch (IllegalArgumentException e) { + continue; } + return method; } } return null;