Skip to content

Instantly share code, notes, and snippets.

@lukebemish
Last active January 2, 2024 20:09
Show Gist options
  • Save lukebemish/f40456f16396e417b592ac47eb3363e9 to your computer and use it in GitHub Desktop.
Save lukebemish/f40456f16396e417b592ac47eb3363e9 to your computer and use it in GitHub Desktop.
Accessor/Invoke mixins vs AWs/ATs - Which to pick and which is safer?

Accessor/Invoke mixins vs AWs/ATs - Which to pick?

You have realized, for some reason or another, that you need access to something in the vanilla code base that has a big old "private", "final", "protected", or simply conspicuous lack of "public" on it. This generally speaking isn't great, but modding frameworks (Fabric/Quilt and [Neo]Forge) provide several ways to get at private values - there are a special sort of mixin, known as "accessors", which are generally speaking safer than normal mixins and can "access" otherwise private stuff (plus doing a few more things... I'll get to that), and then there are access wideners (fabric/quilt) and access transformers ([neo]forge), which are basically a big file you list things you want transformed in; forgegradle or loom them transforms the things the way you want in your development environment and at runtime. This short gist is written as a primer on the different options, the differences between them, which you should use when, and how to be as certain as you can that they will not cause conflicts with other mods when you use them.

How do they work?

I'm not going to go into too much detail here, as there's better sources elsewhere, but I'll go into some specific details that can be helpful if your goal is to not have incompatabilities with other mods. Access wideners are documented fairly well on the fabric wiki, and access transformers at the forge community wiki, so I will not go into too much detail here. One major limitation to note with forge access transformers - or ATs - is that using them will result in a project-specific version of the minecraft jar being used and depended on, instead of using one shared with anywhere else that uses the same forge version, as happens normally. This can marginally increase disk usage space if that's something you're worried about.

The primary drawback of either AWs or ATs is that you have to reload your gradle project to apply them - they're not exactly great for "rapid prototyping". Accessor mixins offer a much faster iteration, at the cost of slightly uglier code and a few limitations. (They also have some other advantages but I'll mention that later!). Accessor mixins are fundamentally just normal old mixins, that happen to be interfaces. Let's imagine that there is a vanilla class that looks like this:

public class VanillaClass {
    private static ResourceKey<Block> BLOCK_KEY;
    
    ...

    protected final ResourceLocation location;

    ...

    private void someMethod(String string) {
        ...
    }

    ...
}

And that I would like to be able to read BLOCK_KEY, mutate the normally final location, and call someMethod. An accessor mixin to do so might look like this:

@Mixin(VanillaClass.class)
public interface VanillaClassAccessor {
    @Accessor(value = "BLOCK_KEY")
    static ResourceKey<Block> mymodid_getBlockKey() {
        return null;
    }

    @Accessor(value = "location")
    @Mutable
    void mymodid_setLocation(ResourceLocation location);

    @Invoker(value = "someMethod")
    void mymodid_someMethod(String string);
}

To actually use the exposed methods, you'd just case your VanillaClass to VanillaClassAccessor and use the methods there.

If you've seen an accessor mixin before, that might look a bit different! That's because I'm being extra cautious here - I've prefixed every method with my mod ID, mymodid, and am providing the method/field name manually. You can leave out the value argument and name the methods, say, setLocation or invokeSomeMethod or the like and it'll work just fine - unless some other mod happens to have an accessor that adds a method of the same name that does different stuff! Is this likely to happen? No, almost certainly not, in fact. Chances are, any two things that add a method of that name are both just normal old accessors. But if you're being extra catious, and are still worried about that small risk - this is what you would do. Manually specifying the value can also be useful to make an invoker for a constructor, by setting it to <init>.

Now, how safe are accessor mixins exactly? Very! About the worst things you can anticipate happening is mutating a normally immutable variable and having some other mod not like that, and though that is a legitimate risk if you use @Mutable it's also the riskiest part of the whole endeavour. The edge case I noted earlier about method name conflicts is not something I have actually seen come up in practice but theoretically it is possible too if you do not prefix your methods. Other than that, though, accessors and invokers are one of the safest sorts of mixin you can write.

Comparisons

Some broad, sweeping comparisons between ATs/AWs and accessor mixins in terms of ability, convenience, and safety:

  • Accessors/Invokers require casting to your accessor mixin interface; ATs/AWs require gradle reloads between changes
  • Accessor mixins can be slightly safer than ATs/AWs in certain cases, especially when making private methods accessible
  • Accessor mixins cannot do some things that ATs/AWs can - such as removing final from methods or classes, or making private classes accessible

A general note on safety

If done right, accessor mixins, ATs, and AWs are extremely safe in many circumstances. The places where issues can arise generally take one of several forms:

  • conflicts between multiple Access Transformers: ATs can do stuff like making non-final things final; obviously this can cause issues
  • mutation of final fields or extension of final methods/classes: many mods see that something in a vanilla class is final, and then make assumptions based on that. If you violate those assumptions by making it not final, you may cause issues with those mods
  • subtle runtime conflicts through ATs/AWs: this is a whole bucket of potential issues that, luckily, are extremely rare - I've only ever seen them in "proof of concept" mods made to show that they could happen, but these issues come from the way the Java Virtual Machine invokes methods.

Which should I use?

Though in most cases the two are interchangable, there are certain cases where the use of a particular approach is safer or more convenient than the other. In other cases, it comes down to personal preference - some people appreciate how non-invasive mixins are in this regard (they avoid changing the visibility of base methods or fields, instead simply exposing an interface which can change them), while others prefer the fact that changes made by ATs/AWs show up in code directly.

Accessing private classes in order to pass around instances, extending final classes, or overriding final methods

Your only option in these cases is to use an AT/AW, as these require changes to the code you are compiling against. If you are making a method or class extensible that is normally final, make sure to think about other alternative approaches first, as making things extensible can lead to issues (especially making classes extensible, as people may assume that they can safely case a vanilla class to a subclass which they can not).

Changing instance method visability

I would highly reccommend using an invoker in these cases instead of an AT or AW. Though the conditions needed for issues to arise are truly an edge case, they are certainly possible - and substantially more likely as NeoForge switches to using official Mojang mappings at runtime. Ony potential issue takes the form of a mod without any ATs or AWs containing a method, unrelated to vanilla code, that happens to share a name and signature with a private method in a vanilla class that is, any distance back, a superclass. Normally, this subclass method does not override the superclass method - as private methods can't be overridden. However, if the superclass method is made public or protected, the subclass method would now override it - which would lead to the subclass method being called whenever the superclass private method is invoked, leading to anything from unexpected behaviour to crashes due to mismatched generic parameters, which aren't counted in the signature.

The liklihood of this occuring is vanishingly rare - after all, you'd need a name overlap between a method in some modded class and a private method in a superclass, which seems nearly impossible with the mangled intermediary names used on fabric and still unlikely with the human readable MojMaps names NeoForge is switching to - but the number of mods out there is vast, and this particular issue can either leave no stack trace at all or a stacktrace that makes it very hard to track down the cause, so if you'd like to be extra careful then using an invoker is the best approach.

Mutate final fields or access restricted fields, constructors, and static methods

With the same warnings in mind about changing final-ness as before, this is really up to you - it can be done with either ATs/AWs or accessors with no particular disadvantages to either other than those inherent to the techiniques.

Other approaches

Though I have focused on accessors and ATs/AWs, these are certainly not the only ways to get access to otherwise hidden fields. Tools such as the reflection API or MethodHandles system can be used as well, though these may be slightly slower (especially if you forget to stick your field/method references in static final fields!) and you will have to take remapping into account. They have their own tradeoffs compared to either accessors or ATs/AWs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment