1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-16 10:09:55 +00:00

Make Generic methods per-ComputerContext

- Move the class cache out of Generator into MethodSupplierImpl. This
   means we cache class generation globally (that's really expensive!),
   but the class -> method list lookup is local.

 - Move the global GenericSource/GenericMethod registry out of core,
   passing in the list of generic methods to the ComputerContext.

I'm not entirely thrilled by the slight overlap of MethodSupplierImpl and
Generator here, something to clean up in the future.
This commit is contained in:
Jonathan Coates 2023-06-26 21:46:55 +01:00
parent 591a7eca23
commit 910a63214e
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
12 changed files with 193 additions and 105 deletions

View File

@ -19,7 +19,6 @@
import dan200.computercraft.api.redstone.BundledRedstoneProvider;
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.core.asm.GenericMethod;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.impl.network.wired.WiredNodeImpl;
@ -78,7 +77,7 @@ public final WritableMount createSaveDirMount(MinecraftServer server, String sub
@Override
public final void registerGenericSource(GenericSource source) {
GenericMethod.register(source);
GenericSources.register(source);
}
@Override

View File

@ -11,19 +11,24 @@
import java.util.LinkedHashSet;
import java.util.Objects;
/**
* The global factory for {@link ILuaAPIFactory}s.
*
* @see dan200.computercraft.core.ComputerContext.Builder#apiFactories(Collection)
* @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory)
*/
public final class ApiFactories {
private ApiFactories() {
}
private static final Collection<ILuaAPIFactory> factories = new LinkedHashSet<>();
private static final Collection<ILuaAPIFactory> factoriesView = Collections.unmodifiableCollection(factories);
public static synchronized void register(ILuaAPIFactory factory) {
static synchronized void register(ILuaAPIFactory factory) {
Objects.requireNonNull(factory, "provider cannot be null");
factories.add(factory);
}
public static Collection<ILuaAPIFactory> getAll() {
return factoriesView;
return Collections.unmodifiableCollection(factories);
}
}

View File

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.core.asm.GenericMethod;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
/**
* The global registry for {@link GenericSource}s.
*
* @see dan200.computercraft.core.ComputerContext.Builder#genericMethods(Collection)
* @see dan200.computercraft.api.ComputerCraftAPI#registerGenericSource(GenericSource)
*/
public final class GenericSources {
private GenericSources() {
}
private static final Collection<GenericSource> sources = new LinkedHashSet<>();
static synchronized void register(GenericSource source) {
Objects.requireNonNull(source, "provider cannot be null");
sources.add(source);
}
public static Collection<GenericMethod> getAllMethods() {
return sources.stream().flatMap(GenericMethod::getMethods).toList();
}
}

View File

@ -18,6 +18,7 @@
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.impl.AbstractComputerCraftAPI;
import dan200.computercraft.impl.ApiFactories;
import dan200.computercraft.impl.GenericSources;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import dan200.computercraft.shared.config.ConfigSpec;
@ -74,6 +75,7 @@ private ServerContext(MinecraftServer server) {
.mainThreadScheduler(mainThread)
.luaFactory(luaMachine)
.apiFactories(ApiFactories.getAll())
.genericMethods(GenericSources.getAllMethods())
.build();
idAssigner = new IDAssigner(storageDir.resolve("ids.json"));
}

View File

@ -5,6 +5,7 @@
package dan200.computercraft.core;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.core.asm.GenericMethod;
import dan200.computercraft.core.asm.LuaMethodSupplier;
import dan200.computercraft.core.asm.PeripheralMethodSupplier;
import dan200.computercraft.core.computer.ComputerThread;
@ -165,6 +166,7 @@ public static class Builder {
private @Nullable MainThreadScheduler mainThreadScheduler;
private @Nullable ILuaMachine.Factory luaFactory;
private @Nullable List<ILuaAPIFactory> apiFactories;
private @Nullable List<GenericMethod> genericMethods;
Builder(GlobalEnvironment environment) {
this.environment = environment;
@ -225,6 +227,21 @@ public Builder apiFactories(Collection<ILuaAPIFactory> apis) {
return this;
}
/**
* Set the set of {@link GenericMethod}s used by the {@linkplain MethodSupplier method suppliers}.
*
* @param genericMethods A list of API factories.
* @return {@code this}, for chaining
* @see ComputerContext#luaMethods()
* @see ComputerContext#peripheralMethods()
*/
public Builder genericMethods(Collection<GenericMethod> genericMethods) {
Objects.requireNonNull(genericMethods);
if (this.genericMethods != null) throw new IllegalStateException("Main-thread scheduler already specified");
this.genericMethods = List.copyOf(genericMethods);
return this;
}
/**
* Create a new {@link ComputerContext}.
*
@ -237,8 +254,8 @@ public ComputerContext build() {
mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler,
luaFactory == null ? CobaltLuaMachine::new : luaFactory,
apiFactories == null ? List.of() : apiFactories,
LuaMethodSupplier.create(),
PeripheralMethodSupplier.create()
LuaMethodSupplier.create(genericMethods == null ? List.of() : genericMethods),
PeripheralMethodSupplier.create(genericMethods == null ? List.of() : genericMethods)
);
}
}

View File

@ -10,8 +10,6 @@
import com.google.common.primitives.Primitives;
import com.google.common.reflect.TypeToken;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.peripheral.PeripheralType;
import dan200.computercraft.core.methods.NamedMethod;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
@ -21,11 +19,8 @@
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
@ -55,10 +50,6 @@ final class Generator<T> {
private final Function<T, T> wrap;
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder
.newBuilder()
.build(CacheLoader.from(catching(this::build, Collections.emptyList())));
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
.newBuilder()
.build(CacheLoader.from(catching(this::build, Optional.empty())));
@ -75,58 +66,8 @@ final class Generator<T> {
this.methodDesc = methodDesc.toString();
}
public List<NamedMethod<T>> getMethods(Class<?> klass) {
try {
return classCache.get(klass);
} catch (ExecutionException e) {
LOG.error("Error getting methods for {}.", klass.getName(), e.getCause());
return Collections.emptyList();
}
}
private List<NamedMethod<T>> build(Class<?> klass) {
ArrayList<NamedMethod<T>> methods = null;
for (var method : klass.getMethods()) {
var annotation = method.getAnnotation(LuaFunction.class);
if (annotation == null) continue;
if (Modifier.isStatic(method.getModifiers())) {
LOG.warn("LuaFunction method {}.{} should be an instance method.", method.getDeclaringClass(), method.getName());
continue;
}
var instance = methodCache.getUnchecked(method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();
addMethod(methods, method, annotation, null, instance);
}
for (var method : GenericMethod.all()) {
if (!method.target.isAssignableFrom(klass)) continue;
var instance = methodCache.getUnchecked(method.method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();
addMethod(methods, method.method, method.annotation, method.peripheralType, instance);
}
if (methods == null) return Collections.emptyList();
methods.trimToSize();
return Collections.unmodifiableList(methods);
}
private void addMethod(List<NamedMethod<T>> methods, Method method, LuaFunction annotation, @Nullable PeripheralType genericType, T instance) {
var names = annotation.value();
var isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread();
if (names.length == 0) {
methods.add(new NamedMethod<>(method.getName(), instance, isSimple, genericType));
} else {
for (var name : names) {
methods.add(new NamedMethod<>(name, instance, isSimple, genericType));
}
}
Optional<T> getMethod(Method method) {
return methodCache.getUnchecked(method);
}
private Optional<T> build(Method method) {
@ -337,7 +278,7 @@ private Boolean loadArg(MethodVisitor mw, Class<?> target, Method method, boolea
}
@SuppressWarnings("Guava")
private static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
return x -> {
try {
return function.apply(x);

View File

@ -14,16 +14,14 @@
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
/**
* A generic method is a method belonging to a {@link GenericSource} with a known target.
*/
public class GenericMethod {
public final class GenericMethod {
private static final Logger LOG = LoggerFactory.getLogger(GenericMethod.class);
final Method method;
@ -31,37 +29,24 @@ public class GenericMethod {
final Class<?> target;
final @Nullable PeripheralType peripheralType;
private static final List<GenericSource> sources = new ArrayList<>();
private static @Nullable List<GenericMethod> cache;
GenericMethod(Method method, LuaFunction annotation, Class<?> target, @Nullable PeripheralType peripheralType) {
private GenericMethod(Method method, LuaFunction annotation, Class<?> target, @Nullable PeripheralType peripheralType) {
this.method = method;
this.annotation = annotation;
this.target = target;
this.peripheralType = peripheralType;
}
public String name() {
return method.getName();
}
/**
* Find all public static methods annotated with {@link LuaFunction} which belong to a {@link GenericSource}.
*
* @param source The given generic source.
* @return All available generic methods.
*/
static List<GenericMethod> all() {
if (cache != null) return cache;
return cache = sources.stream().flatMap(GenericMethod::getMethods).toList();
}
public static synchronized void register(GenericSource source) {
Objects.requireNonNull(source, "Source cannot be null");
if (cache != null) {
LOG.warn("Registering a generic source {} after cache has been built. This source will be ignored.", cache);
}
sources.add(source);
}
private static Stream<GenericMethod> getMethods(GenericSource source) {
public static Stream<GenericMethod> getMethods(GenericSource source) {
Class<?> klass = source.getClass();
var type = source instanceof GenericPeripheral generic ? generic.getType() : null;

View File

@ -6,16 +6,21 @@
import dan200.computercraft.api.lua.IDynamicLuaObject;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.List;
import java.util.Objects;
/**
* Provides a {@link MethodSupplier} for {@link LuaMethod}s.
* <p>
* This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide
* method supplier}. It should not be used directly.
*/
public final class LuaMethodSupplier {
@VisibleForTesting
static final Generator<LuaMethod> GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class),
private static final Generator<LuaMethod> GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class),
m -> (target, context, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, args.escapes())))
);
private static final IntCache<LuaMethod> DYNAMIC = new IntCache<>(
@ -25,8 +30,8 @@ public final class LuaMethodSupplier {
private LuaMethodSupplier() {
}
public static MethodSupplier<LuaMethod> create() {
return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic
public static MethodSupplier<LuaMethod> create(List<GenericMethod> genericMethods) {
return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic
? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null")
: null
);

View File

@ -4,17 +4,49 @@
package dan200.computercraft.core.asm;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.api.peripheral.PeripheralType;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.NamedMethod;
import dan200.computercraft.core.methods.ObjectSource;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import static dan200.computercraft.core.asm.Generator.catching;
final class MethodSupplierImpl<T> implements MethodSupplier<T> {
private static final Logger LOG = LoggerFactory.getLogger(MethodSupplierImpl.class);
private final List<GenericMethod> genericMethods;
private final Generator<T> generator;
private final IntCache<T> dynamic;
private final Function<Object, String[]> dynamicMethods;
MethodSupplierImpl(Generator<T> generator, IntCache<T> dynamic, Function<Object, String[]> dynamicMethods) {
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder
.newBuilder()
.build(CacheLoader.from(catching(this::getMethodsImpl, List.of())));
MethodSupplierImpl(
List<GenericMethod> genericMethods,
Generator<T> generator,
IntCache<T> dynamic,
Function<Object, String[]> dynamicMethods
) {
this.genericMethods = genericMethods;
this.generator = generator;
this.dynamic = dynamic;
this.dynamicMethods = dynamicMethods;
@ -22,7 +54,7 @@ final class MethodSupplierImpl<T> implements MethodSupplier<T> {
@Override
public boolean forEachSelfMethod(Object object, UntargetedConsumer<T> consumer) {
var methods = generator.getMethods(object.getClass());
var methods = getMethods(object.getClass());
for (var method : methods) consumer.accept(method.name(), method.method(), method);
var dynamicMethods = this.dynamicMethods.apply(object);
@ -35,14 +67,14 @@ public boolean forEachSelfMethod(Object object, UntargetedConsumer<T> consumer)
@Override
public boolean forEachMethod(Object object, TargetedConsumer<T> consumer) {
var methods = generator.getMethods(object.getClass());
var methods = getMethods(object.getClass());
for (var method : methods) consumer.accept(object, method.name(), method.method(), method);
var hasMethods = !methods.isEmpty();
if (object instanceof ObjectSource source) {
for (var extra : source.getExtra()) {
var extraMethods = generator.getMethods(extra.getClass());
var extraMethods = getMethods(extra.getClass());
if (!extraMethods.isEmpty()) hasMethods = true;
for (var method : extraMethods) consumer.accept(object, method.name(), method.method(), method);
}
@ -58,4 +90,63 @@ public boolean forEachMethod(Object object, TargetedConsumer<T> consumer) {
return hasMethods;
}
@VisibleForTesting
List<NamedMethod<T>> getMethods(Class<?> klass) {
try {
return classCache.get(klass);
} catch (ExecutionException e) {
LOG.error("Error getting methods for {}.", klass.getName(), e.getCause());
return List.of();
}
}
private List<NamedMethod<T>> getMethodsImpl(Class<?> klass) {
ArrayList<NamedMethod<T>> methods = null;
// Find all methods on the current class
for (var method : klass.getMethods()) {
var annotation = method.getAnnotation(LuaFunction.class);
if (annotation == null) continue;
if (Modifier.isStatic(method.getModifiers())) {
LOG.warn("LuaFunction method {}.{} should be an instance method.", method.getDeclaringClass(), method.getName());
continue;
}
var instance = generator.getMethod(method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();
addMethod(methods, method, annotation, null, instance);
}
// Inject generic methods
for (var method : genericMethods) {
if (!method.target.isAssignableFrom(klass)) continue;
var instance = generator.getMethod(method.method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();
addMethod(methods, method.method, method.annotation, method.peripheralType, instance);
}
if (methods == null) return List.of();
methods.trimToSize();
return Collections.unmodifiableList(methods);
}
private void addMethod(List<NamedMethod<T>> methods, Method method, LuaFunction annotation, @Nullable PeripheralType genericType, T instance) {
var names = annotation.value();
var isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread();
if (names.length == 0) {
methods.add(new NamedMethod<>(method.getName(), instance, isSimple, genericType));
} else {
for (var name : names) {
methods.add(new NamedMethod<>(name, instance, isSimple, genericType));
}
}
}
}

View File

@ -7,12 +7,19 @@
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import java.util.List;
import java.util.Objects;
/**
* Provides a {@link MethodSupplier} for {@link PeripheralMethod}s.
* <p>
* This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide
* method supplier}. It should not be used directly.
*/
public class PeripheralMethodSupplier {
private static final Generator<PeripheralMethod> GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class),
m -> (target, context, computer, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, args.escapes())))
@ -21,8 +28,8 @@ public class PeripheralMethodSupplier {
method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args)
);
public static MethodSupplier<PeripheralMethod> create() {
return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic
public static MethodSupplier<PeripheralMethod> create(List<GenericMethod> genericMethods) {
return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic
? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null")
: null
);

View File

@ -12,10 +12,11 @@
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import java.util.List;
import java.util.Map;
public class ObjectWrapper implements ILuaContext {
private static final MethodSupplier<LuaMethod> LUA_METHODS = LuaMethodSupplier.create();
private static final MethodSupplier<LuaMethod> LUA_METHODS = LuaMethodSupplier.create(List.of());
private final Object object;
private final Map<String, LuaMethod> methodMap;

View File

@ -14,6 +14,7 @@
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -23,7 +24,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
public class GeneratorTest {
private static final Generator<LuaMethod> GENERATOR = LuaMethodSupplier.GENERATOR;
private static final MethodSupplierImpl<LuaMethod> GENERATOR = (MethodSupplierImpl<LuaMethod>) LuaMethodSupplier.create(List.of());
@Test
public void testBasic() {