From 2081b63b56857f4d5107cd8d92e812b547b1d7b3 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:38:18 -0400 Subject: [PATCH] Fix looking up `private static final` Capability fields --- ...CapabilityProviderDispatcherGenerator.java | 152 +++++++++++++++--- 1 file changed, 134 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index 72a10cad..6191a2d7 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -1,5 +1,6 @@ package org.embeddedt.modernfix.forge.capability; +import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; import net.minecraftforge.common.util.LazyOptional; import org.embeddedt.modernfix.ModernFix; @@ -10,6 +11,7 @@ import org.objectweb.asm.*; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; +import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.invoke.MethodHandle; 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 MAP_DESC = "Ljava/util/Map;"; private static final String MAP_SIGNATURE = "Ljava/util/Map;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. @@ -117,6 +120,14 @@ public class CapabilityProviderDispatcherGenerator { int generatedClassId = classCounter.incrementAndGet(); String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId; + List 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 + // only needs simple classDataAt calls - no reflection bytecode needed. + LinkedHashMap capRefIndices = collectCapabilityRefs(dispatches); + List> capValues = resolveCapabilityValues(capRefIndices); + ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> { StringBuilder sb = new StringBuilder(); for (int i = 0; i < providerTypes.size(); i++) { @@ -126,11 +137,14 @@ public class CapabilityProviderDispatcherGenerator { return sb; }); - byte[] classBytes = generateClassBytes(className, providerTypes, analysisResults); + byte[] classBytes = generateClassBytes(className, providerTypes, dispatches, capRefIndices); - // Define the hidden class - MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass( + // Define the hidden class, injecting the resolved Capability instances as class data. + // The generated 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, + capValues, true, 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 collectCapabilityRefs(List dispatches) { + LinkedHashMap 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> resolveCapabilityValues(LinkedHashMap capRefIndices) { + @SuppressWarnings("unchecked") + Capability[] caps = new Capability[capRefIndices.size()]; + for (Map.Entry 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. */ @@ -261,9 +316,8 @@ public class CapabilityProviderDispatcherGenerator { return fields; } - private static byte[] generateClassBytes(String className, List> providerTypes, List analysisResults) { - List dispatches = optimizeDispatches(buildDispatchList(providerTypes, analysisResults)); - + private static byte[] generateClassBytes(String className, List> providerTypes, + List dispatches, LinkedHashMap capRefIndices) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { @Override protected ClassLoader getClassLoader() { @@ -271,11 +325,13 @@ public class CapabilityProviderDispatcherGenerator { } }; + String internalName = className.replace('.', '/'); + // Class declaration: implements ICapabilityProvider cw.visit( V17, ACC_PUBLIC | ACC_FINAL | ACC_SUPER, - className.replace('.', '/'), + internalName, null, "java/lang/Object", 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 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 entry : capRefIndices.entrySet()) { + cw.visitField(ACC_PRIVATE | ACC_STATIC | ACC_FINAL, + capRefFieldName(entry.getValue()), CAPABILITY_DESC, null, null).visitEnd(); + } + + // Generate to load capability instances from class data + if (!capRefIndices.isEmpty()) { + generateClinit(cw, internalName, capRefIndices); + } + // Generate constructor - generateConstructor(cw, className, providerFields, dispatches); + generateConstructor(cw, className, providerFields, dispatches, capRefIndices); // Generate getCapability method with sided parameter - generateGetCapabilityMethod(cw, className, dispatches); + generateGetCapabilityMethod(cw, className, dispatches, capRefIndices); cw.visitEnd(); return cw.toByteArray(); } - private static void generateConstructor(ClassWriter cw, String className, Map providerFields, List dispatches) { + private static String capRefFieldName(int index) { + return "capRef" + index; + } + + /** + * Generates {@code } 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 capRefIndices) { + MethodVisitor mv = cw.visitMethod(ACC_STATIC, "", "()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 capRefIndices) { + mv.visitFieldInsn(GETSTATIC, internalName, capRefFieldName(capRefIndices.get(ref)), CAPABILITY_DESC); + } + + private static void generateConstructor(ClassWriter cw, String className, Map providerFields, + List dispatches, Map capRefIndices) { Method constructor = Method.getMethod("void (net.minecraftforge.common.capabilities.ICapabilityProvider[])"); GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw); Type classType = Type.getObjectType(className.replace('.', '/')); @@ -338,7 +451,7 @@ public class CapabilityProviderDispatcherGenerator { // Build hash maps for (ProviderDispatch dispatch : dispatches) { if (dispatch instanceof ProviderDispatch.Hash hash) { - generateMapConstruction(mg, classType, hash); + generateMapConstruction(mg, classType, hash, capRefIndices); } } @@ -346,7 +459,8 @@ public class CapabilityProviderDispatcherGenerator { 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 capRefIndices) { List entries = hash.entries(); mg.loadThis(); // for PUTFIELD at the end @@ -356,7 +470,7 @@ public class CapabilityProviderDispatcherGenerator { ProviderDispatch.Guarded g = entries.get(i); mg.dup(); 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.push(g.providerIndex()); mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); @@ -370,7 +484,8 @@ public class CapabilityProviderDispatcherGenerator { mg.putField(classType, "capMap" + hash.mapIndex(), Type.getType(MAP_DESC)); } - private static void generateGetCapabilityMethod(ClassWriter cw, String className, List dispatches) { + private static void generateGetCapabilityMethod(ClassWriter cw, String className, List dispatches, + Map capRefIndices) { // Method: LazyOptional getCapability(Capability, Direction) MethodVisitor mv = cw.visitMethod( ACC_PUBLIC, @@ -401,7 +516,7 @@ public class CapabilityProviderDispatcherGenerator { emitHashDispatch(mv, internalName, getCapDesc, hash, nextLabel); di++; } else if (dispatch instanceof ProviderDispatch.Guarded) { - di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel); + di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel, capRefIndices); } else { var u = (ProviderDispatch.Unguarded) dispatch; emitProviderGetCapability(mv, internalName, getCapDesc, u.providerIndex(), u.fieldDesc()); @@ -490,7 +605,8 @@ public class CapabilityProviderDispatcherGenerator { * @return the updated dispatch index (past the consumed group) */ private static int emitGuardedDispatch(MethodVisitor mv, String internalName, String getCapDesc, - List dispatches, int di, Label nextLabel) { + List dispatches, int di, Label nextLabel, + Map capRefIndices) { var guarded = (ProviderDispatch.Guarded) dispatches.get(di); // Peek ahead to collect consecutive Guarded entries with same providerIndex @@ -507,7 +623,7 @@ public class CapabilityProviderDispatcherGenerator { var g = (ProviderDispatch.Guarded) dispatches.get(gi); CapabilityRef ref = g.capability(); mv.visitVarInsn(ALOAD, 1); - mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); + emitCapabilityLoad(mv, internalName, ref, capRefIndices); if (gi < groupEnd - 1) { mv.visitJumpInsn(IF_ACMPEQ, matchLabel); } else { @@ -544,4 +660,4 @@ public class CapabilityProviderDispatcherGenerator { } return result.toString(); } -} \ No newline at end of file +}