Mixin
Mixin copied to clipboard
Generic '&' operator / fabric
Hello,
i'm developing a fabric mod using the sponge mixins where i need to to invoke a private method from within a mixin. I created an interface with the method signatures i want to call from the target class. It look like the following:
@Mixin(Screen.class)
public interface IScreen {
@Invoker(remap = true)
<T extends Drawable> T callAddDrawable(T drawable);
@Invoker(remap = true)
<T extends Element & Selectable> T callAddSelectableChild(T child);
}
Now in my implementing (abstract) screen mixin, i can call the "callAddDrawable" method and it works great. However, calling the "callAddSelectableChild" the same way does ONLY work in dev (gradle runClient) and not if i build the mod and run it with the fabric loader. I get the following exception:
Mixin apply failed mymod.mixins.json:IScreen -> net.minecraft.class_437: org.spongepowered.asm.mixin.gen.throwables.InvalidAccessorException No candidates were found matching addSelectableChild(Lnet/minecraft/class_364;)Lnet/minecraft/class_364; in net/minecraft/class_437 for mymod.mixins.json:IScreen->@Invoker[METHOD_PROXY]::invokeAddSelectableChild(Lnet/minecraft/class_364;)Lnet/minecraft/class_364; [INJECT Applicator Phase -> mymod.mixins.json:IScreen -> Apply Accessors -> -> Locate -> mymod.mixins.json:IScreen->@Invoker[METHOD_PROXY]::invokeAddSelectableChild(Lnet/minecraft/class_364;)Lnet/minecraft/class_364;]
org.spongepowered.asm.mixin.gen.throwables.InvalidAccessorException: No candidates were found matching addSelectableChild(Lnet/minecraft/class_364;)Lnet/minecraft/class_364; in net/minecraft/class_437 for mymod.mixins.json:IScreen->@Invoker[METHOD_PROXY]::invokeAddSelectableChild(Lnet/minecraft/class_364;)Lnet/minecraft/class_364; [INJECT Applicator Phase -> mymod.mixins.json:IScreen -> Apply Accessors -> -> Locate -> mymod.mixins.json:IScreen->@Invoker[METHOD_PROXY]::invokeAddSelectableChild(Lnet/minecraft/class_364;)Lnet/minecraft/class_364;]
at org.spongepowered.asm.mixin.gen.InvokerInfo.findTargetMethod(InvokerInfo.java:117)
at org.spongepowered.asm.mixin.gen.InvokerInfo.locate(InvokerInfo.java:101)
at org.spongepowered.asm.mixin.transformer.MixinTargetContext.generateAccessors(MixinTargetContext.java:1318)
at org.spongepowered.asm.mixin.transformer.MixinApplicatorStandard.applyAccessors(MixinApplicatorStandard.java:1051)
at org.spongepowered.asm.mixin.transformer.MixinApplicatorStandard.applyMixin(MixinApplicatorStandard.java:393)
at org.spongepowered.asm.mixin.transformer.MixinApplicatorStandard.apply(MixinApplicatorStandard.java:320)
at org.spongepowered.asm.mixin.transformer.TargetClassContext.applyMixins(TargetClassContext.java:345)
at org.spongepowered.asm.mixin.transformer.MixinProcessor.applyMixins(MixinProcessor.java:569)
at org.spongepowered.asm.mixin.transformer.MixinProcessor.applyMixins(MixinProcessor.java:351)
at org.spongepowered.asm.mixin.transformer.MixinTransformer.transformClass(MixinTransformer.java:208)
at org.spongepowered.asm.mixin.transformer.MixinTransformer.transformClassBytes(MixinTransformer.java:178)
at org.spongepowered.asm.mixin.transformer.FabricMixinTransformerProxy.transformClassBytes(FabricMixinTransformerProxy.java:23)
at net.fabricmc.loader.launch.knot.KnotClassDelegate.getPostMixinClassByteArray(KnotClassDelegate.java:157)
at net.fabricmc.loader.launch.knot.KnotClassLoader.loadClass(KnotClassLoader.java:150)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:519)
at net.optifine.reflect.Reflector.<clinit>(Reflector.java:185)
at net.minecraft.class_128.<init>(class_128.java:44)
at net.minecraft.class_128.method_24305(class_128.java:349)
at net.minecraft.client.main.Main.main(Main.java:137)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at net.fabricmc.loader.game.MinecraftGameProvider.launch(MinecraftGameProvider.java:226)
at net.fabricmc.loader.launch.knot.Knot.launch(Knot.java:146)
at net.fabricmc.loader.launch.knot.KnotClient.main(KnotClient.java:28)
The other one (callAddDrawable) still works with fabric for some reason. I suspect the problem with the '&' operator of the generic T type of the function.
Enrivonment:
- Java 16 AdoptOpenJDK
- Minecraft 1.17
I don't know if this is a bug with fabric, sponge or my fault and need your help. Any help is greatly appreciated, thanks.
Me still on Java 8 be like:
"...what type of black magic that & operator thing is?.."
Intersection types are in Java 8...
Intersection types are in Java 8...
Today I learned. To be honest I just never saw this & operator thing in any codebases based on Java 8, which I work with a lot of, so I assumed it must be something from those newer versions of Java which I never touched upon.
@Aizistral this isnt' a forum, how is your comment a useful addition to this ticket? Please keep your thoughts on discord where they belong.
I don't know if this is a bug with fabric, sponge or my fault and need your help.
It might not be a bug at all, it could just be an artefact of how remapping is handled and how types declared in generics actually translate into bytecode. A further consideration is how generics are presented in Mirror (the compile-time equivalent of Reflection used by the AP) and the limitations imposed by that. It could also be a bug in the AP's resolution of the generic upper bound.
Method and field descriptors aren't generic thanks to the fact that generics aren't reified in Java and therefore the "real" descriptor that actually ends up in the bytecode can be (and in a lot of cases has to be) different, usually this will be the most covariant type that can satisfy the type arguments, but can sometimes not be the "true" upper bound because scope allows resolution to a more derived type.
In mixins, we often get around the fact that the type erasure happens under the hood by virtue of the fact that the mixin and the target class just happen to resolve the bounds for the generics in the same way, this is particularly true for anything with obviously resolvable bounds like <T extends Comparable<String>>
which is pretty much always going to resolve to the concrete raw type Comparable
in the bytecode for example. This is the only thing that matters at runtime, at compile time the bounds of the specified type arguments are checked.
For intersections, the upper is resolved in much the same way (so in your example the method callAddSelectableChild
will most likely have an upper bound of Element
, but will have a lower bound of Element, Selectable
, again this only matters for compilation, so as long as your consuming code is happy then everything is still fine.
When reobfuscating, in general only the upper bound matters, since that will most likely be the "real" type used in the method descriptor and therefore the type in the mappings. The Mixin AP resolves this upper bound where necessary, following the rabbit-hole where necessary.
All of which is to say that generics in Java suck, and just like with injectors there's a need to be pragmatic vs. idealistic when constructing mixins, since ultimately they're a DSL for weaving into the bytecode, so it's the bytecode that matters. Possibilities:
- Mixin AP is resolving the wrong upper bound for the method, and is for example resolving
Selectable
as the upper bound instead ofElement
. You can determine this by finding out whatclass_364
is. - Mixin AP is resolving the correct upper bound based on the method signature, but your method signature is wrong because it's looking at decompiled code which doesn't correctly represent the bytecode, and therefore the journey from bytecode -> sourcecode -> mixin -> bytecode is losing something in translation, causing the upper bound to be resolved to the wrong class
- The upper bound is being resolved correctly but the mappings being used are wrong/corrupt
I would only consider the first situation a bug, since it represents a deviation of mixin's resolution from the resolution of the compiler, which we're trying to replicate.
Things you can do:
- Don't bother replicating the generic at all: Mixin doesn't care, your consuming code won't care, and the runtime won't care. Just remove the generic type argument and use the upper bound as it appears in the bytecode. This will also rule out (3) as a possible cause.
- Change the generic bound to either re-order the type arguments or remove one of the type arguments so that the declaration is still "generic" but resolves to the correct type.
Personally I'd go with the first, since as I said the goal of mixin is to replicate what's in the bytecode, rather than creating some utopia where copy/paste always works.
If you still have issues, then it's time to look at both the upper bound being resolved by the AP, and whether the mappings in use contain an entry for that method with the upper bound in question.
Also please don't put "tags" such as [HELP] in the issue title.
I believe this should be re-labelled as a bug following our conversation in the Discord a little while ago.
TypeUtils.getUpperBound0
needs some kind of handling of Type.IntersectionClassType
types.
Assuming the type in bytecode is the same as type.getBounds().get(0)
would be one approach.