Add logic to allow synchronizing methods to main thread.

This commit is contained in:
Florian Nücke
2020-11-29 17:37:20 +01:00
parent 11e612c789
commit 35f5f2e2b6
6 changed files with 68 additions and 18 deletions

View File

@@ -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;

View File

@@ -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.
*/

View File

@@ -13,12 +13,22 @@ import java.lang.annotation.Target;
* <p>
* Intended to be used in classes instances of which are used in combination with
* {@link ObjectDevice} and subclasses of {@link ObjectDevice}.
* <p>
* For
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Callback {
/**
* Allows automatically moving method invocation into the main thread.
* <p>
* 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).
* <p>
* 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.
*/

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}