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 -> {