User Tools

Site Tools


drafts:mixin_injectors

Injectors (DRAFT)

Mixin provides many ways to modify classes, but the one modders should prioritize the most is injectors. Injectors are a type of tool which includes @Inject, @WrapOperation, @ModifyArg to only mention a few.
Injectors are characterized by the fact that they do not remove any of the target class's bytecode. This allows for highly compatible modifications that can chain with others directed at the same target. Following the broader principle that Mixin modifications should be as compatible and precise as possible, injectors are therefore the primary tool for adding new operations or modifying existing ones.

Injectors make modifications to methods in the target class's bytecode, this includes synthetic methods like lambdas or areas of a class that are implicitly a method such as the static initializer containing a class's field.

General Structure

In the source code of a Mixin class, an injector consists of two elements: the annotation(s), and the “handler” method, also simply called the handler, decorated by the annotation. The typical structure of an injector's annotation and method will look as follows:

@InjectorAnnotation(method = "<target method name or descriptor>", at = @At(value = "<INJECTION POINT>"))
private ReturnType handlerMethod(<Parameters>) {
 
}

The handler method's return type and parameters are highly dependent on the specific injector, as they vary based on how the handler method is integrated into the target class. Additionally, the handler method may need to be static depending on the target.

Target method

Injectors are applied to methods. Assuming the method does not have an overload, a method of the same name and return type but with different parameters, it can be specified by its name without any additional specification.

Mixin's JavaDocs provides helpful documentation on specifying the target method at the documentation for the MemberInfo class.

Overloads

For overloads, the method's descriptor for its parameters must be appended to the name. This is detailed upon on the relevant Docs page's method and field descriptor section.

MCDev's autocompletion also accounts for overloads, so just autocompleting the annotation's method = attribute with the proper name and parameters should automatically specify it.

Constructor, Static Initializer

Constructor methods are unnamed in source code, but are specified using the name <init>. In the case of multiple constructors, the same as named method overloads applies.

The static initialization code, which includes things such as the initialization of static fields, is specified using the name <clinit>. Injecting into the static initialization code is useful if you need to, for example, inject into the value which a static field is initialized to. However, static blocks in the Mixin class will be merged into the target class aswell, this can be used to reassign static fields without using an injector. This should be prioritized unless you need to specifically modify the initial value of a given field, which cannot be done directly through a static block.

Lambda Methods

Lambda methods are unnamed in source code, and turned into a synthetic method in bytecode where Mixin operates, and as such can be a bit tricky to inject into directly. There are two approaches to injecting into a lambda method's body.

The first, and most solid, approach is to “compose” the lambda if possible. This means using an injector which wraps the lambda's call site (@WrapOperation, @ModifyExpressionValue or @ModifyArg typically), and calling it in your own lambda which also contains your additional operations, and then returning the composed lambda:

@ModifyExpressionValue(...)
private Consumer<ItemStack> exampleComposition(Consumer<ItemStack> original) {
    Consumer<ItemStack> composedConsumer = itemStack -> {
        // New operations here or after the original call
        original.accept(itemStack);
    };
    return composedConsumer;
}

This is very useful for when you need to add operations at the head or tail of a lambda, as it both chains with other injectors, and avoids needing to directly inject into the lambda method. Since you are still mixing into the lambda's enclosing method, you also still have access to all the variables outside of the lambda's scope.

If for whatever reason this is not suitable, you can open the target class's bytecode (IntelliJ, VSCode), and look for the lambda's bytecode by searching for the synthetic keyword. Lambdas in bytecode are either named lambda$enclosingMethod$indexNumber, for example lambda$shear$2 would be the second lambda in the class, declared in the method shear; or they may be named in the intermediary method_XXXXX format.

It's possible to either leverage MCDev's autocompletion by first typing method_ in the method = attribute and pressing CTRL + SPACE and seeing, if the lambdas show up in intermediary, which one has the parameter and return type you're looking for, or doing the same but by starting to type lambda$ in the attribute rather than method_.

If you must however look at the bytecode, it is possible to quickly find synthetic methods by searching for the synthetic keyword. You may search for synthetic method_ to test if they are in intermediary format. You can then look at the lambda method body in source code to identify key elements such as parameters and certain stand-out method calls or constants, and use that to figure out which method's bytecode corresponds to the lambda you wish to mix into.

Once you have specified the lambda's bytecode name successfully, you can now modify it as you would normally. Be aware that lambdas are not passed all of the variables from the outer scope, and only capture their parameters' values.

The @At annotation

Most injectors need to specify where in the target method to inject/modify instructions, this is done through the at attribute, which can take in one or more @At annotations.

Injection Point, the value attribute

The @At annotation takes one necessary value attribute, followed by optional ones: the “injection point”. Injection Points serve as a “base” indicating what type of instruction in the target method to select for. They are referenced via a String, and are tied to an InjectionPoint subclass. Some common injection points include:

String Form in Annotation Function
HEAD Inject at the earliest possible point in the method
RETURN Inject before every RETURN instruction in the target method
TAIL Inject before the very last RETURN instruction in the target method
INVOKE Inject before a method call/invoke instruction
FIELD Inject before a field access instruction
MIXINEXTRAS:EXPRESSION Target via an Expression.

More details on the base Mixin injection points can be found at the Injection Point Reference.

It is very important to note that for injectors which rely on modifying or wrapping their targets (@WrapOperation, @ModifyArg, @ModifyExpressionValue…) rather than injecting new instructions relative to the target (@Inject, @ModifyVariable…) the injection point will not be used to inject “before” the target, but to specify the target to modify/wrap directly.

However, the injection points themselves are at times insufficient, such as with INVOKE, which on its own would target every method call.

Target Discriminators

On top of specifying an injection point it is possible to use and combine different discriminators to filter the potential targets further. Three are consistent across all injectors and injection points: ordinal, slice and injection point specifiers.

Ordinals are used to pick one of the potential candidate targets based on other discriminators similarly as using an array index.

Slicing is used to define a range within which to search for candidates, based on two additional @Ats.

Injection point specifiers can be used to additionally specify the intended targeting for the injection point, taking the form of a suffix appended to the injection point.

Certain injection points also take a target attribute in coordination with the injection point. This can be used, typically, to specify the specific field being accessed with FIELD, or to specify the method being called with INVOKE.

The FIELD injection point uses a special opcode discriminator to specify the specific field interaction.

MIXINEXTRAS:EXPRESSION uses Expressions, and does not support target.

Whilst some discriminators may be exclusive to certain injection points, they are not fundamentally incompatible with one another. It is possible to combine an Expression with a slice, an opcode with an ordinal etc., as long as the injection point supports both of the discriminators.

Some discriminators are however more “brittle” than others, meaning that they are likelier to break between updates, as their approach to selecting the target(s) is susceptible to break from unrelated modifications.

Injection Point Specifiers

Injection points can be supplied an additional specifier from the following list to modify how they behave:

Specifier Name Function
ALL Default when not slicing. Takes in all instructions that match discriminators
FIRST Default when slicing. Takes in the first matching instruction
LAST Takes in the last matching instruction
ONE There must be only one matching instruction, otherwise fail and throw an exception.
DEFAULT Use the default specifier behavior for the consumer

Specifiers FIRST and LAST should be used instead of their ordinal equivalents when they are relevant, and ONE can be used to enforce there being only one matching target when using the Mixin, thus making it impossible to silently target multiple instructions between updates, for example.

The default behavior being FIRST for slices should be kept in mind if you intend to inject into every matching instruction when slicing, which would necessitate using ALL.

Specifiers are compatible with every injection point, including MIXINEXTRAS:EXPRESSION, and are appended as a suffix which separates the injection point from the specifier with a colon :. For instance:

@At(value = "INVOKE:FIRST", target = "...")

Would take the first call to the method specified by target.

Target Attribute

The target attribute is added to the @At to specify an identifying characteristic of the target which sets it apart from other instructions corresponding to the same injection point. For INVOKE, this would be specifying which method is being invoked, and for FIELD this would be which field is being accessed.

The target is specified by using the target's descriptor, based on type descriptors. This can be entirely autocompleted using the MCDev plugin.

The target attribute is specified as with any other annotation attribute:

@At(value = "<INJECTION POINT>", target = "<TARGET REFERENCE>")

This discriminator is not particularly brittle in any particular way, and should mostly always be used on its relevant injection points in combination with other discriminators for the target.

Ordinal

Ordinals allow selecting one instruction returned by other discriminators and the injection point similarly as a zero-indexed array index. This means that an ordinal of 0 corresponds to the first instruction in the available list, whilst an ordinal of 3 would correspond to the fourth. Ordinals are processed after a slice. The ordinal attribute, if unspecified, will not be used, and all instructions in the list will be targeted by the injector.

It is specified as an integer attribute in the @At annotation:

@At(value = "INJECTION POINT", ordinal = <int>)

:!: Ordinals are generally the most brittle discriminator, as it may break from a new matching instruction being added prior to the one selected by the ordinal. A specifier of LAST should generally be used in place of an ordinal of the last ordinal available; in most cases it is recommendable to use an expression and/or a slice instead of an ordinal, as those are much more expressive and less likely to break between updates.

Slice

A slice uses one or two additional @Ats to specify a range to include when searching for instructions matching the main @At. Slicing is specified via the slice attribute, which takes a @Slice annotation (code differently formatted to distinguish between the different @Ats more clearly):

@At(
    value = "INJECTION POINT",
    slice = @Slice(
        from = @At(...)
        to = @At(...)
    )
)    

from defaults to @At(“HEAD”), and to defaults to @At(“TAIL”), you may only specify one's value if needed.

Slicing is a lot less brittle than ordinals assuming the from and to can be specified without being brittle themselves. Similarly to other discriminators, slicing can be used to complement other targeting tools and be combined with them.

Opcode

opcode is a discriminator used for FIELD injection point. The opcode attribute takes an integer value corresponding to the Bytecode opcode. This should be filled using ASM's Opcodes interface's presets, which are named to match the Opcode corresponding to their values:

@At(value = "FIELD", target = "...", opcode = Opcodes.<...>)

Opcodes for FIELD would include GETSTATIC, PUTSTATIC for accessing a static field, and GETFIELD, PUTFIELD for a non-static field. This can be autocompleted by MCDev, which will give a warning to remind you to use an opcode in the event your FIELD injector does not use one already, with a quick-fix to add it.

opcode can be combined with a target and other discriminators allowed by the FIELD injection point. It should generally always be used when using FIELD.

Expressions

MixinExtras 0.5.0 (bundled in Fabric Loader version 0.17.0+) adds a new injection point: MIXINEXTRAS:EXPRESSION, which does not take a target, but instead uses an @Expression annotation to define a Java-like String that will correspond to the instructions to target. This allows, importantly, to define the surrounding context of a target, allowing for very high precision without having to resort to use ordinals or slices. The possibility of adding context rather than targeting preset instructions or groups of instructions also allows targeting much more complex expressions in the target class. It is sometimes the only way to target a specific set of instructions:

@Expression("...")
@InjectorAnnotation(method = "...", at = @At(value = "MIXINEXTRAS:EXPRESSION"))

The concept is much more precisely elaborated upon in its official Wiki pages, which contain many examples for its syntax and use-cases.

Choosing the right Injector

FIXME DRAFT NOTE: This segment is still unfinished.

Knowing the overall structure and usage of injectors in general does not help in knowing which injector is right for your use-case. This section aims to go over the different Mixin and MixinExtras injectors, and where they are best-suited.

Whether an Injector is Needed

It should first be mentioned that there are many situations where using an Injector or even a Mixin is unnecessary. Mixins should be resorted to if the given modding framework does not provide other, more convenient, ways to achieve the specified goal.

Existing Events

Whilst using Mixins can be functionally equivalent to events, since Fabric uses Mixins to back its events already, some developers may prefer going through the Fabric-defined event to avoid needing to create a Mixin class and writing an injector when an event already exists. Events are also ported between versions by the Fabric API's team of maintainers, whilst you will need to maintain your own Mixins manually. On the other hand, there is not necessarily a guarantee that the events you use will be uniformly ported through different MC versions.

Mixins are however a fully valid option if the existing events either do not fill your use-case or dissatisfy you in any way. If you find that a useful event does not exist, you may create the Mixin implementation for it and contribute it into Fabric API yourself.

Attaching Data

Mixins are able to merge new members into target classes and modify existing methods related to data management, which can theoretically be used to effectively add data to existing classes for your own use-cases. This, however, is not advised as it takes a great amount of work, and Fabric already provides a Data Attachment API for this purpose, along with other means to save data persistently. Using Mixins to do it manually will often result in unnecessary maintenance and a higher likelihood for bugs.

Modifying Vs Adding Operations or Values

Adding

The first question to ask yourself when trying to choose the correct injector for a given situation is whether you wish to modify existing values and operations, or whether you want to add new operations altogether. @Inject for example is only ever rarely suitable for other purposes than adding new operations, as it is limited to adding a call to your injector's handler method, and possibly injecting an early return. This is not suited for modifying values, method calls, etc.

There are however contexts where @Inject may also not be the injector to choose for adding new operations, such as when needing to get more context relative to the operation you are injecting next to. It can be wise to use @WrapOperation or @ModifyExpressionValue to be able to capture one or more values relevant to an operation, and then adding one's own operations before or after it. Those injectors are also more relevant for wrapping the operation itself with something, such as a try/catch.

Modifying

In order to modify existing operations and values, the three main tools are @WrapOperation, which is ideal for modifying things such as comparisons, method calls, and a variety of other operations, whilst also giving context, @ModifyExpressionValue, which is exceptionally flexible in the amount of expressions and operations it can target, as long as the given operation results in a value; and @ModifyArg, which is preferable for modifying method arguments, comparatively to using a @ModifyExpressionValue on a given method's passed argument.

Further Reading

Injector-Specific pages

The following are pages currently available to give information about a specific injector:


To the Attention of Draft Reviewers and Contributors

This page is made with the purpose of giving general, shared, information between injectors, aswell as giving a cursory guidance as to which injectors may be fitting for general situations.

Goals

  • Give Wiki readers a more informed and thorough guidance on injector syntax and purpose
  • Give cursory guidance on what certain injectors are more useful for, allowing for a more informed approach to selecting an injector
  • Overall reduce the need for beginners to ask repeatedly what injectors are, what injectors are for, and how to use them

Non-Goals

  • Give in-detail description of each injector, which should be done on injector-specific pages
  • Replace asking for help in support channels
  • Replace existing well-maintained documentation on injectors, mainly the MixinExtras Wiki's injector and Expressions pages

Things to do before getting out of drafting

  • Get proper reviewing on the page as a whole, a review by LlamaLad7 would be ideal
  • Possibly extract target discriminators segment into its own page draft
  • Finish work on the injector selection guidance
  • Reassess the example code blocks' syntax and whether they may be improved

Review feedback to likely implement

  • Expanding upon when not to use a Mixin, particularly in the data attachment section, for which looking into cases where Fabric doesn't provide a helpful API to note would be helpful.
  • Add injection point specifiers to target_discriminators segment

Plans after getting out of Drafting

  • Keep a close eye on the page for the month or so after first pushing it
  • Work on more injector-specific pages, with non MixinExtras injectors as a priority
  • Cull general injector information from mixin_injects which should ideally now be here.

Thank you kindly for reviewing this draft.

drafts/mixin_injectors.txt · Last modified: 2026/01/07 20:56 by gauntrecluse