Fix looking up private static final Capability fields

This commit is contained in:
embeddedt 2026-03-27 22:38:18 -04:00
parent 94f1fbf4db
commit 2081b63b56
No known key found for this signature in database
GPG Key ID: A69433EC199B5613

View File

@ -1,5 +1,6 @@
package org.embeddedt.modernfix.forge.capability; package org.embeddedt.modernfix.forge.capability;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider; import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.common.util.LazyOptional;
import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.ModernFix;
@ -10,6 +11,7 @@ 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.Field;
import java.lang.reflect.Modifier; 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;
@ -74,6 +76,7 @@ public class CapabilityProviderDispatcherGenerator {
private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;"; private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;";
private static final String MAP_DESC = "Ljava/util/Map;"; private static final String MAP_DESC = "Ljava/util/Map;";
private static final String MAP_SIGNATURE = "Ljava/util/Map<Lnet/minecraftforge/common/capabilities/Capability<*>;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;>;"; private static final String MAP_SIGNATURE = "Ljava/util/Map<Lnet/minecraftforge/common/capabilities/Capability<*>;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;>;";
private static final String LOOKUP_DESC = "Ljava/lang/invoke/MethodHandles$Lookup;";
/** /**
* Gets or generates a constructor MethodHandle for the given capability provider types. * Gets or generates a constructor MethodHandle for the given capability provider types.
@ -117,6 +120,14 @@ public class CapabilityProviderDispatcherGenerator {
int generatedClassId = classCounter.incrementAndGet(); int generatedClassId = classCounter.incrementAndGet();
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId; String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId;
List<ProviderDispatch> dispatches = optimizeDispatches(buildDispatchList(providerTypes, analysisResults));
// Assign a stable index to every unique CapabilityRef across all dispatches.
// We resolve the actual Capability<?> instances here (in Java) so the generated
// <clinit> only needs simple classDataAt calls - no reflection bytecode needed.
LinkedHashMap<CapabilityRef, Integer> capRefIndices = collectCapabilityRefs(dispatches);
List<Capability<?>> capValues = resolveCapabilityValues(capRefIndices);
ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> { ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (int i = 0; i < providerTypes.size(); i++) { for (int i = 0; i < providerTypes.size(); i++) {
@ -126,11 +137,14 @@ public class CapabilityProviderDispatcherGenerator {
return sb; return sb;
}); });
byte[] classBytes = generateClassBytes(className, providerTypes, analysisResults); byte[] classBytes = generateClassBytes(className, providerTypes, dispatches, capRefIndices);
// Define the hidden class // Define the hidden class, injecting the resolved Capability instances as class data.
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass( // The generated <clinit> retrieves them via MethodHandles.classDataAt so it never
// needs to perform reflection itself - private fields are handled transparently here.
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClassWithClassData(
classBytes, classBytes,
capValues,
true, true,
MethodHandles.Lookup.ClassOption.NESTMATE MethodHandles.Lookup.ClassOption.NESTMATE
); );
@ -154,6 +168,47 @@ public class CapabilityProviderDispatcherGenerator {
} }
} }
/**
* Collects all unique {@link CapabilityRef}s referenced by {@code dispatches} in encounter order,
* assigning each a stable list index for use with {@code classDataAt}.
*/
private static LinkedHashMap<CapabilityRef, Integer> collectCapabilityRefs(List<ProviderDispatch> dispatches) {
LinkedHashMap<CapabilityRef, Integer> result = new LinkedHashMap<>();
for (ProviderDispatch dispatch : dispatches) {
if (dispatch instanceof ProviderDispatch.Guarded g) {
result.putIfAbsent(g.capability(), result.size());
} else if (dispatch instanceof ProviderDispatch.Hash hash) {
for (ProviderDispatch.Guarded g : hash.entries()) {
result.putIfAbsent(g.capability(), result.size());
}
}
}
return result;
}
/**
* Resolves the actual {@link Capability} instances for all refs at class-generation time.
* Uses reflection (with {@code setAccessible}) so private fields are handled without any
* reflection bytecode appearing in the generated class.
*/
private static List<Capability<?>> resolveCapabilityValues(LinkedHashMap<CapabilityRef, Integer> capRefIndices) {
@SuppressWarnings("unchecked")
Capability<?>[] caps = new Capability[capRefIndices.size()];
for (Map.Entry<CapabilityRef, Integer> entry : capRefIndices.entrySet()) {
CapabilityRef ref = entry.getKey();
try {
Class<?> clazz = Class.forName(ref.owner().replace('/', '.'), false,
CapabilityProviderDispatcherGenerator.class.getClassLoader());
Field field = clazz.getDeclaredField(ref.fieldName());
field.setAccessible(true);
caps[entry.getValue()] = (Capability<?>) field.get(null);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to resolve capability field " + ref, e);
}
}
return Arrays.asList(caps);
}
/** /**
* Build the dispatch list describing how each provider should be handled. * Build the dispatch list describing how each provider should be handled.
*/ */
@ -261,9 +316,8 @@ public class CapabilityProviderDispatcherGenerator {
return fields; return fields;
} }
private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes, List<CapabilityAnalysisResult> analysisResults) { private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes,
List<ProviderDispatch> dispatches = optimizeDispatches(buildDispatchList(providerTypes, analysisResults)); List<ProviderDispatch> dispatches, LinkedHashMap<CapabilityRef, Integer> capRefIndices) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) {
@Override @Override
protected ClassLoader getClassLoader() { protected ClassLoader getClassLoader() {
@ -271,11 +325,13 @@ public class CapabilityProviderDispatcherGenerator {
} }
}; };
String internalName = className.replace('.', '/');
// Class declaration: implements ICapabilityProvider // Class declaration: implements ICapabilityProvider
cw.visit( cw.visit(
V17, V17,
ACC_PUBLIC | ACC_FINAL | ACC_SUPER, ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
className.replace('.', '/'), internalName,
null, null,
"java/lang/Object", "java/lang/Object",
new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" } new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" }
@ -301,17 +357,74 @@ public class CapabilityProviderDispatcherGenerator {
} }
} }
// Generate one static final field per unique CapabilityRef.
// These are populated in <clinit> via MethodHandles.classDataAt, which reads the
// Capability<?> instances injected by defineHiddenClassWithClassData. This avoids
// any reflection bytecode in the generated class and handles private fields transparently.
for (Map.Entry<CapabilityRef, Integer> entry : capRefIndices.entrySet()) {
cw.visitField(ACC_PRIVATE | ACC_STATIC | ACC_FINAL,
capRefFieldName(entry.getValue()), CAPABILITY_DESC, null, null).visitEnd();
}
// Generate <clinit> to load capability instances from class data
if (!capRefIndices.isEmpty()) {
generateClinit(cw, internalName, capRefIndices);
}
// Generate constructor // Generate constructor
generateConstructor(cw, className, providerFields, dispatches); generateConstructor(cw, className, providerFields, dispatches, capRefIndices);
// Generate getCapability method with sided parameter // Generate getCapability method with sided parameter
generateGetCapabilityMethod(cw, className, dispatches); generateGetCapabilityMethod(cw, className, dispatches, capRefIndices);
cw.visitEnd(); cw.visitEnd();
return cw.toByteArray(); return cw.toByteArray();
} }
private static void generateConstructor(ClassWriter cw, String className, Map<Integer, String> providerFields, List<ProviderDispatch> dispatches) { private static String capRefFieldName(int index) {
return "capRef" + index;
}
/**
* Generates {@code <clinit>} that loads each capability from class data injected at define time.
* The bytecode is simply: {@code capRefN = MethodHandles.classDataAt(lookup(), "", Capability.class, N)}.
*/
private static void generateClinit(ClassWriter cw, String internalName, LinkedHashMap<CapabilityRef, Integer> capRefIndices) {
MethodVisitor mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
for (int i = 0; i < capRefIndices.size(); i++) {
// MethodHandles.lookup()
mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup",
"()" + LOOKUP_DESC, false);
// "_" (classDataAt requires this exact name)
mv.visitLdcInsn("_");
// Capability.class
mv.visitLdcInsn(Type.getType(CAPABILITY_DESC));
// index
mv.visitLdcInsn(i);
// MethodHandles.classDataAt(lookup, name, type, index) Object
mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "classDataAt",
"(" + LOOKUP_DESC + "Ljava/lang/String;Ljava/lang/Class;I)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/Capability");
mv.visitFieldInsn(PUTSTATIC, internalName, capRefFieldName(i), CAPABILITY_DESC);
}
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
/**
* Emits a load of the capability constant for {@code ref} from the generated class's own static field.
*/
private static void emitCapabilityLoad(MethodVisitor mv, String internalName, CapabilityRef ref,
Map<CapabilityRef, Integer> capRefIndices) {
mv.visitFieldInsn(GETSTATIC, internalName, capRefFieldName(capRefIndices.get(ref)), CAPABILITY_DESC);
}
private static void generateConstructor(ClassWriter cw, String className, Map<Integer, String> providerFields,
List<ProviderDispatch> dispatches, Map<CapabilityRef, Integer> capRefIndices) {
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);
Type classType = Type.getObjectType(className.replace('.', '/')); Type classType = Type.getObjectType(className.replace('.', '/'));
@ -338,7 +451,7 @@ public class CapabilityProviderDispatcherGenerator {
// Build hash maps // Build hash maps
for (ProviderDispatch dispatch : dispatches) { for (ProviderDispatch dispatch : dispatches) {
if (dispatch instanceof ProviderDispatch.Hash hash) { if (dispatch instanceof ProviderDispatch.Hash hash) {
generateMapConstruction(mg, classType, hash); generateMapConstruction(mg, classType, hash, capRefIndices);
} }
} }
@ -346,7 +459,8 @@ public class CapabilityProviderDispatcherGenerator {
mg.endMethod(); mg.endMethod();
} }
private static void generateMapConstruction(GeneratorAdapter mg, Type classType, ProviderDispatch.Hash hash) { private static void generateMapConstruction(GeneratorAdapter mg, Type classType, ProviderDispatch.Hash hash,
Map<CapabilityRef, Integer> capRefIndices) {
List<ProviderDispatch.Guarded> entries = hash.entries(); List<ProviderDispatch.Guarded> entries = hash.entries();
mg.loadThis(); // for PUTFIELD at the end mg.loadThis(); // for PUTFIELD at the end
@ -356,7 +470,7 @@ public class CapabilityProviderDispatcherGenerator {
ProviderDispatch.Guarded g = entries.get(i); ProviderDispatch.Guarded g = entries.get(i);
mg.dup(); mg.dup();
mg.push(i); mg.push(i);
mg.visitFieldInsn(GETSTATIC, g.capability().owner(), g.capability().fieldName(), CAPABILITY_DESC); emitCapabilityLoad(mg, classType.getInternalName(), g.capability(), capRefIndices);
mg.loadArg(0); mg.loadArg(0);
mg.push(g.providerIndex()); mg.push(g.providerIndex());
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC));
@ -370,7 +484,8 @@ public class CapabilityProviderDispatcherGenerator {
mg.putField(classType, "capMap" + hash.mapIndex(), Type.getType(MAP_DESC)); mg.putField(classType, "capMap" + hash.mapIndex(), Type.getType(MAP_DESC));
} }
private static void generateGetCapabilityMethod(ClassWriter cw, String className, List<ProviderDispatch> dispatches) { private static void generateGetCapabilityMethod(ClassWriter cw, String className, List<ProviderDispatch> dispatches,
Map<CapabilityRef, Integer> capRefIndices) {
// 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,
@ -401,7 +516,7 @@ public class CapabilityProviderDispatcherGenerator {
emitHashDispatch(mv, internalName, getCapDesc, hash, nextLabel); emitHashDispatch(mv, internalName, getCapDesc, hash, nextLabel);
di++; di++;
} else if (dispatch instanceof ProviderDispatch.Guarded) { } else if (dispatch instanceof ProviderDispatch.Guarded) {
di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel); di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel, capRefIndices);
} else { } else {
var u = (ProviderDispatch.Unguarded) dispatch; var u = (ProviderDispatch.Unguarded) dispatch;
emitProviderGetCapability(mv, internalName, getCapDesc, u.providerIndex(), u.fieldDesc()); emitProviderGetCapability(mv, internalName, getCapDesc, u.providerIndex(), u.fieldDesc());
@ -490,7 +605,8 @@ public class CapabilityProviderDispatcherGenerator {
* @return the updated dispatch index (past the consumed group) * @return the updated dispatch index (past the consumed group)
*/ */
private static int emitGuardedDispatch(MethodVisitor mv, String internalName, String getCapDesc, private static int emitGuardedDispatch(MethodVisitor mv, String internalName, String getCapDesc,
List<ProviderDispatch> dispatches, int di, Label nextLabel) { List<ProviderDispatch> dispatches, int di, Label nextLabel,
Map<CapabilityRef, Integer> capRefIndices) {
var guarded = (ProviderDispatch.Guarded) dispatches.get(di); var guarded = (ProviderDispatch.Guarded) dispatches.get(di);
// Peek ahead to collect consecutive Guarded entries with same providerIndex // Peek ahead to collect consecutive Guarded entries with same providerIndex
@ -507,7 +623,7 @@ public class CapabilityProviderDispatcherGenerator {
var g = (ProviderDispatch.Guarded) dispatches.get(gi); var g = (ProviderDispatch.Guarded) dispatches.get(gi);
CapabilityRef ref = g.capability(); CapabilityRef ref = g.capability();
mv.visitVarInsn(ALOAD, 1); mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); emitCapabilityLoad(mv, internalName, ref, capRefIndices);
if (gi < groupEnd - 1) { if (gi < groupEnd - 1) {
mv.visitJumpInsn(IF_ACMPEQ, matchLabel); mv.visitJumpInsn(IF_ACMPEQ, matchLabel);
} else { } else {
@ -544,4 +660,4 @@ public class CapabilityProviderDispatcherGenerator {
} }
return result.toString(); return result.toString();
} }
} }