diff --git a/src/main/java/li/cil/oc2/api/device/AbstractDeviceMethod.java b/src/main/java/li/cil/oc2/api/device/AbstractDeviceMethod.java index 4e42b79c..d759cd2d 100644 --- a/src/main/java/li/cil/oc2/api/device/AbstractDeviceMethod.java +++ b/src/main/java/li/cil/oc2/api/device/AbstractDeviceMethod.java @@ -5,17 +5,27 @@ package li.cil.oc2.api.device; */ public abstract class AbstractDeviceMethod implements DeviceMethod { protected final String name; + protected final boolean synchronize; protected final Class returnType; protected final DeviceMethodParameter[] parameters; - protected AbstractDeviceMethod(final String name, final Class returnType, final DeviceMethodParameter... parameters) { + protected AbstractDeviceMethod(final String name, final boolean synchronize, final Class returnType, final DeviceMethodParameter... parameters) { this.name = name; + this.synchronize = synchronize; this.returnType = returnType; this.parameters = parameters; } + protected AbstractDeviceMethod(final String name, final Class returnType, final DeviceMethodParameter... parameters) { + this(name, false, returnType, parameters); + } + + protected AbstractDeviceMethod(final String name, final boolean synchronize, final DeviceMethodParameter... parameters) { + this(name, synchronize, void.class, parameters); + } + protected AbstractDeviceMethod(final String name, final DeviceMethodParameter... parameters) { - this(name, void.class, parameters); + this(name, false, void.class, parameters); } @Override @@ -23,6 +33,11 @@ public abstract class AbstractDeviceMethod implements DeviceMethod { return name; } + @Override + public boolean isSynchronized() { + return synchronize; + } + @Override public Class getReturnType() { return returnType; diff --git a/src/main/java/li/cil/oc2/api/device/DeviceMethod.java b/src/main/java/li/cil/oc2/api/device/DeviceMethod.java index 31fef05c..d662b50e 100644 --- a/src/main/java/li/cil/oc2/api/device/DeviceMethod.java +++ b/src/main/java/li/cil/oc2/api/device/DeviceMethod.java @@ -24,6 +24,11 @@ public interface DeviceMethod { */ String getName(); + /** + * When {@code true}, invocations of this method will be synchronized to the main thread. + */ + boolean isSynchronized(); + /** * The type of the values returned by this method. */ diff --git a/src/main/java/li/cil/oc2/api/device/object/Callback.java b/src/main/java/li/cil/oc2/api/device/object/Callback.java index bfa0b4bd..bd879006 100644 --- a/src/main/java/li/cil/oc2/api/device/object/Callback.java +++ b/src/main/java/li/cil/oc2/api/device/object/Callback.java @@ -13,12 +13,22 @@ import java.lang.annotation.Target; *

* Intended to be used in classes instances of which are used in combination with * {@link ObjectDevice} and subclasses of {@link ObjectDevice}. - *

- * For */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Callback { + /** + * Allows automatically moving method invocation into the main thread. + *

+ * Note that this will lead to dramatically slower method calls as viewed from + * the caller as each call will take at least one tick (50ms). + *

+ * Use this when the targeted method interacts with data that is not thread + * safe, for example the world or any objects inside the world, such as + * tile entities and entities. + */ + boolean synchronize() default false; + /** * Option VM visible documentation of this method. */ diff --git a/src/main/java/li/cil/oc2/api/device/object/ObjectDeviceMethod.java b/src/main/java/li/cil/oc2/api/device/object/ObjectDeviceMethod.java index 2e51d89f..1a38d1ee 100644 --- a/src/main/java/li/cil/oc2/api/device/object/ObjectDeviceMethod.java +++ b/src/main/java/li/cil/oc2/api/device/object/ObjectDeviceMethod.java @@ -23,13 +23,12 @@ public final class ObjectDeviceMethod extends AbstractDeviceMethod { private final String returnValueDescription; public ObjectDeviceMethod(final Object target, final Method method) throws IllegalAccessException { - super(method.getName(), method.getReturnType(), getParameters(method)); + super(method.getName(), + Objects.requireNonNull(method.getAnnotation(Callback.class), "Method without Callback annotation.").synchronize(), + method.getReturnType(), + getParameters(method)); final Callback annotation = method.getAnnotation(Callback.class); - if (annotation == null) { - throw new IllegalArgumentException("Method without Callback annotation."); - } - this.handle = MethodHandles.lookup().unreflect(method).bindTo(target); this.description = Strings.isNotBlank(annotation.description()) ? annotation.description() : null; this.returnValueDescription = Strings.isNotBlank(annotation.returnValueDescription()) ? annotation.returnValueDescription() : null; diff --git a/src/main/java/li/cil/oc2/common/bus/DeviceBusControllerImpl.java b/src/main/java/li/cil/oc2/common/bus/DeviceBusControllerImpl.java index f87ed478..cb1ef0b4 100644 --- a/src/main/java/li/cil/oc2/common/bus/DeviceBusControllerImpl.java +++ b/src/main/java/li/cil/oc2/common/bus/DeviceBusControllerImpl.java @@ -59,6 +59,7 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable { @Serialized private final ByteBuffer transmitBuffer; // for data written to device by VM @Serialized private ByteBuffer receiveBuffer; // for data written by device to VM + @Serialized private MethodInvocation synchronizedInvocation; // pending main thread invocation public DeviceBusControllerImpl(final SerialDevice serialDevice) { this(serialDevice, DEFAULT_MAX_MESSAGE_SIZE); @@ -189,6 +190,14 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable { return State.READY; } + public void tick() { + if (synchronizedInvocation != null) { + final MethodInvocation methodInvocation = synchronizedInvocation; + synchronizedInvocation = null; + processMethodInvocation(methodInvocation, true); + } + } + public void step(final int cycles) { readFromDevice(); writeToDevice(); @@ -200,7 +209,7 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable { // method of limiting the write queue size would work, but this is // the most simple and easy to maintain one I could think of. int value; - while (receiveBuffer == null && (value = serialDevice.read()) >= 0) { + while (receiveBuffer == null && synchronizedInvocation == null && (value = serialDevice.read()) >= 0) { if (value == 0) { if (transmitBuffer.limit() > 0) { transmitBuffer.flip(); @@ -253,7 +262,7 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable { } case MESSAGE_TYPE_INVOKE_METHOD: { assert message.data != null : "MethodInvocation deserializer produced null data."; - processMethodInvocation((MethodInvocation) message.data); + processMethodInvocation((MethodInvocation) message.data, false); break; } default: { @@ -266,7 +275,7 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable { } } - private void processMethodInvocation(final MethodInvocation methodInvocation) { + private void processMethodInvocation(final MethodInvocation methodInvocation, final boolean isMainThread) { final Device device = devices.get(methodInvocation.deviceId); if (device == null) { writeError(ERROR_UNKNOWN_DEVICE); @@ -301,6 +310,11 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable { } } + if (method.isSynchronized() && !isMainThread) { + synchronizedInvocation = methodInvocation; + return; + } + try { final Object result = method.invoke(parameters); writeMessage(MESSAGE_TYPE_RESULT, result); @@ -377,10 +391,11 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable { } } - private static final class MethodInvocation { - public final UUID deviceId; - public final String methodName; - public final JsonArray parameters; + @Serialized + public static final class MethodInvocation { + public UUID deviceId; + public String methodName; + public JsonArray parameters; public MethodInvocation(final UUID deviceId, final String methodName, final JsonArray parameters) { this.deviceId = deviceId; diff --git a/src/test/java/li/cil/oc2/vm/AbstractTestMethod.java b/src/test/java/li/cil/oc2/vm/AbstractTestMethod.java index df13b7bf..362a33b7 100644 --- a/src/test/java/li/cil/oc2/vm/AbstractTestMethod.java +++ b/src/test/java/li/cil/oc2/vm/AbstractTestMethod.java @@ -3,6 +3,7 @@ package li.cil.oc2.vm; import li.cil.oc2.api.device.DeviceMethod; import li.cil.oc2.api.device.DeviceMethodParameter; +import javax.annotation.Nullable; import java.util.Optional; abstract class AbstractTestMethod implements DeviceMethod { @@ -22,6 +23,11 @@ abstract class AbstractTestMethod implements DeviceMethod { return getClass().getSimpleName(); } + @Override + public boolean isSynchronized() { + return false; + } + @Override public Class getReturnType() { return returnType; @@ -33,10 +39,10 @@ abstract class AbstractTestMethod implements DeviceMethod { } private static final class TestParameter implements DeviceMethodParameter { - private final String name; + @Nullable private final String name; private final Class type; - public TestParameter(final String name, final Class type) { + public TestParameter(@Nullable final String name, final Class type) { this.name = name; this.type = type; }