1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-26 07:03:22 +00:00

Allow generic sources to have instance methods

Rather than assuming static methods are generic, and instance methods
are direct, the Generator now has separate entrypoints for handling
instance and generic methods.

As a result of this change, we've also relaxed some of the validation
code. As a result, we now allow calling private/protected methods
which are annotated with @LuaFunction.
This commit is contained in:
Jonathan Coates 2023-11-22 09:44:16 +00:00
parent f8b7422294
commit fe826f5c9c
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
5 changed files with 139 additions and 79 deletions

View File

@ -11,11 +11,10 @@
* A generic source of {@link LuaFunction} functions.
* <p>
* Unlike normal objects ({@link IDynamicLuaObject} or {@link IPeripheral}), methods do not target this object but
* instead are defined as {@code static} and accept their target as the first parameter. This allows you to inject
* methods onto objects you do not own, as well as declaring methods for a specific "trait" (for instance, a Forge
* capability or Fabric block lookup interface).
* accept their target as the first parameter. This allows you to inject methods onto objects you do not own, as well as
* declaring methods for a specific "trait" (for instance, a Forge capability or Fabric block lookup interface).
* <p>
* Currently the "generic peripheral" system is incompatible with normal peripherals. Peripherals explicitly provided
* Currently, the "generic peripheral" system is incompatible with normal peripherals. Peripherals explicitly provided
* by capabilities/the block lookup API take priority. Block entities which use this system are given a peripheral name
* determined by their id, rather than any peripheral provider, though additional types may be provided by overriding
* {@link GenericPeripheral#getType()}.
@ -25,7 +24,7 @@
* <pre>{@code
* public class InventoryMethods implements GenericSource {
* \@LuaFunction( mainThread = true )
* public static int size(IItemHandler inventory) {
* public int size(IItemHandler inventory) {
* return inventory.getSlots();
* }
*

View File

@ -18,10 +18,7 @@
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.lang.reflect.Type;
import java.lang.reflect.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.function.Function;
@ -106,9 +103,14 @@ static void addArgType(Map<Class<?>, ArgMethods> types, Class<?> type, String na
private final Function<MethodHandle, T> factory;
private final Function<T, T> wrap;
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
private final LoadingCache<Method, Optional<T>> instanceCache = CacheBuilder
.newBuilder()
.build(CacheLoader.from(catching(this::build, Optional.empty())));
.build(CacheLoader.from(catching(this::buildInstanceMethod, Optional.empty())));
private final LoadingCache<GenericMethod, Optional<T>> genericCache = CacheBuilder
.newBuilder()
.weakKeys()
.build(CacheLoader.from(catching(this::buildGenericMethod, Optional.empty())));
Generator(List<Class<?>> context, Function<MethodHandle, T> factory, Function<T, T> wrap) {
this.context = context;
@ -131,65 +133,94 @@ static void addArgType(Map<Class<?>, ArgMethods> types, Class<?> type, String na
}
}
Optional<T> getMethod(Method method) {
return methodCache.getUnchecked(method);
Optional<T> getInstanceMethod(Method method) {
return instanceCache.getUnchecked(method);
}
private Optional<T> build(Method method) {
var name = method.getDeclaringClass().getName() + "." + method.getName();
var modifiers = method.getModifiers();
Optional<T> getGenericMethod(GenericMethod method) {
return genericCache.getUnchecked(method);
}
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) {
LOG.warn("Lua Method {} should be final.", name);
/**
* Check if a {@link LuaFunction}-annotated method can be used in this context.
*
* @param method The method to check.
* @return Whether the method is valid.
*/
private boolean checkMethod(Method method) {
if (method.isBridge()) {
LOG.debug("Skipping bridge Lua Method {}.{}", method.getDeclaringClass().getName(), method.getName());
return false;
}
if (!Modifier.isPublic(modifiers)) {
LOG.error("Lua Method {} should be a public method.", name);
return Optional.empty();
}
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
LOG.error("Lua Method {} should be on a public class.", name);
return Optional.empty();
}
LOG.debug("Generating method wrapper for {}.", name);
// Check we don't throw additional exceptions.
var exceptions = method.getExceptionTypes();
for (var exception : exceptions) {
if (exception != LuaException.class) {
LOG.error("Lua Method {} cannot throw {}.", name, exception.getName());
return Optional.empty();
LOG.error("Lua Method {}.{} cannot throw {}.", method.getDeclaringClass().getName(), method.getName(), exception.getName());
return false;
}
}
// unsafe can only be used on the computer thread, so reject it for mainThread functions.
var annotation = method.getAnnotation(LuaFunction.class);
if (annotation.unsafe() && annotation.mainThread()) {
LOG.error("Lua Method {} cannot use unsafe and mainThread", name);
return Optional.empty();
LOG.error("Lua Method {}.{} cannot use unsafe and mainThread.", method.getDeclaringClass().getName(), method.getName());
return false;
}
try {
var originalHandle = LOOKUP.unreflect(method);
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 handle = buildMethodHandle(method, originalHandle, parameters, annotation.unsafe());
if (handle == null) return Optional.empty();
var instance = factory.apply(handle);
return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance);
} catch (ReflectiveOperationException | RuntimeException e) {
LOG.error("Error generating wrapper for {}.", name, e);
return Optional.empty();
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
var modifiers = method.getModifiers();
if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) {
LOG.warn("Lua Method {}.{} should be final.", method.getDeclaringClass().getName(), method.getName());
}
return true;
}
private Optional<T> buildInstanceMethod(Method method) {
if (!checkMethod(method)) return Optional.empty();
var handle = tryUnreflect(method);
if (handle == null) return Optional.empty();
return build(method, handle, Arrays.asList(method.getGenericParameterTypes()));
}
private Optional<T> buildGenericMethod(GenericMethod method) {
if (!checkMethod(method.method)) return Optional.empty();
var handle = tryUnreflect(method.method);
if (handle == null) return Optional.empty();
var parameters = Arrays.asList(method.method.getGenericParameterTypes());
return build(
method.method,
Modifier.isStatic(method.method.getModifiers()) ? handle : handle.bindTo(method.source),
parameters.subList(1, parameters.size()) // Drop the instance argument.
);
}
/**
* Generate our {@link T} instance for a specific method.
* <p>
* This {@linkplain #buildMethodHandle(Member, MethodHandle, List, boolean)} builds the method handle, and then
* wraps it with {@link #factory}.
*
* @param method The original method, for reflection and error reporting.
* @param handle The method handle to execute.
* @param parameters The generic parameters to this method handle.
* @return The generated method, or {@link Optional#empty()} if an error occurred.
*/
private Optional<T> build(Method method, MethodHandle handle, List<Type> parameters) {
LOG.debug("Generating method wrapper for {}.{}.", method.getDeclaringClass().getName(), method.getName());
var annotation = method.getAnnotation(LuaFunction.class);
var wrappedHandle = buildMethodHandle(method, handle, parameters, annotation.unsafe());
if (wrappedHandle == null) return Optional.empty();
var instance = factory.apply(wrappedHandle);
return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance);
}
/**
@ -202,8 +233,7 @@ private Optional<T> build(Method method) {
* @param unsafe Whether to allow unsafe argument getters.
* @return The wrapped method handle.
*/
@Nullable
private MethodHandle buildMethodHandle(Member method, MethodHandle handle, List<Type> parameterTypes, boolean unsafe) {
private @Nullable 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");
}
@ -263,8 +293,7 @@ private MethodHandle buildMethodHandle(Member method, MethodHandle handle, List<
}
}
@Nullable
private static MethodHandle loadArg(Member method, boolean unsafe, Class<?> argType, Type genericArg, int argIndex) {
private static @Nullable 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;
@ -312,6 +341,22 @@ private static MethodHandle setReturn(MethodHandle handle, Class<?> retTy) {
return null;
}
/**
* A wrapper over {@link MethodHandles.Lookup#unreflect(Method)} which discards errors.
*
* @param method The method to unreflect.
* @return The resulting handle, or {@code null} if it cannot be unreflected.
*/
private static @Nullable MethodHandle tryUnreflect(Method method) {
try {
method.setAccessible(true);
return LOOKUP.unreflect(method);
} catch (SecurityException | InaccessibleObjectException | IllegalAccessException e) {
LOG.error("Lua Method {}.{} is not accessible.", method.getDeclaringClass().getName(), method.getName());
return null;
}
}
@SuppressWarnings("Guava")
static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
return x -> {
@ -320,7 +365,7 @@ static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> func
} catch (Exception | LinkageError e) {
// LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching
// methods on a class which references non-existent (i.e. client-only) types.
LOG.error("Error generating @LuaFunctions", e);
LOG.error("Error generating @LuaFunction for {}", x, e);
return def;
}
};

View File

@ -13,7 +13,6 @@
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
@ -56,16 +55,11 @@ public static Stream<GenericMethod> getMethods(GenericSource source) {
Class<?> klass = source.getClass();
var type = source instanceof GenericPeripheral generic ? generic.getType() : null;
return Arrays.stream(klass.getDeclaredMethods())
return Arrays.stream(klass.getMethods())
.map(method -> {
var annotation = method.getAnnotation(LuaFunction.class);
if (annotation == null) return null;
if (!Modifier.isStatic(method.getModifiers())) {
LOG.error("GenericSource method {}.{} should be static.", method.getDeclaringClass(), method.getName());
return null;
}
var types = method.getGenericParameterTypes();
if (types.length == 0) {
LOG.error("GenericSource method {}.{} has no parameters.", method.getDeclaringClass(), method.getName());

View File

@ -110,7 +110,7 @@ private List<NamedMethod<T>> getMethodsImpl(Class<?> klass) {
continue;
}
var instance = generator.getMethod(method).orElse(null);
var instance = generator.getInstanceMethod(method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();
@ -121,7 +121,7 @@ private List<NamedMethod<T>> getMethodsImpl(Class<?> klass) {
for (var method : genericMethods) {
if (!method.target.isAssignableFrom(klass)) continue;
var instance = generator.getMethod(method.method).orElse(null);
var instance = generator.getGenericMethod(method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();

View File

@ -22,6 +22,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static dan200.computercraft.test.core.ContramapMatcher.contramap;
import static org.hamcrest.MatcherAssert.assertThat;
@ -30,7 +31,7 @@
public class GeneratorTest {
private static final MethodSupplierImpl<LuaMethod> GENERATOR = (MethodSupplierImpl<LuaMethod>) LuaMethodSupplier.create(
GenericMethod.getMethods(new StaticMethod()).toList()
Stream.of(new StaticGeneric(), new InstanceGeneric()).flatMap(GenericMethod::getMethods).toList()
);
@Test
@ -65,8 +66,10 @@ public void testEmptyClass() {
}
@Test
public void testNonPublicClass() {
assertThat(GENERATOR.getMethods(NonPublic.class), is(empty()));
public void testNonPublicClass() throws LuaException {
var methods = GENERATOR.getMethods(NonPublic.class);
assertThat(methods, contains(named("go")));
assertThat(apply(methods, new NonPublic(), "go"), is(MethodResult.of()));
}
@Test
@ -75,10 +78,18 @@ public void testNonInstance() {
}
@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()));
public void testStaticGenericMethod() throws LuaException {
var methods = GENERATOR.getMethods(GenericMethodTarget.class);
assertThat(methods, hasItem(named("goStatic")));
assertThat(apply(methods, new GenericMethodTarget(), "goStatic", "Hello", 123), is(MethodResult.of()));
}
@Test
public void testInstanceGenericrMethod() throws LuaException {
var methods = GENERATOR.getMethods(GenericMethodTarget.class);
assertThat(methods, hasItem(named("goInstance")));
assertThat(apply(methods, new GenericMethodTarget(), "goInstance", "Hello", 123), is(MethodResult.of()));
}
@Test
@ -181,17 +192,28 @@ public static void go() {
}
}
public static class StaticMethodTarget {
public static class GenericMethodTarget {
}
public static class StaticMethod implements GenericSource {
public static class StaticGeneric implements GenericSource {
@Override
public String id() {
return "source";
return "static";
}
@LuaFunction
public static void go(StaticMethodTarget target, String arg1, int arg2, ILuaContext context) {
public static void goStatic(GenericMethodTarget target, String arg1, int arg2, ILuaContext context) {
}
}
public static class InstanceGeneric implements GenericSource {
@Override
public String id() {
return "instance";
}
@LuaFunction
public void goInstance(GenericMethodTarget target, String arg1, int arg2, ILuaContext context) {
}
}