diff --git a/src/main/java/com/llamalad7/mixinextras/sugar/Cancellable.java b/src/main/java/com/llamalad7/mixinextras/sugar/Cancellable.java
new file mode 100644
index 0000000..9fcc682
--- /dev/null
+++ b/src/main/java/com/llamalad7/mixinextras/sugar/Cancellable.java
@@ -0,0 +1,31 @@
+package com.llamalad7.mixinextras.sugar;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyConstant;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Allows you to receive a cancellable {@link CallbackInfo} or {@link CallbackInfoReturnable} as appropriate
+ * from any kind of injector. This allows you to optionally cancel the target method without being forced to use
+ * {@link Inject @Inject}.
+ *
+ * The same {@link CallbackInfo}s will be passed to every handler method in a chain of
+ * {@link WrapOperation @WrapOperation}s (i.e. any number of {@link WrapOperation @WrapOperation}s and at most one inner
+ * {@link Redirect @Redirect} / {@link ModifyConstant @ModifyConstant}). This means you can choose to use the
+ * {@link CallbackInfo#isCancelled()} and {@link CallbackInfoReturnable#getReturnValue()} methods to see if the wrapped
+ * handler cancelled, so you can respond accordingly.
+ *
+ * See the wiki article for more info.
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.CLASS)
+public @interface Cancellable {
+}
diff --git a/src/main/java/com/llamalad7/mixinextras/sugar/impl/CancellableSugarApplicator.java b/src/main/java/com/llamalad7/mixinextras/sugar/impl/CancellableSugarApplicator.java
new file mode 100644
index 0000000..dd926c0
--- /dev/null
+++ b/src/main/java/com/llamalad7/mixinextras/sugar/impl/CancellableSugarApplicator.java
@@ -0,0 +1,115 @@
+package com.llamalad7.mixinextras.sugar.impl;
+
+import com.llamalad7.mixinextras.injector.StackExtension;
+import com.llamalad7.mixinextras.utils.ASMUtils;
+import com.llamalad7.mixinextras.utils.Decorations;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.*;
+import org.spongepowered.asm.mixin.injection.struct.InjectionInfo;
+import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode;
+import org.spongepowered.asm.mixin.injection.struct.Target;
+
+class CancellableSugarApplicator extends SugarApplicator {
+ CancellableSugarApplicator(InjectionInfo info, SugarParameter parameter) {
+ super(info, parameter);
+ }
+
+ @Override
+ void validate(Target target, InjectionNode node) {
+ }
+
+ @Override
+ void prepare(Target target, InjectionNode node) {
+ }
+
+ @Override
+ void inject(Target target, InjectionNode node, StackExtension stack) {
+ Type ciType = Type.getObjectType(target.getCallbackInfoClass());
+ if (!ciType.equals(paramType)) {
+ throw new IllegalStateException(
+ String.format(
+ "@Cancellable sugar has wrong type! Expected %s but got %s!",
+ ciType.getClassName(),
+ paramType.getClassName()
+ )
+ );
+ }
+ int ciIndex = getOrCreateCi(target, node, stack, ciType);
+ stack.extra(1);
+ target.insns.insertBefore(node.getCurrentTarget(), new VarInsnNode(Opcodes.ALOAD, ciIndex));
+ }
+
+ @Override
+ int postProcessingPriority() {
+ // Early, we don't care about being particularly tight compared to e.g. `@Local`s.
+ return -1000;
+ }
+
+ private int getOrCreateCi(Target target, InjectionNode node, StackExtension stack, Type ciType) {
+ if (node.hasDecoration(Decorations.CANCELLABLE_CI_INDEX)) {
+ return node.getDecoration(Decorations.CANCELLABLE_CI_INDEX);
+ }
+ int ciIndex = target.allocateLocal();
+ target.addLocalVariable(ciIndex, "callbackInfo" + ciIndex, ciType.getDescriptor());
+ node.decorate(Decorations.CANCELLABLE_CI_INDEX, ciIndex);
+
+ InsnList init = new InsnList();
+ init.add(new TypeInsnNode(Opcodes.NEW, ciType.getInternalName()));
+ init.add(new InsnNode(Opcodes.DUP));
+ init.add(new LdcInsnNode(target.method.name));
+ init.add(new InsnNode(Opcodes.ICONST_1));
+ init.add(new MethodInsnNode(
+ Opcodes.INVOKESPECIAL,
+ ciType.getInternalName(),
+ "",
+ "(Ljava/lang/String;Z)V",
+ false
+ ));
+ init.add(new VarInsnNode(Opcodes.ASTORE, ciIndex));
+ target.insertBefore(node, init);
+ stack.extra(4);
+
+ SugarPostProcessingExtension.enqueuePostProcessing(this, () -> {
+ InsnList cancellation = new InsnList();
+ LabelNode notCancelled = new LabelNode();
+ cancellation.add(new VarInsnNode(Opcodes.ALOAD, ciIndex));
+ cancellation.add(new MethodInsnNode(
+ Opcodes.INVOKEVIRTUAL,
+ ciType.getInternalName(),
+ "isCancelled",
+ "()Z",
+ false
+ ));
+ cancellation.add(new JumpInsnNode(Opcodes.IFEQ, notCancelled));
+ cancellation.add(new VarInsnNode(Opcodes.ALOAD, ciIndex));
+ if (target.returnType.equals(Type.VOID_TYPE)) {
+ cancellation.add(new InsnNode(Opcodes.RETURN));
+ } else if (ASMUtils.isPrimitive(target.returnType)) {
+ cancellation.add(new MethodInsnNode(
+ Opcodes.INVOKEVIRTUAL,
+ ciType.getInternalName(),
+ "getReturnValue" + target.returnType.getDescriptor(),
+ "()" + target.returnType.getDescriptor(),
+ false
+ ));
+ cancellation.add(new InsnNode(target.returnType.getOpcode(Opcodes.IRETURN)));
+ } else {
+ cancellation.add(new MethodInsnNode(
+ Opcodes.INVOKEVIRTUAL,
+ ciType.getInternalName(),
+ "getReturnValue",
+ "()Ljava/lang/Object;",
+ false
+ ));
+ cancellation.add(new TypeInsnNode(Opcodes.CHECKCAST, target.returnType.getInternalName()));
+ cancellation.add(new InsnNode(Opcodes.ARETURN));
+ }
+ cancellation.add(notCancelled);
+ target.insns.insert(node.getCurrentTarget(), cancellation);
+ // No need to adjust the stack because we only increase the height by at most 2, which is covered by
+ // our bump of 4 earlier.
+ });
+ return ciIndex;
+ }
+}
diff --git a/src/main/java/com/llamalad7/mixinextras/sugar/impl/LocalSugarApplicator.java b/src/main/java/com/llamalad7/mixinextras/sugar/impl/LocalSugarApplicator.java
index 745ba17..0f213db 100644
--- a/src/main/java/com/llamalad7/mixinextras/sugar/impl/LocalSugarApplicator.java
+++ b/src/main/java/com/llamalad7/mixinextras/sugar/impl/LocalSugarApplicator.java
@@ -73,6 +73,12 @@ void inject(Target target, InjectionNode node, StackExtension stack) {
}
}
+ @Override
+ int postProcessingPriority() {
+ // Late, we need to be tight around the handler calls to ensure proper initialization and disposal.
+ return 1000;
+ }
+
private void initAndLoadLocalRef(Target target, InjectionNode node, int index, StackExtension stack) {
String refName = LocalRefClassGenerator.getForType(targetLocalType);
int refIndex = getOrCreateRef(target, node, index, refName, stack);
diff --git a/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarApplicator.java b/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarApplicator.java
index 1ce7d35..e6d6e44 100644
--- a/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarApplicator.java
+++ b/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarApplicator.java
@@ -2,8 +2,10 @@
import com.llamalad7.mixinextras.injector.StackExtension;
import com.llamalad7.mixinextras.service.MixinExtrasService;
+import com.llamalad7.mixinextras.sugar.Cancellable;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
+import com.llamalad7.mixinextras.utils.ASMUtils;
import com.llamalad7.mixinextras.utils.CompatibilityHelper;
import org.apache.commons.lang3.tuple.Pair;
import org.objectweb.asm.Type;
@@ -26,6 +28,7 @@ abstract class SugarApplicator {
static {
List, Class extends SugarApplicator>>> sugars = Arrays.asList(
+ Pair.of(Cancellable.class, CancellableSugarApplicator.class),
Pair.of(Local.class, LocalSugarApplicator.class),
Pair.of(Share.class, ShareSugarApplicator.class)
);
@@ -60,6 +63,15 @@ abstract class SugarApplicator {
abstract void inject(Target target, InjectionNode node, StackExtension stack);
+ int postProcessingPriority() {
+ throw new UnsupportedOperationException(
+ String.format(
+ "Sugar type %s does not support post-processing! Please inform LlamaLad7!",
+ ASMUtils.annotationToString(sugar)
+ )
+ );
+ }
+
static SugarApplicator create(InjectionInfo info, SugarParameter parameter) {
try {
Class extends SugarApplicator> clazz = MAP.get(parameter.sugar.desc);
diff --git a/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarPostProcessingExtension.java b/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarPostProcessingExtension.java
index 1ee7462..2e22a60 100644
--- a/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarPostProcessingExtension.java
+++ b/src/main/java/com/llamalad7/mixinextras/sugar/impl/SugarPostProcessingExtension.java
@@ -5,16 +5,14 @@
import org.spongepowered.asm.mixin.transformer.ext.IExtension;
import org.spongepowered.asm.mixin.transformer.ext.ITargetClassContext;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
public class SugarPostProcessingExtension implements IExtension {
- private static final Map> POST_PROCESSING_TASKS = new HashMap<>();
+ private static final Map> POST_PROCESSING_TASKS = new HashMap<>();
static void enqueuePostProcessing(SugarApplicator applicator, Runnable task) {
- POST_PROCESSING_TASKS.computeIfAbsent(applicator.info.getClassNode().name, k -> new ArrayList<>()).add(task);
+ POST_PROCESSING_TASKS.computeIfAbsent(applicator.info.getClassNode().name, k -> new ArrayList<>())
+ .add(new Task(applicator.postProcessingPriority(), task));
}
@Override
@@ -29,14 +27,33 @@ public void preApply(ITargetClassContext context) {
@Override
public void postApply(ITargetClassContext context) {
String targetName = context.getClassNode().name;
- List tasks = POST_PROCESSING_TASKS.get(targetName);
+ List tasks = POST_PROCESSING_TASKS.remove(targetName);
if (tasks != null) {
- tasks.forEach(Runnable::run);
- POST_PROCESSING_TASKS.remove(targetName);
+ Collections.sort(tasks);
+ tasks.forEach(Task::run);
}
}
@Override
public void export(MixinEnvironment env, String name, boolean force, ClassNode classNode) {
}
+
+ private static class Task implements Comparable {
+ private final int priority;
+ private final Runnable body;
+
+ public Task(int priority, Runnable body) {
+ this.priority = priority;
+ this.body = body;
+ }
+
+ public void run() {
+ body.run();
+ }
+
+ @Override
+ public int compareTo(Task o) {
+ return Integer.compare(priority, o.priority);
+ }
+ }
}
diff --git a/src/main/java/com/llamalad7/mixinextras/utils/Decorations.java b/src/main/java/com/llamalad7/mixinextras/utils/Decorations.java
index a4e4472..71c38a9 100644
--- a/src/main/java/com/llamalad7/mixinextras/utils/Decorations.java
+++ b/src/main/java/com/llamalad7/mixinextras/utils/Decorations.java
@@ -33,4 +33,9 @@ public class Decorations {
* Stores that this node has been wrapped by a {@link WrapOperation}.
*/
public static final String WRAPPED = "mixinextras_wrappedOperation";
+
+ /**
+ * Stores the shared CallbackInfo local index for this target instruction.
+ */
+ public static final String CANCELLABLE_CI_INDEX = "mixinextras_cancellableCiIndex";
}