Clean up how we enumerate Lua/peripheral methods

- Move several interfaces out of `d00.computercraft.core.asm` into a
   new `aethods` package. It may make sense to expose this to the
   public API in a future commit (possibly part of #1462).

 - Add a new MethodSupplier<T> interface, which provides methods to
   iterate over all methods exported by an object (either directly, or
   including those from ObjectSources).

   This interface's concrete implementation (asm.MethodSupplierImpl),
   uses Generators and IntCaches as before - we can now make that all
   package-private though, which is nice!

 - Make the LuaMethod and PeripheralMethod MethodSupplier local to the
   ComputerContext. This currently has no effect (the underlying
   Generator is still global), but eventually we'll make GenericMethods
   non-global, which unlocks the door for #1382.

 - Update everything to use this new interface. This is mostly pretty
   sensible, but is a little uglier on the MC side (especially in
   generic peripherals), as we need to access the global ServerContext.
This commit is contained in:
Jonathan Coates 2023-06-26 19:32:09 +01:00
parent a29a516a3f
commit 591a7eca23
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
33 changed files with 449 additions and 188 deletions

View File

@ -14,6 +14,8 @@
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
import dan200.computercraft.core.lua.CobaltLuaMachine;
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.impl.AbstractComputerCraftAPI;
import dan200.computercraft.impl.ApiFactories;
import dan200.computercraft.shared.CommonHooks;
@ -134,6 +136,16 @@ ComputerContext computerContext() {
return context;
}
/**
* Get the {@link MethodSupplier} used to find methods on peripherals.
*
* @return The {@link PeripheralMethod} method supplier.
* @see ComputerContext#peripheralMethods()
*/
public MethodSupplier<PeripheralMethod> peripheralMethods() {
return context.peripheralMethods();
}
/**
* Tick all components of this server context. This should <em>NOT</em> be called outside of {@link CommonHooks}.
*/

View File

@ -7,7 +7,7 @@
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.methods.ObjectSource;
import java.nio.ByteBuffer;
import java.util.Collections;

View File

@ -6,9 +6,12 @@
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.PeripheralType;
import dan200.computercraft.core.asm.NamedMethod;
import dan200.computercraft.core.asm.PeripheralMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.NamedMethod;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.shared.computer.core.ServerContext;
import net.minecraft.core.Direction;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.block.entity.BlockEntity;
import javax.annotation.Nullable;
@ -25,37 +28,36 @@
* See the platform-specific peripheral providers for the usage of this.
*/
final class GenericPeripheralBuilder {
private final MethodSupplier<PeripheralMethod> peripheralMethods;
private @Nullable String name;
private final Set<String> additionalTypes = new HashSet<>(0);
private final ArrayList<SaturatedMethod> methods = new ArrayList<>(0);
private final ArrayList<SaturatedMethod> methods = new ArrayList<>();
GenericPeripheralBuilder(MinecraftServer server) {
peripheralMethods = ServerContext.get(server).peripheralMethods();
}
@Nullable
IPeripheral toPeripheral(BlockEntity tile, Direction side) {
IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) {
if (methods.isEmpty()) return null;
methods.trimToSize();
return new GenericPeripheral(tile, side, name, additionalTypes, methods);
return new GenericPeripheral(blockEntity, side, name, additionalTypes, methods);
}
boolean addMethods(Object target) {
var methods = PeripheralMethod.GENERATOR.getMethods(target.getClass());
if (methods.isEmpty()) return false;
var saturatedMethods = this.methods;
saturatedMethods.ensureCapacity(saturatedMethods.size() + methods.size());
for (var method : methods) {
saturatedMethods.add(new SaturatedMethod(target, method.name(), method.method()));
return peripheralMethods.forEachSelfMethod(target, (name, method, info) -> {
methods.add(new SaturatedMethod(target, name, method));
// If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods
// don't change).
var type = method.genericType();
var type = info == null ? null : info.genericType();
if (type != null && type.getPrimaryType() != null) {
var name = type.getPrimaryType();
if (this.name == null || this.name.compareTo(name) > 0) this.name = name;
var primaryType = type.getPrimaryType();
if (this.name == null || this.name.compareTo(primaryType) > 0) this.name = primaryType;
}
if (type != null) additionalTypes.addAll(type.getAdditionalTypes());
}
return true;
});
}
}

View File

@ -9,7 +9,7 @@
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.core.asm.PeripheralMethod;
import dan200.computercraft.core.methods.PeripheralMethod;
/**
* A {@link PeripheralMethod} along with the method's target.

View File

@ -16,10 +16,12 @@
import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.apis.PeripheralAPI;
import dan200.computercraft.core.asm.PeripheralMethod;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.util.LuaUtil;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.peripheral.modem.ModemPeripheral;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -284,7 +286,8 @@ public void detachPeripheral(String name) {
private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String, RemotePeripheralWrapper> peripherals, String periphName, IPeripheral peripheral) {
if (!peripherals.containsKey(periphName) && !periphName.equals(getLocalPeripheral().getConnectedName())) {
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName);
var methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral);
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods);
peripherals.put(periphName, wrapper);
wrapper.attach();
}
@ -314,7 +317,7 @@ private static class RemotePeripheralWrapper implements IComputerAccess {
private volatile boolean attached;
private final Set<String> mounts = new HashSet<>();
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name) {
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map<String, PeripheralMethod> methods) {
this.element = element;
this.peripheral = peripheral;
this.computer = computer;
@ -322,7 +325,7 @@ private static class RemotePeripheralWrapper implements IComputerAccess {
type = Objects.requireNonNull(peripheral.getType(), "Peripheral type cannot be null");
additionalTypes = peripheral.getAdditionalTypes();
methodMap = PeripheralAPI.getMethods(peripheral);
methodMap = methods;
}
public void attach() {

View File

@ -5,12 +5,18 @@
package dan200.computercraft.core;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.core.asm.LuaMethodSupplier;
import dan200.computercraft.core.asm.PeripheralMethodSupplier;
import dan200.computercraft.core.computer.ComputerThread;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
import dan200.computercraft.core.lua.CobaltLuaMachine;
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.lua.MachineEnvironment;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
@ -28,17 +34,22 @@ public final class ComputerContext {
private final MainThreadScheduler mainThreadScheduler;
private final ILuaMachine.Factory luaFactory;
private final List<ILuaAPIFactory> apiFactories;
private final MethodSupplier<LuaMethod> luaMethods;
private final MethodSupplier<PeripheralMethod> peripheralMethods;
ComputerContext(
GlobalEnvironment globalEnvironment, ComputerThread computerScheduler,
MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory,
List<ILuaAPIFactory> apiFactories
List<ILuaAPIFactory> apiFactories, MethodSupplier<LuaMethod> luaMethods,
MethodSupplier<PeripheralMethod> peripheralMethods
) {
this.globalEnvironment = globalEnvironment;
this.computerScheduler = computerScheduler;
this.mainThreadScheduler = mainThreadScheduler;
this.luaFactory = luaFactory;
this.apiFactories = apiFactories;
this.luaMethods = luaMethods;
this.peripheralMethods = peripheralMethods;
}
/**
@ -87,6 +98,25 @@ public List<ILuaAPIFactory> apiFactories() {
return apiFactories;
}
/**
* Get the {@link MethodSupplier} used to find methods on Lua values.
*
* @return The {@link LuaMethod} method supplier.
* @see MachineEnvironment#luaMethods()
*/
public MethodSupplier<LuaMethod> luaMethods() {
return luaMethods;
}
/**
* Get the {@link MethodSupplier} used to find methods on peripherals.
*
* @return The {@link PeripheralMethod} method supplier.
*/
public MethodSupplier<PeripheralMethod> peripheralMethods() {
return peripheralMethods;
}
/**
* Close the current {@link ComputerContext}, disposing of any resources inside.
*
@ -206,7 +236,9 @@ public ComputerContext build() {
new ComputerThread(threads),
mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler,
luaFactory == null ? CobaltLuaMachine::new : luaFactory,
apiFactories == null ? List.of() : apiFactories
apiFactories == null ? List.of() : apiFactories,
LuaMethodSupplier.create(),
PeripheralMethodSupplier.create()
);
}
}

View File

@ -10,9 +10,9 @@
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.metrics.OperationTimer;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.metrics.OperationTimer;
import dan200.computercraft.core.terminal.Terminal;
import javax.annotation.Nullable;

View File

@ -7,13 +7,12 @@
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.PeripheralMethod;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.util.LuaUtil;
@ -44,7 +43,7 @@ private class PeripheralWrapper extends ComputerAccess {
type = Objects.requireNonNull(peripheral.getType(), "Peripheral type cannot be null");
additionalTypes = peripheral.getAdditionalTypes();
methodMap = PeripheralAPI.getMethods(peripheral);
methodMap = peripheralMethods.getSelfMethods(peripheral);
}
public IPeripheral getPeripheral() {
@ -172,11 +171,13 @@ public WorkMonitor getMainThreadMonitor() {
}
private final IAPIEnvironment environment;
private final MethodSupplier<PeripheralMethod> peripheralMethods;
private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6];
private boolean running;
public PeripheralAPI(IAPIEnvironment environment) {
public PeripheralAPI(IAPIEnvironment environment, MethodSupplier<PeripheralMethod> peripheralMethods) {
this.environment = environment;
this.peripheralMethods = peripheralMethods;
this.environment.setPeripheralChangeListener(this);
running = false;
}
@ -315,21 +316,4 @@ public final MethodResult call(ILuaContext context, IArguments args) throws LuaE
throw e;
}
}
public static Map<String, PeripheralMethod> getMethods(IPeripheral peripheral) {
var dynamicMethods = peripheral instanceof IDynamicPeripheral
? Objects.requireNonNull(((IDynamicPeripheral) peripheral).getMethodNames(), "Peripheral methods cannot be null")
: LuaMethod.EMPTY_METHODS;
var methods = PeripheralMethod.GENERATOR.getMethods(peripheral.getClass());
Map<String, PeripheralMethod> methodMap = new HashMap<>(methods.size() + dynamicMethods.length);
for (var i = 0; i < dynamicMethods.length; i++) {
methodMap.put(dynamicMethods[i], PeripheralMethod.DYNAMIC.get(i));
}
for (var method : methods) {
methodMap.put(method.name(), method.method());
}
return methodMap;
}
}

View File

@ -103,7 +103,7 @@ public static int optIntField(Map<?, ?> table, String key, int def) throws LuaEx
public static Optional<Double> optRealField(Map<?, ?> table, String key) throws LuaException {
var value = table.get(key);
if(value == null) {
if (value == null) {
return Optional.empty();
} else {
return Optional.of(getRealField(table, key));

View File

@ -10,7 +10,7 @@
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.HandleGeneric;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.methods.ObjectSource;
import java.util.Collections;
import java.util.Map;

View File

@ -11,6 +11,7 @@
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;
@ -30,7 +31,7 @@
import static org.objectweb.asm.Opcodes.*;
public final class Generator<T> {
final class Generator<T> {
private static final Logger LOG = LoggerFactory.getLogger(Generator.class);
private static final AtomicInteger METHOD_ID = new AtomicInteger();

View File

@ -7,7 +7,7 @@
import java.util.Arrays;
import java.util.function.IntFunction;
public final class IntCache<T> {
final class IntCache<T> {
private final IntFunction<T> factory;
private volatile Object[] cache = new Object[16];

View File

@ -1,23 +0,0 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.asm;
import dan200.computercraft.api.lua.*;
import java.util.Collections;
public interface LuaMethod {
Generator<LuaMethod> GENERATOR = new Generator<>(LuaMethod.class, Collections.singletonList(ILuaContext.class),
m -> (target, context, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, args.escapes())))
);
IntCache<LuaMethod> DYNAMIC = new IntCache<>(
method -> (instance, context, args) -> ((IDynamicLuaObject) instance).callMethod(context, method, args)
);
String[] EMPTY_METHODS = new String[0];
MethodResult apply(Object target, ILuaContext context, IArguments args) throws LuaException;
}

View File

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.asm;
import dan200.computercraft.api.lua.IDynamicLuaObject;
import dan200.computercraft.api.lua.ILuaContext;
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;
public final class LuaMethodSupplier {
@VisibleForTesting
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<>(
method -> (instance, context, args) -> ((IDynamicLuaObject) instance).callMethod(context, method, args)
);
private LuaMethodSupplier() {
}
public static MethodSupplier<LuaMethod> create() {
return new MethodSupplierImpl<>(GENERATOR, DYNAMIC, x -> x instanceof IDynamicLuaObject dynamic
? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null")
: null
);
}
}

View File

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.asm;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.ObjectSource;
import java.util.function.Function;
final class MethodSupplierImpl<T> implements MethodSupplier<T> {
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) {
this.generator = generator;
this.dynamic = dynamic;
this.dynamicMethods = dynamicMethods;
}
@Override
public boolean forEachSelfMethod(Object object, UntargetedConsumer<T> consumer) {
var methods = generator.getMethods(object.getClass());
for (var method : methods) consumer.accept(method.name(), method.method(), method);
var dynamicMethods = this.dynamicMethods.apply(object);
if (dynamicMethods != null) {
for (var i = 0; i < dynamicMethods.length; i++) consumer.accept(dynamicMethods[i], dynamic.get(i), null);
}
return !methods.isEmpty() || dynamicMethods != null;
}
@Override
public boolean forEachMethod(Object object, TargetedConsumer<T> consumer) {
var methods = generator.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());
if (!extraMethods.isEmpty()) hasMethods = true;
for (var method : extraMethods) consumer.accept(object, method.name(), method.method(), method);
}
}
var dynamicMethods = this.dynamicMethods.apply(object);
if (dynamicMethods != null) {
hasMethods = true;
for (var i = 0; i < dynamicMethods.length; i++) {
consumer.accept(object, dynamicMethods[i], dynamic.get(i), null);
}
}
return hasMethods;
}
}

View File

@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.asm;
import java.util.function.BiConsumer;
/**
* A Lua object which exposes additional methods.
* <p>
* This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely
* happy with the interface - something I'd like to think about first.
*/
public interface ObjectSource {
Iterable<Object> getExtra();
static <T> void allMethods(Generator<T> generator, Object object, BiConsumer<Object, NamedMethod<T>> accept) {
for (var method : generator.getMethods(object.getClass())) accept.accept(object, method);
if (object instanceof ObjectSource source) {
for (var extra : source.getExtra()) {
for (var method : generator.getMethods(extra.getClass())) accept.accept(extra, method);
}
}
}
}

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.asm;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import java.util.Arrays;
public interface PeripheralMethod {
Generator<PeripheralMethod> GENERATOR = new Generator<>(PeripheralMethod.class, Arrays.asList(ILuaContext.class, IComputerAccess.class),
m -> (target, context, computer, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, args.escapes())))
);
IntCache<PeripheralMethod> DYNAMIC = new IntCache<>(
method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args)
);
MethodResult apply(Object target, ILuaContext context, IComputerAccess computer, IArguments args) throws LuaException;
}

View File

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.asm;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import java.util.List;
import java.util.Objects;
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())))
);
private static final IntCache<PeripheralMethod> DYNAMIC = new IntCache<>(
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
? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null")
: null
);
}
}

View File

@ -15,6 +15,8 @@
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.lua.MachineEnvironment;
import dan200.computercraft.core.lua.MachineException;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.util.Colour;
@ -61,6 +63,7 @@ final class ComputerExecutor {
private final MetricsObserver metrics;
private final List<ILuaAPI> apis = new ArrayList<>();
private final ComputerThread scheduler;
private final MethodSupplier<LuaMethod> luaMethods;
final TimeoutState timeout;
private @Nullable FileSystem fileSystem;
@ -168,6 +171,7 @@ final class ComputerExecutor {
metrics = computerEnvironment.getMetrics();
luaFactory = context.luaFactory();
scheduler = context.computerScheduler();
luaMethods = context.luaMethods();
timeout = new TimeoutState(scheduler);
var environment = computer.getEnvironment();
@ -176,7 +180,7 @@ final class ComputerExecutor {
apis.add(new TermAPI(environment));
apis.add(new RedstoneAPI(environment));
apis.add(new FSAPI(environment));
apis.add(new PeripheralAPI(environment));
apis.add(new PeripheralAPI(environment, context.peripheralMethods()));
apis.add(new OSAPI(environment));
if (CoreConfig.httpEnabled) apis.add(new HTTPAPI(environment));
@ -382,6 +386,7 @@ private ILuaMachine createLuaMachine() {
return luaFactory.create(new MachineEnvironment(
new LuaContext(computer), metrics, timeout,
() -> apis.stream().map(api -> api instanceof ApiWrapper wrapper ? wrapper.getDelegate() : api).iterator(),
luaMethods,
computer.getGlobalEnvironment().getHostString()
), bios);
} catch (IOException e) {

View File

@ -8,7 +8,7 @@
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.methods.LuaMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.LuaError;

View File

@ -10,9 +10,9 @@
import dan200.computercraft.api.lua.ILuaFunction;
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.core.util.SanitisedError;
import org.slf4j.Logger;
@ -39,6 +39,7 @@ public class CobaltLuaMachine implements ILuaMachine {
private final TimeoutState timeout;
private final Runnable timeoutListener = this::updateTimeout;
private final ILuaContext context;
private final MethodSupplier<LuaMethod> luaMethods;
private final LuaState state;
private final LuaThread mainRoutine;
@ -51,6 +52,7 @@ public class CobaltLuaMachine implements ILuaMachine {
public CobaltLuaMachine(MachineEnvironment environment, InputStream bios) throws MachineException, IOException {
timeout = environment.timeout();
context = environment.context();
luaMethods = environment.luaMethods();
// Create an environment to run in
var state = this.state = LuaState.builder()
@ -161,28 +163,13 @@ public void close() {
@Nullable
private LuaTable wrapLuaObject(Object object) {
var dynamicMethods = object instanceof IDynamicLuaObject dynamic
? Objects.requireNonNull(dynamic.getMethodNames(), "Methods cannot be null")
: LuaMethod.EMPTY_METHODS;
var table = new LuaTable();
for (var i = 0; i < dynamicMethods.length; i++) {
var method = dynamicMethods[i];
table.rawset(method, new ResultInterpreterFunction(this, LuaMethod.DYNAMIC.get(i), object, context, method));
}
var found = luaMethods.forEachMethod(object, (target, name, method, info) ->
table.rawset(name, info != null && info.nonYielding()
? new BasicFunction(this, method, target, context, name)
: new ResultInterpreterFunction(this, method, target, context, name)));
ObjectSource.allMethods(LuaMethod.GENERATOR, object, (instance, method) ->
table.rawset(method.name(), method.nonYielding()
? new BasicFunction(this, method.method(), instance, context, method.name())
: new ResultInterpreterFunction(this, method.method(), instance, context, method.name())));
try {
if (table.next(Constants.NIL).first().isNil()) return null;
} catch (LuaError ignored) {
// next should never throw on nil.
}
return table;
return found ? table : null;
}
private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap<Object, LuaValue> values) {

View File

@ -8,6 +8,8 @@
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.metrics.MetricsObserver;
/**
@ -18,6 +20,7 @@
* @param timeout The current timeout state. This should be used by the machine to interrupt its execution.
* @param apis APIs to inject into the global environment. Each API should be converted into a Lua object
* (following the same rules as any other value), and then set to all names in {@link ILuaAPI#getNames()}.
* @param luaMethods A {@link MethodSupplier} to find methods on returned values.
* @param hostString A {@linkplain GlobalEnvironment#getHostString() host string} to identify the current environment.
* @see ILuaMachine.Factory
*/
@ -26,6 +29,7 @@ public record MachineEnvironment(
MetricsObserver metrics,
TimeoutState timeout,
Iterable<ILuaAPI> apis,
MethodSupplier<LuaMethod> luaMethods,
String hostString
) {
}

View File

@ -9,7 +9,7 @@
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.methods.LuaMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.*;

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.methods;
import dan200.computercraft.api.lua.*;
/**
* A basic Lua function (i.e. one not associated with a peripheral) on some object (such as a {@link IDynamicLuaObject}
* or {@link ILuaAPI}.
* <p>
* This interface is not typically implemented yourself, but instead generated from a {@link LuaFunction}-annotated
* method.
*
* @see NamedMethod
*/
@FunctionalInterface
public interface LuaMethod {
/**
* Apply this method.
*
* @param target The object instance that this method targets.
* @param context The Lua context for this function call.
* @param args Arguments to this function.
* @return The return call of this function.
* @throws LuaException Thrown by the underlying method call.
* @see IDynamicLuaObject#callMethod(ILuaContext, int, IArguments)
*/
MethodResult apply(Object target, ILuaContext context, IArguments args) throws LuaException;
}

View File

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.methods;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* Finds methods available on an object and yields them.
*
* @param <T> The type of method, such as {@link LuaMethod} or {@link PeripheralMethod}.
*/
public interface MethodSupplier<T> {
/**
* Iterate over methods available on an object, ignoring {@link ObjectSource}s.
*
* @param object The object to find methods for.
* @param consumer The consumer to call for each method.
* @return Whether any methods were found.
*/
boolean forEachSelfMethod(Object object, UntargetedConsumer<T> consumer);
/**
* Generate a map of all methods targeting the current object, ignoring {@link ObjectSource}s.
*
* @param object The object to find methods for.
* @return A map of all methods on the object.
*/
default Map<String, T> getSelfMethods(Object object) {
var map = new HashMap<String, T>();
forEachSelfMethod(object, (n, m, i) -> map.put(n, m));
return map;
}
/**
* Iterate over all methods on an object, including {@link ObjectSource}s.
*
* @param object The object to find methods for.
* @param consumer The consumer to call for each method.
* @return Whether any methods were found.
*/
boolean forEachMethod(Object object, TargetedConsumer<T> consumer);
/**
* A function which is called for each method on an object.
*
* @param <T> The type of method, such as {@link LuaMethod} or {@link PeripheralMethod}.
* @see #forEachSelfMethod(Object, UntargetedConsumer)
*/
@FunctionalInterface
interface UntargetedConsumer<T> {
/**
* Consume a method on an object.
*
* @param name The name of this method.
* @param method The actual method definition.
* @param info Additional information about the method, such as whether it will yield. May be {@code null}.
*/
void accept(String name, T method, @Nullable NamedMethod<T> info);
}
/**
* A function which is called for each method on an object and possibly nested objects.
*
* @param <T> The type of method, such as {@link LuaMethod} or {@link PeripheralMethod}.
* @see #forEachMethod(Object, TargetedConsumer)
*/
@FunctionalInterface
interface TargetedConsumer<T> {
/**
* Consume a method on an object.
*
* @param object The object this method targets, should be passed to the method's {@code apply(...)} function.
* @param name The name of this method.
* @param method The actual method definition.
* @param info Additional information about the method, such as whether it will yield. May be {@code null}.
*/
void accept(Object object, String name, T method, @Nullable NamedMethod<T> info);
}
}

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.asm;
package dan200.computercraft.core.methods;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.MethodResult;

View File

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.methods;
/**
* A Lua object which exposes additional methods.
* <p>
* This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely
* happy with the interface - something I'd like to think about first.
*/
public interface ObjectSource {
Iterable<Object> getExtra();
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.methods;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.api.peripheral.IPeripheral;
/**
* A Lua function associated with some peripheral.
* <p>
* This interface is not typically implemented yourself, but instead generated from a {@link LuaFunction}-annotated
* method.
*
* @see NamedMethod
* @see IPeripheral
*/
@FunctionalInterface
public interface PeripheralMethod {
/**
* Apply this method.
*
* @param target The object instance that this method targets.
* @param context The Lua context for this function call.
* @param computer The interface to the computer that is making the call.
* @param args Arguments to this function.
* @return The return call of this function.
* @throws LuaException Thrown by the underlying method call.
* @see IDynamicPeripheral#callMethod(IComputerAccess, ILuaContext, int, IArguments)
*/
MethodResult apply(Object target, ILuaContext context, IComputerAccess computer, IArguments args) throws LuaException;
}

View File

@ -4,32 +4,25 @@
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaTask;
import dan200.computercraft.api.lua.ObjectArguments;
import dan200.computercraft.core.asm.LuaMethodSupplier;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ObjectWrapper implements ILuaContext {
private static final MethodSupplier<LuaMethod> LUA_METHODS = LuaMethodSupplier.create();
private final Object object;
private final Map<String, LuaMethod> methodMap;
public ObjectWrapper(Object object) {
this.object = object;
var dynamicMethods = object instanceof IDynamicLuaObject dynamic
? Objects.requireNonNull(dynamic.getMethodNames(), "Methods cannot be null")
: LuaMethod.EMPTY_METHODS;
var methods = LuaMethod.GENERATOR.getMethods(object.getClass());
var methodMap = this.methodMap = new HashMap<>(methods.size() + dynamicMethods.length);
for (var i = 0; i < dynamicMethods.length; i++) {
methodMap.put(dynamicMethods[i], LuaMethod.DYNAMIC.get(i));
}
for (var method : methods) {
methodMap.put(method.name(), method.method());
}
methodMap = LUA_METHODS.getSelfMethods(object);
}
public Object[] call(String name, Object... args) throws LuaException {

View File

@ -7,6 +7,8 @@
import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.NamedMethod;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
@ -21,9 +23,11 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
public class GeneratorTest {
private static final Generator<LuaMethod> GENERATOR = LuaMethodSupplier.GENERATOR;
@Test
public void testBasic() {
var methods = LuaMethod.GENERATOR.getMethods(Basic.class);
var methods = GENERATOR.getMethods(Basic.class);
assertThat(methods, contains(
allOf(
named("go"),
@ -34,48 +38,48 @@ public void testBasic() {
@Test
public void testIdentical() {
var methods = LuaMethod.GENERATOR.getMethods(Basic.class);
var methods2 = LuaMethod.GENERATOR.getMethods(Basic.class);
var methods = GENERATOR.getMethods(Basic.class);
var methods2 = GENERATOR.getMethods(Basic.class);
assertThat(methods, sameInstance(methods2));
}
@Test
public void testIdenticalMethods() {
var methods = LuaMethod.GENERATOR.getMethods(Basic.class);
var methods2 = LuaMethod.GENERATOR.getMethods(Basic2.class);
var methods = GENERATOR.getMethods(Basic.class);
var methods2 = GENERATOR.getMethods(Basic2.class);
assertThat(methods, contains(named("go")));
assertThat(methods.get(0).method(), sameInstance(methods2.get(0).method()));
}
@Test
public void testEmptyClass() {
assertThat(LuaMethod.GENERATOR.getMethods(Empty.class), is(empty()));
assertThat(GENERATOR.getMethods(Empty.class), is(empty()));
}
@Test
public void testNonPublicClass() {
assertThat(LuaMethod.GENERATOR.getMethods(NonPublic.class), is(empty()));
assertThat(GENERATOR.getMethods(NonPublic.class), is(empty()));
}
@Test
public void testNonInstance() {
assertThat(LuaMethod.GENERATOR.getMethods(NonInstance.class), is(empty()));
assertThat(GENERATOR.getMethods(NonInstance.class), is(empty()));
}
@Test
public void testIllegalThrows() {
assertThat(LuaMethod.GENERATOR.getMethods(IllegalThrows.class), is(empty()));
assertThat(GENERATOR.getMethods(IllegalThrows.class), is(empty()));
}
@Test
public void testCustomNames() {
var methods = LuaMethod.GENERATOR.getMethods(CustomNames.class);
var methods = GENERATOR.getMethods(CustomNames.class);
assertThat(methods, contains(named("go1"), named("go2")));
}
@Test
public void testArgKinds() {
var methods = LuaMethod.GENERATOR.getMethods(ArgKinds.class);
var methods = GENERATOR.getMethods(ArgKinds.class);
assertThat(methods, containsInAnyOrder(
named("objectArg"), named("intArg"), named("optIntArg"),
named("context"), named("arguments")
@ -84,7 +88,7 @@ public void testArgKinds() {
@Test
public void testEnum() throws LuaException {
var methods = LuaMethod.GENERATOR.getMethods(EnumMethods.class);
var methods = GENERATOR.getMethods(EnumMethods.class);
assertThat(methods, containsInAnyOrder(named("getEnum"), named("optEnum")));
assertThat(apply(methods, new EnumMethods(), "getEnum", "front"), one(is("FRONT")));
@ -95,7 +99,7 @@ public void testEnum() throws LuaException {
@Test
public void testMainThread() throws LuaException {
var methods = LuaMethod.GENERATOR.getMethods(MainThread.class);
var methods = GENERATOR.getMethods(MainThread.class);
assertThat(methods, contains(allOf(
named("go"),
contramap(is(false), "non-yielding", NamedMethod::nonYielding)
@ -107,7 +111,7 @@ public void testMainThread() throws LuaException {
@Test
public void testUnsafe() {
var methods = LuaMethod.GENERATOR.getMethods(Unsafe.class);
var methods = GENERATOR.getMethods(Unsafe.class);
assertThat(methods, contains(named("withUnsafe")));
}

View File

@ -10,6 +10,7 @@
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.computer.ComputerBootstrap;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.methods.ObjectSource;
import org.junit.jupiter.api.Test;
import javax.annotation.Nullable;

View File

@ -11,11 +11,15 @@
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.List;
public class GenericPeripheralProvider {
private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralProvider.class);
interface Lookup<T> {
@Nullable
T find(Level world, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, Direction context);
@ -29,7 +33,13 @@ interface Lookup<T> {
public static IPeripheral getPeripheral(Level level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity) {
if (blockEntity == null) return null;
var builder = new GenericPeripheralBuilder();
var server = level.getServer();
if (server == null) {
LOG.warn("Fetching peripherals on a non-server level {}.", level, new IllegalStateException("Fetching peripherals on a non-server level."));
return null;
}
var builder = new GenericPeripheralBuilder(server);
builder.addMethods(blockEntity);
for (var lookup : lookups) {

View File

@ -11,12 +11,16 @@
import net.minecraft.world.level.Level;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.util.NonNullConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Objects;
public class GenericPeripheralProvider {
private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralProvider.class);
private static final ArrayList<Capability<?>> capabilities = new ArrayList<>();
public static synchronized void addCapability(Capability<?> capability) {
@ -29,7 +33,13 @@ public static IPeripheral getPeripheral(Level level, BlockPos pos, Direction sid
var blockEntity = level.getBlockEntity(pos);
if (blockEntity == null) return null;
var builder = new GenericPeripheralBuilder();
var server = level.getServer();
if (server == null) {
LOG.warn("Fetching peripherals on a non-server level {}.", level, new IllegalStateException("Fetching peripherals on a non-server level."));
return null;
}
var builder = new GenericPeripheralBuilder(server);
builder.addMethods(blockEntity);
for (var capability : capabilities) {