1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-09-29 23:40:46 +00:00

Replace ASM generation with MethodHandles

This is the second time I've rewritten our class generation in a little
over a month. Oh dear!

Back in d562a051c7 we started using method
handles inside our generated ASM, effectively replacing a direct call
with .invokeExact on a constant method handle.

This goes one step further and removes our ASM entirely, building up a
MethodHandle that checks arguments and then wraps the return value.
Rather than generating a class, we just return a new LuaFunction
instance that invokeExacts the method handle.

This is definitely slower than what we had before, but in the order of
8ns vs 12ns (in the worst case, sometimes they're much more comparable),
so I'm not too worried in practice.

However, generation of the actual method is now a bit faster. I've not
done any proper benchmarking, but it's about 20-30% faster.

This also gives us a bit more flexibility in the future, for instance
uisng bound MethodHandles in generation (e.g. for instance methods on
GenericSources). Not something I'm planning on doing right now, but is
an option.
This commit is contained in:
Jonathan Coates 2023-10-11 20:05:20 +01:00
parent bd327e37eb
commit 71669cf49c
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
8 changed files with 266 additions and 225 deletions

View File

@ -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)

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 <T> The type of the interface the generated classes implement.
*/
@ -49,47 +43,87 @@ final class Generator<T> {
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<Class<?>, 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<Class<?>, 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<Class<?>, 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<T> base;
private final List<Class<?>> context;
private final List<Class<?>> contextWithArguments;
private final MethodHandle argumentGetter;
private final List<MethodHandle> contextGetters;
private final String[] interfaces;
private final String methodDesc;
private final String classPrefix;
private final Function<MethodHandle, T> factory;
private final Function<T, T> wrap;
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
.newBuilder()
.build(CacheLoader.from(catching(this::build, Optional.empty())));
Generator(Class<T> base, List<Class<?>> context, Function<T, T> wrap) {
this.base = base;
Generator(List<Class<?>> context, Function<MethodHandle, T> factory, Function<T, T> 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<T> getMethod(Method method) {
@ -131,184 +165,144 @@ final class Generator<T> {
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<Type> 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, "<init>", "()V", null, null);
mw.visitCode();
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mw.visitInsn(RETURN);
mw.visitMaxs(0, 0);
mw.visitEnd();
private MethodHandle buildMethodHandle(Member method, MethodHandle handle, List<Type> 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<MethodHandle> 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, "<init>", "(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;
}

View File

@ -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<LuaMethod> GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class),
private static final Generator<LuaMethod> 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)));

View File

@ -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<PeripheralMethod> GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class),
private static final Generator<PeripheralMethod> 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)));

View File

@ -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);
}
}
}

View File

@ -22,4 +22,13 @@ final class ResultHelpers {
return result.getResult();
}
static RuntimeException throwUnchecked(Throwable t) {
return throwUnchecked0(t);
}
@SuppressWarnings("unchecked")
private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T {
throw (T) t;
}
}

View File

@ -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<LuaMethod> GENERATOR = (MethodSupplierImpl<LuaMethod>) LuaMethodSupplier.create(List.of());
private static final MethodSupplierImpl<LuaMethod> GENERATOR = (MethodSupplierImpl<LuaMethod>) 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")

View File

@ -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<T> {
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<T> {
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<T> {
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, "<init>", "(Ljava/lang/Object;)V", false);
return true;
@ -258,16 +260,16 @@ public final class StaticGenerator<T> {
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<T> {
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<T> {
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 <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
return x -> {