diff --git a/projects/core/build.gradle.kts b/projects/core/build.gradle.kts index fc84c1771..01996a83e 100644 --- a/projects/core/build.gradle.kts +++ b/projects/core/build.gradle.kts @@ -24,13 +24,13 @@ dependencies { implementation(libs.netty.socks) implementation(libs.netty.proxy) implementation(libs.slf4j) - implementation(libs.asm) testFixturesImplementation(libs.slf4j) testFixturesApi(platform(libs.kotlin.platform)) testFixturesApi(libs.bundles.test) testFixturesApi(libs.bundles.kotlin) + testImplementation(libs.asm) testImplementation(libs.bundles.test) testRuntimeOnly(libs.bundles.testRuntime) testRuntimeOnly(libs.slf4j.simple) diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java index e2ad40241..6d7c76ea5 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -11,37 +11,31 @@ import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import dan200.computercraft.api.lua.*; import dan200.computercraft.core.methods.LuaMethod; -import org.objectweb.asm.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import java.lang.constant.ConstantDescs; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Optional; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.util.*; import java.util.function.Function; -import static org.objectweb.asm.Opcodes.*; - /** * The underlying generator for {@link LuaFunction}-annotated methods. *

- * The constructor {@link Generator#Generator(Class, List, Function)} takes in the type of interface to generate (i.e. - * {@link LuaMethod}), the context arguments for this function (in the case of {@link LuaMethod}, this will just be - * {@link ILuaContext}) and a "wrapper" function to lift a function to execute on the main thread. + * The constructor {@link Generator#Generator(List, Function, Function)} takes in the type of interface to generate + * (i.e. {@link LuaMethod}), the context arguments for this function (in the case of {@link LuaMethod}, this will just + * be {@link ILuaContext}), a factory function (which invokes a method handle), and a "wrapper" function to lift a + * function to execute on the main thread. *

- * The generated class then implements this interface - the {@code apply} method calls the appropriate methods on - * {@link IArguments} to extract the arguments, and then calls the original method. - *

- * As the method is not guaranteed to come from the same classloader, we cannot call the method directly, as that may - * result in linkage errors. We instead inject a {@link MethodHandle} into the class as a dynamic constant, and then - * call the method with {@link MethodHandle#invokeExact(Object...)}. The method handle is constant, and so this has - * equivalent performance to the direct call. + * For each input function, the generator then fabricates a {@link MethodHandle} which performs the argument validation, + * and then calls the factory function to convert it to the desired interface. * * @param The type of the interface the generated classes implement. */ @@ -49,47 +43,87 @@ final class Generator { private static final Logger LOG = LoggerFactory.getLogger(Generator.class); private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - private static final String METHOD_NAME = "apply"; - private static final String[] EXCEPTIONS = new String[]{ Type.getInternalName(LuaException.class) }; + private static final MethodHandle METHOD_RESULT_OF_VOID, METHOD_RESULT_OF_ONE, METHOD_RESULT_OF_MANY; - private static final String INTERNAL_METHOD_RESULT = Type.getInternalName(MethodResult.class); - private static final String DESC_METHOD_RESULT = Type.getDescriptor(MethodResult.class); + private static final Map, ArgMethods> argMethods; + private static final ArgMethods ARG_TABLE_UNSAFE; + private static final MethodHandle ARG_GET_OBJECT, ARG_GET_ENUM, ARG_OPT_ENUM, ARG_GET_STRING_COERCED; - private static final String INTERNAL_ARGUMENTS = Type.getInternalName(IArguments.class); - private static final String DESC_ARGUMENTS = Type.getDescriptor(IArguments.class); + private record ArgMethods(MethodHandle get, MethodHandle opt) { + public static ArgMethods of(Class type, String name) throws ReflectiveOperationException { + return new ArgMethods( + LOOKUP.findVirtual(IArguments.class, "get" + name, MethodType.methodType(type, int.class)), + LOOKUP.findVirtual(IArguments.class, "opt" + name, MethodType.methodType(Optional.class, int.class)) + ); + } + } - private static final String INTERNAL_COERCED = Type.getInternalName(Coerced.class); + static void addArgType(Map, ArgMethods> types, Class type, String name) throws ReflectiveOperationException { + types.put(type, ArgMethods.of(type, name)); + } - private static final ConstantDynamic METHOD_CONSTANT = new ConstantDynamic(ConstantDescs.DEFAULT_NAME, MethodHandle.class.descriptorString(), new Handle( - H_INVOKESTATIC, Type.getInternalName(MethodHandles.class), "classData", - MethodType.methodType(Object.class, MethodHandles.Lookup.class, String.class, Class.class).descriptorString(), false - )); + static { + try { + METHOD_RESULT_OF_VOID = LOOKUP.findStatic(MethodResult.class, "of", MethodType.methodType(MethodResult.class)); + METHOD_RESULT_OF_ONE = LOOKUP.findStatic(MethodResult.class, "of", MethodType.methodType(MethodResult.class, Object.class)); + METHOD_RESULT_OF_MANY = LOOKUP.findStatic(MethodResult.class, "of", MethodType.methodType(MethodResult.class, Object[].class)); + + Map, ArgMethods> argMethodMap = new HashMap<>(); + addArgType(argMethodMap, int.class, "Int"); + addArgType(argMethodMap, boolean.class, "Boolean"); + addArgType(argMethodMap, double.class, "Double"); + addArgType(argMethodMap, long.class, "Long"); + addArgType(argMethodMap, Map.class, "Table"); + addArgType(argMethodMap, String.class, "String"); + addArgType(argMethodMap, ByteBuffer.class, "Bytes"); + argMethods = Map.copyOf(argMethodMap); + + ARG_TABLE_UNSAFE = ArgMethods.of(LuaTable.class, "TableUnsafe"); + ARG_GET_OBJECT = LOOKUP.findVirtual(IArguments.class, "get", MethodType.methodType(Object.class, int.class)); + ARG_GET_ENUM = LOOKUP.findVirtual(IArguments.class, "getEnum", MethodType.methodType(Enum.class, int.class, Class.class)); + ARG_OPT_ENUM = LOOKUP.findVirtual(IArguments.class, "optEnum", MethodType.methodType(Optional.class, int.class, Class.class)); + + // Create a new Coerced<>(args.getStringCoerced(_)) function. + ARG_GET_STRING_COERCED = MethodHandles.filterReturnValue( + setReturn(LOOKUP.findVirtual(IArguments.class, "getStringCoerced", MethodType.methodType(String.class, int.class)), Object.class), + LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class)) + ); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } - private final Class base; private final List> context; + private final List> contextWithArguments; + private final MethodHandle argumentGetter; + private final List contextGetters; - private final String[] interfaces; - private final String methodDesc; - private final String classPrefix; - + private final Function factory; private final Function wrap; private final LoadingCache> methodCache = CacheBuilder .newBuilder() .build(CacheLoader.from(catching(this::build, Optional.empty()))); - Generator(Class base, List> context, Function wrap) { - this.base = base; + Generator(List> context, Function factory, Function wrap) { this.context = context; - interfaces = new String[]{ Type.getInternalName(base) }; + this.factory = factory; this.wrap = wrap; - var methodDesc = new StringBuilder().append("(Ljava/lang/Object;"); - for (var klass : context) methodDesc.append(Type.getDescriptor(klass)); - methodDesc.append(DESC_ARGUMENTS).append(")").append(DESC_METHOD_RESULT); - this.methodDesc = methodDesc.toString(); + var contextWithArguments = this.contextWithArguments = new ArrayList<>(context.size() + 1); + contextWithArguments.addAll(context); + contextWithArguments.add(IArguments.class); - classPrefix = Generator.class.getPackageName() + "." + base.getSimpleName() + "$"; + // Prepare a series of getters of the type (context..., IArguments) -> _ (or some prefix of this), for + // extracting a single context value. + argumentGetter = MethodHandles.dropArguments(MethodHandles.identity(IArguments.class), 0, context); + + var contextGetters = this.contextGetters = new ArrayList<>(context.size()); + for (var i = 0; i < context.size(); i++) { + var getter = MethodHandles.identity(context.get(i)); + if (i > 0) getter = MethodHandles.dropArguments(getter, 0, contextWithArguments.subList(0, i)); + contextGetters.add(getter); + } } Optional getMethod(Method method) { @@ -131,184 +165,144 @@ final class Generator { return Optional.empty(); } - // We have some rather ugly handling of static methods in both here and the main generate function. Static methods - // only come from generic sources, so this should be safe. - var target = Modifier.isStatic(modifiers) ? method.getParameterTypes()[0] : method.getDeclaringClass(); - try { - var handle = LOOKUP.unreflect(method); + var originalHandle = LOOKUP.unreflect(method); - // Convert the handle from one of the form (target, ...) -> ret type to (Object, ...) -> Object. This both - // handles the boxing of primitives for us, and ensures our bytecode does not reference any external types. - // We could handle the conversion to MethodResult here too, but it doesn't feel worth it. - var widenedHandle = handle.asType(widenMethodType(handle.type(), target)); + List parameters; + if (Modifier.isStatic(modifiers)) { + var allParameters = method.getGenericParameterTypes(); + parameters = Arrays.asList(allParameters).subList(1, allParameters.length); + } else { + parameters = Arrays.asList(method.getGenericParameterTypes()); + } - var bytes = generate(classPrefix + method.getName(), target, method, widenedHandle.type().descriptorString(), annotation.unsafe()); - if (bytes == null) return Optional.empty(); + var handle = buildMethodHandle(method, originalHandle, parameters, annotation.unsafe()); + if (handle == null) return Optional.empty(); - var klass = LOOKUP.defineHiddenClassWithClassData(bytes, widenedHandle, true).lookupClass(); - - var instance = klass.asSubclass(base).getDeclaredConstructor().newInstance(); + var instance = factory.apply(handle); return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance); - } catch (ReflectiveOperationException | ClassFormatError | RuntimeException e) { + } catch (ReflectiveOperationException | RuntimeException e) { LOG.error("Error generating wrapper for {}.", name, e); return Optional.empty(); } } - private static MethodType widenMethodType(MethodType source, Class target) { - // Treat the target argument as just Object - we'll do the cast in the method handle. - var args = source.parameterArray(); - for (var i = 0; i < args.length; i++) { - if (args[i] == target) args[i] = Object.class; - } - - // And convert the return value to Object if needed. - var ret = source.returnType(); - return ret == void.class || ret == MethodResult.class || ret == Object[].class - ? MethodType.methodType(ret, args) - : MethodType.methodType(Object.class, args); - } - + /** + * Convert the given handle from type {@code (target, args...) -> ret} to {@code (Object, context..., IArguments) -> MethodResult}, + * inserting calls to {@link IArguments}'s getters, and wrapping the result with {@link MethodResult#of()}. + * + * @param method The original method, for error reporting. + * @param handle The method handle to wrap. + * @param parameterTypes The generic parameter types to this method. This should have the same type as the {@code handle}. + * @param unsafe Whether to allow unsafe argument getters. + * @return The wrapped method handle. + */ @Nullable - private byte[] generate(String className, Class target, Method targetMethod, String targetDescriptor, boolean unsafe) { - var internalName = className.replace(".", "/"); - - // Construct a public final class which extends Object and implements MethodInstance.Delegate - var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - cw.visit(V17, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces); - cw.visitSource("CC generated method", null); - - { // Constructor just invokes super. - var mw = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); - mw.visitCode(); - mw.visitVarInsn(ALOAD, 0); - mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); - mw.visitInsn(RETURN); - mw.visitMaxs(0, 0); - mw.visitEnd(); + private MethodHandle buildMethodHandle(Member method, MethodHandle handle, List parameterTypes, boolean unsafe) { + if (handle.type().parameterCount() != parameterTypes.size() + 1) { + throw new IllegalArgumentException("Argument lists are mismatched"); } - { - var mw = cw.visitMethod(ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS); - mw.visitCode(); + // We start off with a method handle of type (target, args...) -> _. We then append the context and IArguments + // to the end, leaving a handle with type (target, args..., context..., IArguments) -> _. + handle = MethodHandles.dropArguments(handle, handle.type().parameterCount(), contextWithArguments); - mw.visitLdcInsn(METHOD_CONSTANT); + // Then for each argument, generate a method handle of type (context..., IArguments) -> _, which is used to + // extract this argument. + var argCount = 0; + List argSelectors = new ArrayList<>(parameterTypes.size()); + for (var paramType : parameterTypes) { + var paramClass = Reflect.getRawType(method, paramType, true); + if (paramClass == null) return null; - // If we're an instance method, load the target as the first argument. - if (!Modifier.isStatic(targetMethod.getModifiers())) mw.visitVarInsn(ALOAD, 1); - - var argIndex = 0; - for (var genericArg : targetMethod.getGenericParameterTypes()) { - var loadedArg = loadArg(mw, target, targetMethod, unsafe, genericArg, argIndex); - if (loadedArg == null) return null; - if (loadedArg) argIndex++; - } - - mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", targetDescriptor, false); - - // We allow a reasonable amount of flexibility on the return value's type. Alongside the obvious MethodResult, - // we convert basic types into an immediate result. - var ret = targetMethod.getReturnType(); - if (ret != MethodResult.class) { - if (ret == void.class) { - mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "()" + DESC_METHOD_RESULT, false); - } else if (ret == Object[].class) { - mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "([Ljava/lang/Object;)" + DESC_METHOD_RESULT, false); + // We first generate a method handle of type (context..., IArguments) -> _, which is used to extract this + // argument. + MethodHandle argSelector; + if (paramClass == IArguments.class) { + argSelector = argumentGetter; + } else { + var idx = context.indexOf(paramClass); + if (idx >= 0) { + argSelector = contextGetters.get(idx); } else { - mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false); + var selector = loadArg(method, unsafe, paramClass, paramType, argCount++); + if (selector == null) return null; + argSelector = MethodHandles.filterReturnValue(argumentGetter, selector); } } - mw.visitInsn(ARETURN); - - mw.visitMaxs(0, 0); - mw.visitEnd(); + argSelectors.add(argSelector); } - cw.visitEnd(); + // Fold over the original method's arguments, excluding the target in reverse. For each argument, we reduce + // a method of type type (target, args..., arg_n, context..., IArguments) -> _ to (target, args..., context..., IArguments) -> _ + // until eventually we've flattened the whole list. + for (var i = parameterTypes.size() - 1; i >= 0; i--) { + handle = MethodHandles.foldArguments(handle, i + 1, argSelectors.get(i)); + } - return cw.toByteArray(); + // Then cast the target to Object, so it's compatible with the desired type. + handle = handle.asType(handle.type().changeParameterType(0, Object.class)); + + // Finally wrap the returned value into a MethodResult. + var type = handle.type(); + var ret = type.returnType(); + if (ret == MethodResult.class) { + return handle; + } else if (ret == void.class) { + return MethodHandles.filterReturnValue(handle, METHOD_RESULT_OF_VOID); + } else if (ret == Object[].class) { + return MethodHandles.filterReturnValue(handle, METHOD_RESULT_OF_MANY); + } else { + return MethodHandles.filterReturnValue(handle.asType(type.changeReturnType(Object.class)), METHOD_RESULT_OF_ONE); + } } @Nullable - private Boolean loadArg(MethodVisitor mw, Class target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex) { - if (genericArg == target) { - mw.visitVarInsn(ALOAD, 1); - return false; - } - - var arg = Reflect.getRawType(method, genericArg, true); - if (arg == null) return null; - - if (arg == IArguments.class) { - mw.visitVarInsn(ALOAD, 2 + context.size()); - return false; - } - - var idx = context.indexOf(arg); - if (idx >= 0) { - mw.visitVarInsn(ALOAD, 2 + idx); - return false; - } - - if (arg == Coerced.class) { + private static MethodHandle loadArg(Member method, boolean unsafe, Class argType, Type genericArg, int argIndex) { + if (argType == Coerced.class) { var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.COERCED_IN).getType(), false); if (klass == null) return null; - if (klass == String.class) { - mw.visitTypeInsn(NEW, INTERNAL_COERCED); - mw.visitInsn(DUP); - mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); - mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getStringCoerced", "(I)Ljava/lang/String;", true); - mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "", "(Ljava/lang/Object;)V", false); - return true; - } + if (klass == String.class) return MethodHandles.insertArguments(ARG_GET_STRING_COERCED, 1, argIndex); } - if (arg == Optional.class) { - var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false); - if (klass == null) return null; + if (argType == Optional.class) { + var optType = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false); + if (optType == null) return null; - if (Enum.class.isAssignableFrom(klass) && klass != Enum.class) { - mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); - mw.visitLdcInsn(Type.getType(klass)); - mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;", true); - return true; + if (Enum.class.isAssignableFrom(optType) && optType != Enum.class) { + return MethodHandles.insertArguments(ARG_OPT_ENUM, 1, argIndex, optType); } - var name = Reflect.getLuaName(Primitives.unwrap(klass), unsafe); - if (name != null) { - mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); - mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true); - return true; - } + var getter = getArgMethods(Primitives.unwrap(optType), unsafe); + if (getter != null) return MethodHandles.insertArguments(getter.opt(), 1, argIndex); } - if (Enum.class.isAssignableFrom(arg) && arg != Enum.class) { - mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); - mw.visitLdcInsn(Type.getType(arg)); - mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;", true); - mw.visitTypeInsn(CHECKCAST, Type.getInternalName(arg)); - return true; + if (Enum.class.isAssignableFrom(argType) && argType != Enum.class) { + return setReturn(MethodHandles.insertArguments(ARG_GET_ENUM, 1, argIndex, argType), argType); } - var name = arg == Object.class ? "" : Reflect.getLuaName(arg, unsafe); - if (name != null) { - if (Reflect.getRawType(method, genericArg, false) == null) return null; + if (argType == Object.class) return MethodHandles.insertArguments(ARG_GET_OBJECT, 1, argIndex); - mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); - mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor(arg), true); - return true; - } + // Check we don't have a non-wildcard generic. + if (Reflect.getRawType(method, genericArg, false) == null) return null; - LOG.error("Unknown parameter type {} for method {}.{}.", - arg.getName(), method.getDeclaringClass().getName(), method.getName()); + var getter = getArgMethods(argType, unsafe); + if (getter != null) return MethodHandles.insertArguments(getter.get(), 1, argIndex); + + LOG.error("Unknown parameter type {} for method {}.{}.", argType.getName(), method.getDeclaringClass().getName(), method.getName()); + return null; + } + + private static MethodHandle setReturn(MethodHandle handle, Class retTy) { + return handle.asType(handle.type().changeReturnType(retTy)); + } + + private static @Nullable ArgMethods getArgMethods(Class type, boolean unsafe) { + var getter = argMethods.get(type); + if (getter != null) return getter; + if (type == LuaTable.class && unsafe) return ARG_TABLE_UNSAFE; return null; } diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java index 7058ed5b5..86a634be6 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/LuaMethodSupplier.java @@ -6,6 +6,7 @@ package dan200.computercraft.core.asm; import dan200.computercraft.api.lua.IDynamicLuaObject; import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.methods.LuaMethod; import dan200.computercraft.core.methods.MethodSupplier; @@ -20,7 +21,14 @@ import java.util.Objects; * method supplier}. It should not be used directly. */ public final class LuaMethodSupplier { - private static final Generator GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class), + private static final Generator GENERATOR = new Generator<>(List.of(ILuaContext.class), + m -> (target, context, args) -> { + try { + return (MethodResult) m.invokeExact(target, context, args); + } catch (Throwable t) { + throw ResultHelpers.throwUnchecked(t); + } + }, m -> (target, context, args) -> { var escArgs = args.escapes(); return context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, escArgs))); diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java index f03a5ac77..c282494f1 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java @@ -5,6 +5,7 @@ package dan200.computercraft.core.asm; import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IDynamicPeripheral; import dan200.computercraft.core.ComputerContext; @@ -21,7 +22,14 @@ import java.util.Objects; * method supplier}. It should not be used directly. */ public final class PeripheralMethodSupplier { - private static final Generator GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class), + private static final Generator GENERATOR = new Generator<>(List.of(ILuaContext.class, IComputerAccess.class), + m -> (target, context, computer, args) -> { + try { + return (MethodResult) m.invokeExact(target, context, computer, args); + } catch (Throwable t) { + throw ResultHelpers.throwUnchecked(t); + } + }, m -> (target, context, computer, args) -> { var escArgs = args.escapes(); return context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, escArgs))); diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/Reflect.java b/projects/core/src/main/java/dan200/computercraft/core/asm/Reflect.java index 5ffa1ff32..7c14a578c 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/Reflect.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/Reflect.java @@ -5,19 +5,13 @@ package dan200.computercraft.core.asm; import dan200.computercraft.api.lua.Coerced; -import dan200.computercraft.api.lua.LuaTable; -import org.objectweb.asm.MethodVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.lang.reflect.*; -import java.nio.ByteBuffer; -import java.util.Map; import java.util.Optional; -import static org.objectweb.asm.Opcodes.ICONST_0; - final class Reflect { private static final Logger LOG = LoggerFactory.getLogger(Reflect.class); static final java.lang.reflect.Type OPTIONAL_IN = Optional.class.getTypeParameters()[0]; @@ -27,24 +21,7 @@ final class Reflect { } @Nullable - static String getLuaName(Class klass, boolean unsafe) { - if (klass.isPrimitive()) { - if (klass == int.class) return "Int"; - if (klass == boolean.class) return "Boolean"; - if (klass == double.class) return "Double"; - if (klass == long.class) return "Long"; - } else { - if (klass == Map.class) return "Table"; - if (klass == String.class) return "String"; - if (klass == ByteBuffer.class) return "Bytes"; - if (klass == LuaTable.class && unsafe) return "TableUnsafe"; - } - - return null; - } - - @Nullable - static Class getRawType(Method method, Type root, boolean allowParameter) { + static Class getRawType(Member method, Type root, boolean allowParameter) { var underlying = root; while (true) { if (underlying instanceof Class klass) return klass; @@ -71,12 +48,4 @@ final class Reflect { return null; } } - - static void loadInt(MethodVisitor visitor, int value) { - if (value >= -1 && value <= 5) { - visitor.visitInsn(ICONST_0 + value); - } else { - visitor.visitLdcInsn(value); - } - } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java b/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java index 2b9d58c0a..f56d1246e 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/ResultHelpers.java @@ -22,4 +22,13 @@ final class ResultHelpers { return result.getResult(); } + + static RuntimeException throwUnchecked(Throwable t) { + return throwUnchecked0(t); + } + + @SuppressWarnings("unchecked") + private static T throwUnchecked0(Throwable t) throws T { + throw (T) t; + } } diff --git a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java index 7d7d7638b..44322083f 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -18,7 +18,10 @@ import org.objectweb.asm.Opcodes; import java.io.IOException; import java.lang.invoke.MethodHandles; -import java.util.*; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import static dan200.computercraft.test.core.ContramapMatcher.contramap; import static org.hamcrest.MatcherAssert.assertThat; @@ -26,7 +29,9 @@ import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; public class GeneratorTest { - private static final MethodSupplierImpl GENERATOR = (MethodSupplierImpl) LuaMethodSupplier.create(List.of()); + private static final MethodSupplierImpl GENERATOR = (MethodSupplierImpl) LuaMethodSupplier.create( + GenericMethod.getMethods(new StaticMethod()).toList() + ); @Test public void testBasic() { @@ -69,6 +74,13 @@ public class GeneratorTest { assertThat(GENERATOR.getMethods(NonInstance.class), is(empty())); } + @Test + public void testStaticMethod() throws LuaException { + var methods = GENERATOR.getMethods(StaticMethodTarget.class); + assertThat(methods, contains(named("go"))); + assertThat(apply(methods, new StaticMethodTarget(), "go", "Hello", 123), is(MethodResult.of())); + } + @Test public void testIllegalThrows() { assertThat(GENERATOR.getMethods(IllegalThrows.class), is(empty())); @@ -169,6 +181,20 @@ public class GeneratorTest { } } + public static class StaticMethodTarget { + } + + public static class StaticMethod implements GenericSource { + @Override + public String id() { + return "source"; + } + + @LuaFunction + public static void go(StaticMethodTarget target, String arg1, int arg2, ILuaContext context) { + } + } + public static class IllegalThrows { @LuaFunction @SuppressWarnings("DoNotCallSuggester") diff --git a/projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java b/projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java index aa95f2d27..066ffc3ef 100644 --- a/projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java +++ b/projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java @@ -19,7 +19,9 @@ import org.teavm.metaprogramming.ReflectClass; import javax.annotation.Nullable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -39,7 +41,7 @@ import static org.objectweb.asm.Opcodes.*; */ public final class StaticGenerator { private static final String METHOD_NAME = "apply"; - private static final String[] EXCEPTIONS = new String[]{Type.getInternalName(LuaException.class)}; + private static final String[] EXCEPTIONS = new String[]{ Type.getInternalName(LuaException.class) }; private static final String INTERNAL_METHOD_RESULT = Type.getInternalName(MethodResult.class); private static final String DESC_METHOD_RESULT = Type.getDescriptor(MethodResult.class); @@ -67,7 +69,7 @@ public final class StaticGenerator { this.context = context; this.createClass = createClass; - interfaces = new String[]{Type.getInternalName(base)}; + interfaces = new String[]{ Type.getInternalName(base) }; var methodDesc = new StringBuilder().append("(Ljava/lang/Object;"); for (var klass : context) methodDesc.append(Type.getDescriptor(klass)); @@ -245,7 +247,7 @@ public final class StaticGenerator { mw.visitTypeInsn(NEW, INTERNAL_COERCED); mw.visitInsn(DUP); mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); + loadInt(mw, argIndex); mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getStringCoerced", "(I)Ljava/lang/String;", true); mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "", "(Ljava/lang/Object;)V", false); return true; @@ -258,16 +260,16 @@ public final class StaticGenerator { if (Enum.class.isAssignableFrom(klass) && klass != Enum.class) { mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); + loadInt(mw, argIndex); mw.visitLdcInsn(Type.getType(klass)); mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;", true); return true; } - var name = Reflect.getLuaName(Primitives.unwrap(klass), unsafe); + var name = getLuaName(Primitives.unwrap(klass), unsafe); if (name != null) { mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); + loadInt(mw, argIndex); mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true); return true; } @@ -275,19 +277,19 @@ public final class StaticGenerator { if (Enum.class.isAssignableFrom(arg) && arg != Enum.class) { mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); + loadInt(mw, argIndex); mw.visitLdcInsn(Type.getType(arg)); mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;", true); mw.visitTypeInsn(CHECKCAST, Type.getInternalName(arg)); return true; } - var name = arg == Object.class ? "" : Reflect.getLuaName(arg, unsafe); + var name = arg == Object.class ? "" : getLuaName(arg, unsafe); if (name != null) { if (Reflect.getRawType(method, genericArg, false) == null) return null; mw.visitVarInsn(ALOAD, 2 + context.size()); - Reflect.loadInt(mw, argIndex); + loadInt(mw, argIndex); mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor(arg), true); return true; } @@ -297,6 +299,31 @@ public final class StaticGenerator { return null; } + @Nullable + private static String getLuaName(Class klass, boolean unsafe) { + if (klass.isPrimitive()) { + if (klass == int.class) return "Int"; + if (klass == boolean.class) return "Boolean"; + if (klass == double.class) return "Double"; + if (klass == long.class) return "Long"; + } else { + if (klass == Map.class) return "Table"; + if (klass == String.class) return "String"; + if (klass == ByteBuffer.class) return "Bytes"; + if (klass == LuaTable.class && unsafe) return "TableUnsafe"; + } + + return null; + } + + private static void loadInt(MethodVisitor visitor, int value) { + if (value >= -1 && value <= 5) { + visitor.visitInsn(ICONST_0 + value); + } else { + visitor.visitLdcInsn(value); + } + } + @SuppressWarnings("Guava") static com.google.common.base.Function catching(Function function, U def) { return x -> {