diff --git a/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java b/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java index b6ba303f8..dc4ca4f7a 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/AbstractComputerCraftAPI.java @@ -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 diff --git a/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java b/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java index ef96738ef..388854fce 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/ApiFactories.java @@ -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 factories = new LinkedHashSet<>(); - private static final Collection 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 getAll() { - return factoriesView; + return Collections.unmodifiableCollection(factories); } } diff --git a/projects/common/src/main/java/dan200/computercraft/impl/GenericSources.java b/projects/common/src/main/java/dan200/computercraft/impl/GenericSources.java new file mode 100644 index 000000000..0ba250c9c --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/impl/GenericSources.java @@ -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 sources = new LinkedHashSet<>(); + + static synchronized void register(GenericSource source) { + Objects.requireNonNull(source, "provider cannot be null"); + sources.add(source); + } + + public static Collection getAllMethods() { + return sources.stream().flatMap(GenericMethod::getMethods).toList(); + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java index 108c95483..64480353b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java @@ -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")); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java index 75a6cac6d..b7fefae86 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java +++ b/projects/core/src/main/java/dan200/computercraft/core/ComputerContext.java @@ -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 apiFactories; + private @Nullable List genericMethods; Builder(GlobalEnvironment environment) { this.environment = environment; @@ -225,6 +227,21 @@ public Builder apiFactories(Collection 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 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) ); } } 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 4b79e0060..7a973f5b3 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 @@ -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 { private final Function wrap; - private final LoadingCache, List>> classCache = CacheBuilder - .newBuilder() - .build(CacheLoader.from(catching(this::build, Collections.emptyList()))); - private final LoadingCache> methodCache = CacheBuilder .newBuilder() .build(CacheLoader.from(catching(this::build, Optional.empty()))); @@ -75,58 +66,8 @@ final class Generator { this.methodDesc = methodDesc.toString(); } - public List> 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> build(Class klass) { - ArrayList> 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> 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 getMethod(Method method) { + return methodCache.getUnchecked(method); } private Optional build(Method method) { @@ -337,7 +278,7 @@ private Boolean loadArg(MethodVisitor mw, Class target, Method method, boolea } @SuppressWarnings("Guava") - private static com.google.common.base.Function catching(Function function, U def) { + static com.google.common.base.Function catching(Function function, U def) { return x -> { try { return function.apply(x); diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java b/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java index d7a8468bf..1e18b0a0b 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/GenericMethod.java @@ -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 sources = new ArrayList<>(); - private static @Nullable List 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 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 getMethods(GenericSource source) { + public static Stream getMethods(GenericSource source) { Class klass = source.getClass(); var type = source instanceof GenericPeripheral generic ? generic.getType() : 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 626e9a329..11f8e4310 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,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. + *

+ * 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 GENERATOR = new Generator<>(LuaMethod.class, List.of(ILuaContext.class), + private static final Generator 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 DYNAMIC = new IntCache<>( @@ -25,8 +30,8 @@ public final class LuaMethodSupplier { private LuaMethodSupplier() { } - public static MethodSupplier create() { - return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic + public static MethodSupplier create(List genericMethods) { + return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic ? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null") : null ); diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java index 1e532d713..44e5775e4 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java @@ -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 implements MethodSupplier { + private static final Logger LOG = LoggerFactory.getLogger(MethodSupplierImpl.class); + + private final List genericMethods; private final Generator generator; private final IntCache dynamic; private final Function dynamicMethods; - MethodSupplierImpl(Generator generator, IntCache dynamic, Function dynamicMethods) { + private final LoadingCache, List>> classCache = CacheBuilder + .newBuilder() + .build(CacheLoader.from(catching(this::getMethodsImpl, List.of()))); + + MethodSupplierImpl( + List genericMethods, + Generator generator, + IntCache dynamic, + Function dynamicMethods + ) { + this.genericMethods = genericMethods; this.generator = generator; this.dynamic = dynamic; this.dynamicMethods = dynamicMethods; @@ -22,7 +54,7 @@ final class MethodSupplierImpl implements MethodSupplier { @Override public boolean forEachSelfMethod(Object object, UntargetedConsumer 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 consumer) @Override public boolean forEachMethod(Object object, TargetedConsumer 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 consumer) { return hasMethods; } + + @VisibleForTesting + List> 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> getMethodsImpl(Class klass) { + ArrayList> 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> 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)); + } + } + } + } 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 5010449c9..174b4ef19 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 @@ -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. + *

+ * 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 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 create() { - return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic + public static MethodSupplier create(List genericMethods) { + return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic ? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null") : null ); diff --git a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java index 6b389a1e8..7c8ebedde 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java +++ b/projects/core/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java @@ -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 LUA_METHODS = LuaMethodSupplier.create(); + private static final MethodSupplier LUA_METHODS = LuaMethodSupplier.create(List.of()); private final Object object; private final Map methodMap; 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 cc504eb27..9fc6620e5 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 @@ -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 GENERATOR = LuaMethodSupplier.GENERATOR; + private static final MethodSupplierImpl GENERATOR = (MethodSupplierImpl) LuaMethodSupplier.create(List.of()); @Test public void testBasic() {