Add bytecode analysis to filter ICapabilityProvider impls where possible
Currently disabled by default till more testing is completed
This commit is contained in:
parent
878b3798f3
commit
b9933b1158
|
|
@ -174,6 +174,7 @@ public class ModernFixEarlyConfig {
|
|||
.put("mixin.feature.blockentity_incorrect_thread", false)
|
||||
.put("mixin.perf.clear_mixin_classinfo", false)
|
||||
.put("mixin.perf.deduplicate_climate_parameters", false)
|
||||
.put("mixin.perf.faster_capabilities.bytecode_analysis", false)
|
||||
.put("mixin.bugfix.packet_leak", false)
|
||||
.put("mixin.perf.deduplicate_location", false)
|
||||
.put("mixin.perf.dynamic_entity_renderers", false)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,21 @@ package org.embeddedt.modernfix.forge.capability;
|
|||
|
||||
import net.minecraftforge.common.capabilities.ICapabilityProvider;
|
||||
import org.embeddedt.modernfix.ModernFix;
|
||||
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult;
|
||||
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer;
|
||||
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityRef;
|
||||
import org.objectweb.asm.*;
|
||||
import org.objectweb.asm.commons.GeneratorAdapter;
|
||||
import org.objectweb.asm.commons.Method;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
|
@ -23,6 +31,7 @@ import static org.objectweb.asm.Opcodes.*;
|
|||
* and performs direct dispatch instead of megamorphic virtual calls.
|
||||
*/
|
||||
public class CapabilityProviderDispatcherGenerator {
|
||||
private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", "");
|
||||
|
||||
private static final ConcurrentHashMap<List<Class<? extends ICapabilityProvider>>, MethodHandle> cache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
|
@ -67,10 +76,27 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
}
|
||||
|
||||
private static MethodHandle generateClass(List<Class<? extends ICapabilityProvider>> providerTypes) {
|
||||
ModernFix.LOGGER.debug("Generating capability dispatcher for types: [{}]", providerTypes.stream().map(Class::getName).collect(Collectors.joining(", ")));
|
||||
try {
|
||||
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + classCounter.incrementAndGet();
|
||||
byte[] classBytes = generateClassBytes(className, providerTypes);
|
||||
// Analyze each provider type
|
||||
List<CapabilityAnalysisResult> analysisResults = new ArrayList<>(providerTypes.size());
|
||||
for (Class<? extends ICapabilityProvider> type : providerTypes) {
|
||||
CapabilityAnalysisResult result = CapabilityAnalyzer.analyze(type);
|
||||
analysisResults.add(result);
|
||||
}
|
||||
|
||||
int generatedClassId = classCounter.incrementAndGet();
|
||||
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId;
|
||||
|
||||
ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < providerTypes.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
sb.append(providerTypes.get(i).getName()).append(" -> ").append(formatAnalysisResult(analysisResults.get(i)));
|
||||
}
|
||||
return sb;
|
||||
});
|
||||
|
||||
byte[] classBytes = generateClassBytes(className, providerTypes, analysisResults);
|
||||
|
||||
// Define the hidden class
|
||||
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass(
|
||||
|
|
@ -79,6 +105,12 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
MethodHandles.Lookup.ClassOption.NESTMATE
|
||||
);
|
||||
|
||||
if (!GENERATED_CLASSES_FOLDER.isBlank()) {
|
||||
Path path = Paths.get(GENERATED_CLASSES_FOLDER, "generatedDispatcher" + generatedClassId + ".class");
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.write(path, classBytes);
|
||||
}
|
||||
|
||||
// Return a MethodHandle to the constructor
|
||||
// Constructor signature: (ICapabilityProvider[])V
|
||||
// The constructor is adapted to take an Object and return an ICapabilityProvider to match
|
||||
|
|
@ -92,7 +124,7 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes) {
|
||||
private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes, List<CapabilityAnalysisResult> analysisResults) {
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
|
||||
// Class declaration: implements ICapabilityProvider
|
||||
|
|
@ -105,28 +137,36 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" }
|
||||
);
|
||||
|
||||
// Compute field descriptors: use concrete type when possible for JIT devirtualization
|
||||
String[] fieldDescs = new String[providerTypes.size()];
|
||||
for (int i = 0; i < providerTypes.size(); i++) {
|
||||
Class<? extends ICapabilityProvider> type = providerTypes.get(i);
|
||||
fieldDescs[i] = (!type.isHidden() && Modifier.isPublic(type.getModifiers()))
|
||||
? Type.getDescriptor(type) : ICAP_PROVIDER_DESC;
|
||||
}
|
||||
|
||||
// Generate final fields for each provider
|
||||
for (int i = 0; i < providerTypes.size(); i++) {
|
||||
cw.visitField(
|
||||
ACC_PRIVATE | ACC_FINAL,
|
||||
"provider" + i,
|
||||
ICAP_PROVIDER_DESC,
|
||||
fieldDescs[i],
|
||||
null,
|
||||
null
|
||||
).visitEnd();
|
||||
}
|
||||
|
||||
// Generate constructor
|
||||
generateConstructor(cw, className, providerTypes.size());
|
||||
generateConstructor(cw, className, providerTypes.size(), fieldDescs);
|
||||
|
||||
// Generate getCapability method with sided parameter
|
||||
generateGetCapabilityMethod(cw, className, providerTypes.size());
|
||||
generateGetCapabilityMethod(cw, className, fieldDescs, analysisResults);
|
||||
|
||||
cw.visitEnd();
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
private static void generateConstructor(ClassWriter cw, String className, int providerCount) {
|
||||
private static void generateConstructor(ClassWriter cw, String className, int providerCount, String[] fieldDescs) {
|
||||
Method constructor = Method.getMethod("void <init>(net.minecraftforge.common.capabilities.ICapabilityProvider[])");
|
||||
GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw);
|
||||
|
||||
|
|
@ -136,14 +176,18 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
|
||||
// Unpack array into final fields
|
||||
for (int i = 0; i < providerCount; i++) {
|
||||
Type fieldType = Type.getType(fieldDescs[i]);
|
||||
mg.loadThis(); // this
|
||||
mg.loadArg(0); // array
|
||||
mg.push(i); // index
|
||||
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i]
|
||||
if (!fieldDescs[i].equals(ICAP_PROVIDER_DESC)) {
|
||||
mg.checkCast(fieldType);
|
||||
}
|
||||
mg.putField(
|
||||
Type.getObjectType(className.replace('.', '/')),
|
||||
"provider" + i,
|
||||
Type.getType(ICAP_PROVIDER_DESC)
|
||||
fieldType
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +195,9 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
mg.endMethod();
|
||||
}
|
||||
|
||||
private static void generateGetCapabilityMethod(ClassWriter cw, String className, int providerCount) {
|
||||
private static void generateGetCapabilityMethod(ClassWriter cw, String className, String[] fieldDescs, List<CapabilityAnalysisResult> analysisResults) {
|
||||
int providerCount = fieldDescs.length;
|
||||
|
||||
// Method: <T> LazyOptional<T> getCapability(Capability<T>, Direction)
|
||||
MethodVisitor mv = cw.visitMethod(
|
||||
ACC_PUBLIC,
|
||||
|
|
@ -168,15 +214,45 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
Label endLabel = new Label();
|
||||
|
||||
for (int i = 0; i < providerCount; i++) {
|
||||
CapabilityAnalysisResult analysis = analysisResults.get(i);
|
||||
Label nextLabel = new Label();
|
||||
|
||||
// AlwaysEmpty: skip code generation for this provider entirely
|
||||
if (analysis instanceof CapabilityAnalysisResult.AlwaysEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// KnownCapabilities: emit guard checks before dispatch
|
||||
if (analysis instanceof CapabilityAnalysisResult.KnownCapabilities known
|
||||
&& known.capabilities().size() <= 5) {
|
||||
if (known.capabilities().size() == 1) {
|
||||
// Single cap: if (cap != KNOWN_CAP) goto nextProvider
|
||||
CapabilityRef ref = known.capabilities().iterator().next();
|
||||
mv.visitVarInsn(ALOAD, 1); // cap parameter
|
||||
mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC);
|
||||
mv.visitJumpInsn(IF_ACMPNE, nextLabel);
|
||||
} else {
|
||||
// Multiple caps: check each, jump to callProvider on match
|
||||
Label callProvider = new Label();
|
||||
for (CapabilityRef ref : known.capabilities()) {
|
||||
mv.visitVarInsn(ALOAD, 1); // cap parameter
|
||||
mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC);
|
||||
mv.visitJumpInsn(IF_ACMPEQ, callProvider);
|
||||
}
|
||||
// No match, skip this provider
|
||||
mv.visitJumpInsn(GOTO, nextLabel);
|
||||
mv.visitLabel(callProvider);
|
||||
}
|
||||
}
|
||||
// Indeterminate: no guard, fall through to dispatch
|
||||
|
||||
// LazyOptional<T> result = this.providerN.getCapability(cap, side);
|
||||
mv.visitVarInsn(ALOAD, 0); // this
|
||||
mv.visitFieldInsn(
|
||||
GETFIELD,
|
||||
className.replace('.', '/'),
|
||||
"provider" + i,
|
||||
ICAP_PROVIDER_DESC
|
||||
fieldDescs[i]
|
||||
);
|
||||
mv.visitVarInsn(ALOAD, 1); // cap parameter
|
||||
mv.visitVarInsn(ALOAD, 2); // side parameter
|
||||
|
|
@ -211,9 +287,6 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
mv.visitInsn(ARETURN);
|
||||
|
||||
mv.visitLabel(nextLabel);
|
||||
if (i < providerCount - 1) {
|
||||
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
// If no provider returned a capability, return empty
|
||||
|
|
@ -231,29 +304,16 @@ public class CapabilityProviderDispatcherGenerator {
|
|||
mv.visitEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of the optimized dispatcher for the given providers.
|
||||
*/
|
||||
public static ICapabilityProvider createDispatcher(ICapabilityProvider[] providers) {
|
||||
try {
|
||||
MethodHandle constructor = getOrGenerateConstructor(providers);
|
||||
return (ICapabilityProvider) constructor.invoke(providers);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Failed to create capability dispatcher instance", e);
|
||||
private static String formatAnalysisResult(CapabilityAnalysisResult result) {
|
||||
if (result instanceof CapabilityAnalysisResult.AlwaysEmpty) {
|
||||
return "always empty (skipped)";
|
||||
} else if (result instanceof CapabilityAnalysisResult.KnownCapabilities known) {
|
||||
return "known caps: " + known.capabilities().stream()
|
||||
.map(ref -> ref.owner() + "#" + ref.fieldName())
|
||||
.collect(Collectors.joining(", "));
|
||||
} else if (result instanceof CapabilityAnalysisResult.Indeterminate ind) {
|
||||
return "indeterminate (" + ind.reason() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache of generated classes. Use with caution.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of cached dispatcher classes.
|
||||
*/
|
||||
public static int getCacheSize() {
|
||||
return cache.size();
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package org.embeddedt.modernfix.forge.capability.analysis;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Result of analyzing a capability provider's {@code getCapability} bytecode.
|
||||
*/
|
||||
public sealed interface CapabilityAnalysisResult {
|
||||
/**
|
||||
* The provider can only return non-empty for these specific capabilities.
|
||||
*/
|
||||
record KnownCapabilities(Set<CapabilityRef> capabilities) implements CapabilityAnalysisResult {}
|
||||
|
||||
/**
|
||||
* The provider always returns {@code LazyOptional.empty()}.
|
||||
*/
|
||||
record AlwaysEmpty() implements CapabilityAnalysisResult {}
|
||||
|
||||
/**
|
||||
* Analysis could not determine the set of capabilities; fall back to unguarded dispatch.
|
||||
*/
|
||||
record Indeterminate(String reason) implements CapabilityAnalysisResult {}
|
||||
}
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
package org.embeddedt.modernfix.forge.capability.analysis;
|
||||
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraftforge.common.capabilities.Capability;
|
||||
import net.minecraftforge.common.capabilities.ICapabilityProvider;
|
||||
import org.embeddedt.modernfix.ModernFix;
|
||||
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.*;
|
||||
import org.objectweb.asm.tree.analysis.Analyzer;
|
||||
import org.objectweb.asm.tree.analysis.AnalyzerException;
|
||||
import org.objectweb.asm.tree.analysis.Frame;
|
||||
import org.objectweb.asm.tree.analysis.SourceValue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Analyzes {@code getCapability} bytecode to determine which capabilities a provider handles.
|
||||
*/
|
||||
public class CapabilityAnalyzer {
|
||||
|
||||
private static final ConcurrentHashMap<Class<?>, CapabilityAnalysisResult> cache = new ConcurrentHashMap<>();
|
||||
|
||||
private static final String CAPABILITY_INTERNAL = "net/minecraftforge/common/capabilities/Capability";
|
||||
private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;";
|
||||
private static final String LAZY_OPTIONAL_INTERNAL = "net/minecraftforge/common/util/LazyOptional";
|
||||
private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;";
|
||||
private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;";
|
||||
private static final String GET_CAPABILITY_DESC = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC;
|
||||
private static final String ICAP_PROVIDER_INTERNAL = "net/minecraftforge/common/capabilities/ICapabilityProvider";
|
||||
|
||||
public static CapabilityAnalysisResult analyze(Class<? extends ICapabilityProvider> clazz) {
|
||||
if (!ModernFixMixinPlugin.instance.isOptionEnabled("perf.faster_capabilities.bytecode_analysis.CapabilityAnalyzer")) {
|
||||
return new CapabilityAnalysisResult.Indeterminate("bytecode analysis disabled");
|
||||
}
|
||||
CapabilityAnalysisResult result = cache.get(clazz);
|
||||
if (result != null) return result;
|
||||
result = doAnalyzeSafe(clazz);
|
||||
CapabilityAnalysisResult existing = cache.putIfAbsent(clazz, result);
|
||||
return existing != null ? existing : result;
|
||||
}
|
||||
|
||||
private static CapabilityAnalysisResult doAnalyzeSafe(Class<?> clazz) {
|
||||
try {
|
||||
return doAnalyze(clazz);
|
||||
} catch (Exception e) {
|
||||
ModernFix.LOGGER.debug("Capability analysis failed for {}: {}", clazz.getName(), e.getMessage());
|
||||
return new CapabilityAnalysisResult.Indeterminate("analysis exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static CapabilityAnalysisResult doAnalyze(Class<?> clazz) throws IOException, AnalyzerException {
|
||||
// Find the class that actually declares getCapability via reflection
|
||||
Class<?> declaringClass;
|
||||
try {
|
||||
declaringClass = clazz.getMethod("getCapability", Capability.class, Direction.class).getDeclaringClass();
|
||||
} catch (NoSuchMethodException e) {
|
||||
return new CapabilityAnalysisResult.AlwaysEmpty();
|
||||
}
|
||||
|
||||
if (declaringClass.getName().replace('.', '/').equals(ICAP_PROVIDER_INTERNAL)) {
|
||||
return new CapabilityAnalysisResult.AlwaysEmpty();
|
||||
}
|
||||
|
||||
String declaringClassName = declaringClass.getName().replace('.', '/');
|
||||
ClassNode declaringClassNode = readClass(declaringClass);
|
||||
if (declaringClassNode == null) {
|
||||
return new CapabilityAnalysisResult.Indeterminate("cannot read bytecode for " + declaringClass.getName());
|
||||
}
|
||||
|
||||
MethodNode getCapMethod = findGetCapabilityMethod(declaringClassNode);
|
||||
if (getCapMethod == null) {
|
||||
return new CapabilityAnalysisResult.Indeterminate("method not found in bytecode for " + declaringClass.getName());
|
||||
}
|
||||
|
||||
// Run the source analysis
|
||||
CapabilitySourceInterpreter interpreter = new CapabilitySourceInterpreter();
|
||||
Analyzer<SourceValue> analyzer = new Analyzer<>(interpreter);
|
||||
Frame<SourceValue>[] frames = analyzer.analyze(declaringClassName, getCapMethod);
|
||||
|
||||
// Build if-guard map: maps instruction indices to CapabilityRef for guarded regions
|
||||
List<GuardRegion> guardRegions = findGuardRegions(getCapMethod, frames);
|
||||
|
||||
// Classify each ARETURN
|
||||
InsnList instructions = getCapMethod.instructions;
|
||||
Set<CapabilityRef> knownCaps = new HashSet<>();
|
||||
boolean hasIndeterminate = false;
|
||||
String indeterminateReason = null;
|
||||
|
||||
for (int i = 0; i < instructions.size(); i++) {
|
||||
AbstractInsnNode insn = instructions.get(i);
|
||||
if (insn.getOpcode() != Opcodes.ARETURN) continue;
|
||||
|
||||
Frame<SourceValue> frame = frames[i];
|
||||
if (frame == null) continue; // dead code
|
||||
|
||||
SourceValue topOfStack = frame.getStack(frame.getStackSize() - 1);
|
||||
|
||||
ReturnClassification classification = classifyReturnSources(
|
||||
topOfStack, interpreter, i, guardRegions, clazz, instructions);
|
||||
|
||||
if (classification instanceof ReturnClassification.Known known) {
|
||||
knownCaps.addAll(known.caps);
|
||||
} else if (classification instanceof ReturnClassification.Unknown unknown) {
|
||||
hasIndeterminate = true;
|
||||
indeterminateReason = unknown.reason;
|
||||
}
|
||||
// Empty: skip
|
||||
}
|
||||
|
||||
if (hasIndeterminate) {
|
||||
CapabilityAnalysisResult result = new CapabilityAnalysisResult.Indeterminate(indeterminateReason);
|
||||
ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (knownCaps.isEmpty()) {
|
||||
ModernFix.LOGGER.debug("Capability analysis for {}: AlwaysEmpty", clazz.getName());
|
||||
return new CapabilityAnalysisResult.AlwaysEmpty();
|
||||
}
|
||||
|
||||
CapabilityAnalysisResult result = new CapabilityAnalysisResult.KnownCapabilities(Set.copyOf(knownCaps));
|
||||
ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ReturnClassification classifyReturnSources(
|
||||
SourceValue topOfStack,
|
||||
CapabilitySourceInterpreter interpreter,
|
||||
int returnIndex,
|
||||
List<GuardRegion> guardRegions,
|
||||
Class<?> originalClass,
|
||||
InsnList instructions) {
|
||||
|
||||
Set<CapabilityRef> caps = new HashSet<>();
|
||||
List<AbstractInsnNode> unknownSources = new ArrayList<>();
|
||||
String unknownReason = null;
|
||||
|
||||
for (AbstractInsnNode source : topOfStack.insns) {
|
||||
ReturnClassification sourceClassification = classifySingleSource(
|
||||
source, interpreter, originalClass);
|
||||
|
||||
if (sourceClassification instanceof ReturnClassification.Unknown unknown) {
|
||||
unknownSources.add(source);
|
||||
unknownReason = unknown.reason;
|
||||
} else if (sourceClassification instanceof ReturnClassification.Known known) {
|
||||
caps.addAll(known.caps);
|
||||
}
|
||||
}
|
||||
|
||||
// If any source is unknown, try the guard region fallback before giving up
|
||||
if (!unknownSources.isEmpty()) {
|
||||
// Check if the return itself is in a guard region
|
||||
for (GuardRegion guard : guardRegions) {
|
||||
if (returnIndex > guard.guardIndex && returnIndex < guard.targetIndex) {
|
||||
return new ReturnClassification.Known(Set.of(guard.capabilityRef));
|
||||
}
|
||||
}
|
||||
// Also check if each unknown source instruction is inside a guard region.
|
||||
// This handles ternary patterns where both branches merge into a single
|
||||
// ARETURN after the guard, but the unknown value was produced inside it.
|
||||
boolean allResolved = true;
|
||||
for (AbstractInsnNode unknownSource : unknownSources) {
|
||||
int sourceIndex = instructions.indexOf(unknownSource);
|
||||
boolean resolved = false;
|
||||
for (GuardRegion guard : guardRegions) {
|
||||
if (sourceIndex > guard.guardIndex && sourceIndex < guard.targetIndex) {
|
||||
caps.add(guard.capabilityRef);
|
||||
resolved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!resolved) {
|
||||
allResolved = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allResolved) {
|
||||
return new ReturnClassification.Unknown(unknownReason);
|
||||
}
|
||||
}
|
||||
|
||||
if (caps.isEmpty()) {
|
||||
return ReturnClassification.EMPTY;
|
||||
}
|
||||
return new ReturnClassification.Known(caps);
|
||||
}
|
||||
|
||||
private static ReturnClassification classifySingleSource(
|
||||
AbstractInsnNode source,
|
||||
CapabilitySourceInterpreter interpreter,
|
||||
Class<?> originalClass) {
|
||||
|
||||
if (source instanceof MethodInsnNode methodInsn) {
|
||||
// Case: Capability.orEmpty(...)
|
||||
if (methodInsn.getOpcode() == Opcodes.INVOKEVIRTUAL
|
||||
&& methodInsn.owner.equals(CAPABILITY_INTERNAL)
|
||||
&& methodInsn.name.equals("orEmpty")) {
|
||||
return classifyOrEmptyCall(methodInsn, interpreter);
|
||||
}
|
||||
|
||||
// Case: LazyOptional.empty()
|
||||
if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC
|
||||
&& methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL)
|
||||
&& methodInsn.name.equals("empty")) {
|
||||
return ReturnClassification.EMPTY;
|
||||
}
|
||||
|
||||
// Case: LazyOptional.of(null)
|
||||
if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC
|
||||
&& methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL)
|
||||
&& methodInsn.name.equals("of")) {
|
||||
List<SourceValue> args = interpreter.getCallArguments(methodInsn);
|
||||
if (!args.isEmpty()) {
|
||||
SourceValue arg = args.get(0);
|
||||
if (arg.insns.size() == 1
|
||||
&& arg.insns.iterator().next().getOpcode() == Opcodes.ACONST_NULL) {
|
||||
return ReturnClassification.EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case: super.getCapability(...)
|
||||
if (methodInsn.getOpcode() == Opcodes.INVOKESPECIAL
|
||||
&& methodInsn.name.equals("getCapability")
|
||||
&& methodInsn.desc.equals(GET_CAPABILITY_DESC)) {
|
||||
return classifySuperDelegation(originalClass);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReturnClassification.Unknown("unclassified source: " + source.getClass().getSimpleName()
|
||||
+ " opcode=" + source.getOpcode());
|
||||
}
|
||||
|
||||
private static ReturnClassification classifyOrEmptyCall(
|
||||
MethodInsnNode methodInsn, CapabilitySourceInterpreter interpreter) {
|
||||
List<SourceValue> args = interpreter.getCallArguments(methodInsn);
|
||||
if (args.isEmpty()) {
|
||||
return new ReturnClassification.Unknown("orEmpty call with no recorded arguments");
|
||||
}
|
||||
|
||||
// arg 0 is the receiver (the Capability instance)
|
||||
SourceValue receiver = args.get(0);
|
||||
for (AbstractInsnNode recvSource : receiver.insns) {
|
||||
if (recvSource instanceof FieldInsnNode fieldInsn
|
||||
&& fieldInsn.getOpcode() == Opcodes.GETSTATIC
|
||||
&& fieldInsn.desc.equals(CAPABILITY_DESC)) {
|
||||
return new ReturnClassification.Known(
|
||||
Set.of(new CapabilityRef(fieldInsn.owner, fieldInsn.name)));
|
||||
}
|
||||
}
|
||||
|
||||
return new ReturnClassification.Unknown("orEmpty receiver is not a static Capability field");
|
||||
}
|
||||
|
||||
private static ReturnClassification classifySuperDelegation(Class<?> originalClass) {
|
||||
Class<?> superClass = originalClass.getSuperclass();
|
||||
if (superClass == null || superClass == Object.class) {
|
||||
return ReturnClassification.EMPTY;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends ICapabilityProvider> superProvider =
|
||||
(Class<? extends ICapabilityProvider>) superClass;
|
||||
CapabilityAnalysisResult superResult = analyze(superProvider);
|
||||
if (superResult instanceof CapabilityAnalysisResult.KnownCapabilities known) {
|
||||
return new ReturnClassification.Known(known.capabilities());
|
||||
} else if (superResult instanceof CapabilityAnalysisResult.AlwaysEmpty) {
|
||||
return ReturnClassification.EMPTY;
|
||||
} else if (superResult instanceof CapabilityAnalysisResult.Indeterminate ind) {
|
||||
return new ReturnClassification.Unknown("super delegation: " + ind.reason());
|
||||
}
|
||||
return ReturnClassification.EMPTY;
|
||||
}
|
||||
|
||||
private static List<GuardRegion> findGuardRegions(MethodNode method, Frame<SourceValue>[] frames) {
|
||||
List<GuardRegion> regions = new ArrayList<>();
|
||||
InsnList instructions = method.instructions;
|
||||
|
||||
for (int i = 0; i < instructions.size(); i++) {
|
||||
AbstractInsnNode insn = instructions.get(i);
|
||||
int opcode = insn.getOpcode();
|
||||
if (opcode != Opcodes.IF_ACMPEQ && opcode != Opcodes.IF_ACMPNE) continue;
|
||||
|
||||
Frame<SourceValue> frame = frames[i];
|
||||
if (frame == null || frame.getStackSize() < 2) continue;
|
||||
|
||||
SourceValue val1 = frame.getStack(frame.getStackSize() - 2);
|
||||
SourceValue val2 = frame.getStack(frame.getStackSize() - 1);
|
||||
|
||||
// Check if one traces to ALOAD 1 (cap parameter) and other to GETSTATIC Capability
|
||||
CapabilityRef capRef = findCapRef(val1);
|
||||
if (capRef == null) capRef = findCapRef(val2);
|
||||
boolean hasParamLoad = isCapParamLoad(val1) || isCapParamLoad(val2);
|
||||
|
||||
if (capRef != null && hasParamLoad) {
|
||||
JumpInsnNode jumpInsn = (JumpInsnNode) insn;
|
||||
int targetIndex = instructions.indexOf(jumpInsn.label);
|
||||
|
||||
if (opcode == Opcodes.IF_ACMPNE) {
|
||||
// if (cap != KNOWN_CAP) goto target -> the code between insn and target is for KNOWN_CAP
|
||||
regions.add(new GuardRegion(capRef, i, targetIndex));
|
||||
} else {
|
||||
// IF_ACMPEQ: if (cap == KNOWN_CAP) goto target -> the target is the guarded code
|
||||
// Find the end of the guarded region (next label or return)
|
||||
// For simplicity, mark from target to the next unconditional branch or return
|
||||
// TODO: that simplification may have edge cases
|
||||
int endIndex = findGuardedRegionEnd(instructions, targetIndex);
|
||||
regions.add(new GuardRegion(capRef, targetIndex, endIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
private static int findGuardedRegionEnd(InsnList instructions, int startIndex) {
|
||||
for (int i = startIndex; i < instructions.size(); i++) {
|
||||
AbstractInsnNode insn = instructions.get(i);
|
||||
int opcode = insn.getOpcode();
|
||||
if (opcode == Opcodes.GOTO || opcode == Opcodes.ARETURN
|
||||
|| opcode == Opcodes.RETURN || opcode == Opcodes.ATHROW) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return instructions.size();
|
||||
}
|
||||
|
||||
private static CapabilityRef findCapRef(SourceValue sv) {
|
||||
CapabilityRef ref = null;
|
||||
for (AbstractInsnNode src : sv.insns) {
|
||||
if (src instanceof FieldInsnNode fieldInsn
|
||||
&& fieldInsn.getOpcode() == Opcodes.GETSTATIC
|
||||
&& fieldInsn.desc.equals(CAPABILITY_DESC)) {
|
||||
if (ref == null) {
|
||||
ref = new CapabilityRef(fieldInsn.owner, fieldInsn.name);
|
||||
} else if (!ref.owner().equals(fieldInsn.owner) || !ref.fieldName().equals(fieldInsn.name)) {
|
||||
return null; // ambiguous: multiple different capability fields
|
||||
}
|
||||
} else {
|
||||
return null; // non-capability source
|
||||
}
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
private static boolean isCapParamLoad(SourceValue sv) {
|
||||
if (sv.insns.isEmpty()) return false;
|
||||
for (AbstractInsnNode src : sv.insns) {
|
||||
if (!(src instanceof VarInsnNode varInsn)
|
||||
|| varInsn.getOpcode() != Opcodes.ALOAD
|
||||
|| varInsn.var != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static MethodNode findGetCapabilityMethod(ClassNode classNode) {
|
||||
for (MethodNode method : classNode.methods) {
|
||||
if (method.name.equals("getCapability") && method.desc.equals(GET_CAPABILITY_DESC)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ClassNode readClass(Class<?> clazz) throws IOException {
|
||||
String resourcePath = "/" + clazz.getName().replace('.', '/') + ".class";
|
||||
try (InputStream is = clazz.getResourceAsStream(resourcePath)) {
|
||||
if (is == null) return null;
|
||||
ClassReader reader = new ClassReader(is);
|
||||
ClassNode node = new ClassNode();
|
||||
reader.accept(node, 0);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface ReturnClassification {
|
||||
ReturnClassification EMPTY = new Empty();
|
||||
|
||||
record Known(Set<CapabilityRef> caps) implements ReturnClassification {}
|
||||
record Empty() implements ReturnClassification {}
|
||||
record Unknown(String reason) implements ReturnClassification {}
|
||||
}
|
||||
|
||||
private record GuardRegion(CapabilityRef capabilityRef, int guardIndex, int targetIndex) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package org.embeddedt.modernfix.forge.capability.analysis;
|
||||
|
||||
/**
|
||||
* Identifies a capability by the static field that holds it.
|
||||
*
|
||||
* @param owner internal class name (e.g. {@code net/minecraftforge/common/capabilities/ForgeCapabilities})
|
||||
* @param fieldName field name (e.g. {@code ITEM_HANDLER})
|
||||
*/
|
||||
public record CapabilityRef(String owner, String fieldName) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return owner + "." + fieldName;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package org.embeddedt.modernfix.forge.capability.analysis;
|
||||
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.analysis.SourceInterpreter;
|
||||
import org.objectweb.asm.tree.analysis.SourceValue;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Extends {@link SourceInterpreter} with two enhancements:
|
||||
* <ul>
|
||||
* <li>Propagates source values through copy operations (ALOAD/ASTORE), so that
|
||||
* {@code result = cap.orEmpty(...); return result;} traces back to the INVOKEVIRTUAL.</li>
|
||||
* <li>Records argument {@link SourceValue}s for each call instruction, so we can later
|
||||
* inspect the receiver/arguments of {@code orEmpty()} calls.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class CapabilitySourceInterpreter extends SourceInterpreter {
|
||||
|
||||
private final Map<AbstractInsnNode, List<SourceValue>> callArguments = new HashMap<>();
|
||||
|
||||
public CapabilitySourceInterpreter() {
|
||||
super(ASM9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recorded argument SourceValues for a given call instruction.
|
||||
*/
|
||||
public List<SourceValue> getCallArguments(AbstractInsnNode insn) {
|
||||
return callArguments.getOrDefault(insn, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceValue copyOperation(AbstractInsnNode insn, SourceValue value) {
|
||||
// Propagate sources through loads/stores instead of creating a new source.
|
||||
// However, if the value has no sources (e.g. a method parameter), fall back to
|
||||
// the default behavior so the ALOAD instruction itself is recorded as the source.
|
||||
if (value.insns.isEmpty()) {
|
||||
return super.copyOperation(insn, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceValue naryOperation(AbstractInsnNode insn, List<? extends SourceValue> values) {
|
||||
callArguments.put(insn, new ArrayList<>(values));
|
||||
return super.naryOperation(insn, values);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user