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.feature.blockentity_incorrect_thread", false)
|
||||||
.put("mixin.perf.clear_mixin_classinfo", false)
|
.put("mixin.perf.clear_mixin_classinfo", false)
|
||||||
.put("mixin.perf.deduplicate_climate_parameters", false)
|
.put("mixin.perf.deduplicate_climate_parameters", false)
|
||||||
|
.put("mixin.perf.faster_capabilities.bytecode_analysis", false)
|
||||||
.put("mixin.bugfix.packet_leak", false)
|
.put("mixin.bugfix.packet_leak", false)
|
||||||
.put("mixin.perf.deduplicate_location", false)
|
.put("mixin.perf.deduplicate_location", false)
|
||||||
.put("mixin.perf.dynamic_entity_renderers", 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 net.minecraftforge.common.capabilities.ICapabilityProvider;
|
||||||
import org.embeddedt.modernfix.ModernFix;
|
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.*;
|
||||||
import org.objectweb.asm.commons.GeneratorAdapter;
|
import org.objectweb.asm.commons.GeneratorAdapter;
|
||||||
import org.objectweb.asm.commons.Method;
|
import org.objectweb.asm.commons.Method;
|
||||||
|
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.lang.invoke.MethodType;
|
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.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
@ -23,6 +31,7 @@ import static org.objectweb.asm.Opcodes.*;
|
||||||
* and performs direct dispatch instead of megamorphic virtual calls.
|
* and performs direct dispatch instead of megamorphic virtual calls.
|
||||||
*/
|
*/
|
||||||
public class CapabilityProviderDispatcherGenerator {
|
public class CapabilityProviderDispatcherGenerator {
|
||||||
|
private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", "");
|
||||||
|
|
||||||
private static final ConcurrentHashMap<List<Class<? extends ICapabilityProvider>>, MethodHandle> cache =
|
private static final ConcurrentHashMap<List<Class<? extends ICapabilityProvider>>, MethodHandle> cache =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
@ -67,10 +76,27 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MethodHandle generateClass(List<Class<? extends ICapabilityProvider>> providerTypes) {
|
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 {
|
try {
|
||||||
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + classCounter.incrementAndGet();
|
// Analyze each provider type
|
||||||
byte[] classBytes = generateClassBytes(className, providerTypes);
|
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
|
// Define the hidden class
|
||||||
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass(
|
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass(
|
||||||
|
|
@ -79,6 +105,12 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
MethodHandles.Lookup.ClassOption.NESTMATE
|
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
|
// Return a MethodHandle to the constructor
|
||||||
// Constructor signature: (ICapabilityProvider[])V
|
// Constructor signature: (ICapabilityProvider[])V
|
||||||
// The constructor is adapted to take an Object and return an ICapabilityProvider to match
|
// 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);
|
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||||
|
|
||||||
// Class declaration: implements ICapabilityProvider
|
// Class declaration: implements ICapabilityProvider
|
||||||
|
|
@ -105,28 +137,36 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" }
|
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
|
// Generate final fields for each provider
|
||||||
for (int i = 0; i < providerTypes.size(); i++) {
|
for (int i = 0; i < providerTypes.size(); i++) {
|
||||||
cw.visitField(
|
cw.visitField(
|
||||||
ACC_PRIVATE | ACC_FINAL,
|
ACC_PRIVATE | ACC_FINAL,
|
||||||
"provider" + i,
|
"provider" + i,
|
||||||
ICAP_PROVIDER_DESC,
|
fieldDescs[i],
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
).visitEnd();
|
).visitEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate constructor
|
// Generate constructor
|
||||||
generateConstructor(cw, className, providerTypes.size());
|
generateConstructor(cw, className, providerTypes.size(), fieldDescs);
|
||||||
|
|
||||||
// Generate getCapability method with sided parameter
|
// Generate getCapability method with sided parameter
|
||||||
generateGetCapabilityMethod(cw, className, providerTypes.size());
|
generateGetCapabilityMethod(cw, className, fieldDescs, analysisResults);
|
||||||
|
|
||||||
cw.visitEnd();
|
cw.visitEnd();
|
||||||
return cw.toByteArray();
|
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[])");
|
Method constructor = Method.getMethod("void <init>(net.minecraftforge.common.capabilities.ICapabilityProvider[])");
|
||||||
GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw);
|
GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw);
|
||||||
|
|
||||||
|
|
@ -136,14 +176,18 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
|
|
||||||
// Unpack array into final fields
|
// Unpack array into final fields
|
||||||
for (int i = 0; i < providerCount; i++) {
|
for (int i = 0; i < providerCount; i++) {
|
||||||
|
Type fieldType = Type.getType(fieldDescs[i]);
|
||||||
mg.loadThis(); // this
|
mg.loadThis(); // this
|
||||||
mg.loadArg(0); // array
|
mg.loadArg(0); // array
|
||||||
mg.push(i); // index
|
mg.push(i); // index
|
||||||
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i]
|
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i]
|
||||||
|
if (!fieldDescs[i].equals(ICAP_PROVIDER_DESC)) {
|
||||||
|
mg.checkCast(fieldType);
|
||||||
|
}
|
||||||
mg.putField(
|
mg.putField(
|
||||||
Type.getObjectType(className.replace('.', '/')),
|
Type.getObjectType(className.replace('.', '/')),
|
||||||
"provider" + i,
|
"provider" + i,
|
||||||
Type.getType(ICAP_PROVIDER_DESC)
|
fieldType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +195,9 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
mg.endMethod();
|
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)
|
// Method: <T> LazyOptional<T> getCapability(Capability<T>, Direction)
|
||||||
MethodVisitor mv = cw.visitMethod(
|
MethodVisitor mv = cw.visitMethod(
|
||||||
ACC_PUBLIC,
|
ACC_PUBLIC,
|
||||||
|
|
@ -168,15 +214,45 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
Label endLabel = new Label();
|
Label endLabel = new Label();
|
||||||
|
|
||||||
for (int i = 0; i < providerCount; i++) {
|
for (int i = 0; i < providerCount; i++) {
|
||||||
|
CapabilityAnalysisResult analysis = analysisResults.get(i);
|
||||||
Label nextLabel = new Label();
|
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);
|
// LazyOptional<T> result = this.providerN.getCapability(cap, side);
|
||||||
mv.visitVarInsn(ALOAD, 0); // this
|
mv.visitVarInsn(ALOAD, 0); // this
|
||||||
mv.visitFieldInsn(
|
mv.visitFieldInsn(
|
||||||
GETFIELD,
|
GETFIELD,
|
||||||
className.replace('.', '/'),
|
className.replace('.', '/'),
|
||||||
"provider" + i,
|
"provider" + i,
|
||||||
ICAP_PROVIDER_DESC
|
fieldDescs[i]
|
||||||
);
|
);
|
||||||
mv.visitVarInsn(ALOAD, 1); // cap parameter
|
mv.visitVarInsn(ALOAD, 1); // cap parameter
|
||||||
mv.visitVarInsn(ALOAD, 2); // side parameter
|
mv.visitVarInsn(ALOAD, 2); // side parameter
|
||||||
|
|
@ -211,9 +287,6 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
mv.visitInsn(ARETURN);
|
mv.visitInsn(ARETURN);
|
||||||
|
|
||||||
mv.visitLabel(nextLabel);
|
mv.visitLabel(nextLabel);
|
||||||
if (i < providerCount - 1) {
|
|
||||||
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no provider returned a capability, return empty
|
// If no provider returned a capability, return empty
|
||||||
|
|
@ -231,29 +304,16 @@ public class CapabilityProviderDispatcherGenerator {
|
||||||
mv.visitEnd();
|
mv.visitEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static String formatAnalysisResult(CapabilityAnalysisResult result) {
|
||||||
* Creates an instance of the optimized dispatcher for the given providers.
|
if (result instanceof CapabilityAnalysisResult.AlwaysEmpty) {
|
||||||
*/
|
return "always empty (skipped)";
|
||||||
public static ICapabilityProvider createDispatcher(ICapabilityProvider[] providers) {
|
} else if (result instanceof CapabilityAnalysisResult.KnownCapabilities known) {
|
||||||
try {
|
return "known caps: " + known.capabilities().stream()
|
||||||
MethodHandle constructor = getOrGenerateConstructor(providers);
|
.map(ref -> ref.owner() + "#" + ref.fieldName())
|
||||||
return (ICapabilityProvider) constructor.invoke(providers);
|
.collect(Collectors.joining(", "));
|
||||||
} catch (Throwable e) {
|
} else if (result instanceof CapabilityAnalysisResult.Indeterminate ind) {
|
||||||
throw new RuntimeException("Failed to create capability dispatcher instance", e);
|
return "indeterminate (" + ind.reason() + ")";
|
||||||
}
|
}
|
||||||
}
|
return result.toString();
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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