Access Transformers

Revision as of 03:06, 13 August 2021 by SciWhiz12 (talk | contribs) (whoops, switched the two wildcards)

Access transformers (abbreviated as ATs) are declarations for modifying or transforming the access visibility or finality of a class member (classes, fields, methods). These declarations are parsed by a transformer and applied to classes using the ASM library, either modifying the class files on-disk or modifying the class bytes during runtime.

The AccessTransformers library provides a pluggable ModLauncher transformation service and a command-line application for applying access transformers on class bytes during runtime and on class files on-disk.

Background

Java provides the mechanism of access level modifiers (or access modifiers), to modify the visibility of a member with the modifier to other classes. This is commonly used to hide fields and methods from other classes, such as instance fields for encapsulation or private methods which are part of the implementation of the class and should not be exposed to callers.

A member can be declared with any of the modifier keywords public, private, protected to set the access level of the member, or none of the keywords, which defaults to a package-private access level.

The following table shows the different access levels and their modifiers, and what classes may access members with that access level:

Access levels
Access modifier Within the same class Subclasses Classes in the same package All other classes
public Yes Yes Yes Yes
protected Yes Yes Yes No
no modifier Yes Yes No No
private Yes No No No

For example, a field marked protected cannot be accessed by a class in another package, but can be accessed by: any class within the same package, any subclass of the class (even if the subclass belongs to another package), and within the same class where the field is declared.

Additionally to the access modifiers, there exists the final keyword, which:

  • on classes, prevents any subclassing of the class;
  • on fields, marks it as being assignable only once during construction; and
  • on methods, prevents any subclasses from overriding the method with their own implementation.

Normally, Java programmers restrict access levels to be as restrictive as they need to be, controlling what is the effective publicly-available surface for the application. For example, a field may be declared as private while providing setter and getter methods to access the field (a technique known as encapsulation).

However, this convention of restricting access levels and marking members as final where approriate presents a problem with modding Minecraft, because they restrict what parts of the game that modders can access or modify, usually without other means to access or modify them (such as setters and getters for fields).

One solution is to use Java reflection to reflectively access these fields and methods during runtime, but they suffer from some problems:

  • Reflection is slower than regular method calls or field accesses, as certain optimizations done by the JVM cannot be applied.[1] This makes reflection unsuitable for performance-critical code, such as during rendering.
  • Reflection cannot allow the modification of final fields which are either marked static or belong to a record class.[2]
  • Runtime reflection loses the benefit of compile-time type checks, which allows dangerous operations such as trying to set a field's value to an incompatible type.
  • The use of reflection does not allow a class to extend a non-visible class.

These problems are solved with the use of access transformers, which allow modification of the access modifiers of members such that they can be applied on class files on-disk to allow compiling against them, and applying the same transformer declarations on class bytes during runtime to allow classes which compiled against those transformed classes to work correctly.

Usage

To use access transformers in the development environment, ForgeGradle must be configured with the location of the access transformer configuration file (or files). Because FML is hardwired to look for the configuration file at META-INF/accesstransformer.cfg within JAR files, it is recommended that the AT configuration file for the project be placed at the same location: within the resources folder of the main source set, or src/main/resources/META-INF/accesstransformer.cfg.

To configure the location of the access transformer configuration file with ForgeGradle:

minecraft { // Within the minecraft block
    // Configures FG to look at the given file path for the AT configuration file
    accessTransformer = file("src/main/resources/META-INF/accesstransformer.cfg")
}
Error creating thumbnail: Unable to save thumbnail to destination

The Forge-provided mod development environment's buildscript contains the above line but commented out, as the MDK does not ship with an access transformer file. Users of the MDK can therefore uncomment out the above line and create the AT configuration file at the same location.

Once the configuration file(s) has been added and everytime the contents or location of the file(s) changes, the developer should then refresh the Gradle nature/project in their IDE of choice for ForgeGradle to apply the access transformers to the compiled source. Each change to the access transformer configuration will trigger a full decompilation of the game.

Declarations

Access transformers are declared line-by-line in configuration files, which are then parsed by ForgeGradle in the development environment and by FML in the production or end-user environment.

The # character and any content after that up to the end of the line is considered a comment, and will be ignored by the parser. Any lines which are either empty or consist of only whitespace or comments are ignored.

An access transformer declaration has three different variations, depending on the target to be transformed:

  • for classes: <modifier> <class name>
  • for methods: <modifier> <class name> <method name>([parameter descriptors])<return descriptor>
  • for fields: <modifier> <class name> <field name>

The modifier consists of one of the following words:

  • public for the public access level (denoted by the modifier keyword of the same name)
  • protected for the protected access level (denoted by the modifier keyword of the same name)
  • default for the package-private access level (denoted by the absence of an access modifier keyword)
  • private for the private access level (denoted by the modifier keyword of the same name)
  • For any of the preceding words, the -f suffix removes the final modifier, while the +f suffix adds the modifier.

The class name is the fully qualified name of the class, using . (dots) to separate between (sub)packages and classes (for example, java.lang.Object).

For methods, the method descriptor[3] of the method is included, which consists of the descriptors for each of the method's parameters and the method's return type. The method parameters descriptors can be absent if there are no parameters in the method, but the return type descriptor must be present.

The following are the matching descriptors for each type as seen in the method declaration:

Matching type descriptors
Descriptor Source form Description
Z boolean a true or false value
B byte a 8-bit signed number
S short a 16-bit signed number
C char a Unicode character code point in UTF-16
I int a 32-bit signed number
F float a single-precision floating-point value
J long a 64-bit signed number
D double a double-precision floating-point value
[I int[] one dimension of an array; this can be repeated, and must end with another descriptor other than an array
Ljava/lang/Object; java.lang.Object a reference type; the internal form of the class' binary name must be present (in the given example, the class referenced is java.lang.Object.

Note that inner classes are separated using the $ (dollar sign) character, such as java/lang/System$Logger.

V void a void or no-value return type; only available for the return type of a method descriptor

Wildcard access transformers

A special type of access transformers called wildcard ATs (oftentimes referred to as shotgun ATs allows transforming the access modifier and finality of all fields or methods within a class.

To use wildcard access transformers:

  • for all methods within a class: <modifier> <class name> *()
  • for all fields within a class: <modifier> <class name> *
Modders should avoid using this in live code. Access transformers should always have the fewest and narrowest targets possible, and wildcard ATs violate this convention. Use of wildcard ATs may cause problems with both the ForgeGradle setup process. Wildcard ATs may be removed in a future update of the access transformer specification.

Examples

The following are some examples of access transformer declarations:

# Makes public the ScreenConstructor class in MenuScreens
public net.minecraft.client.gui.screens.MenuScreens$ScreenConstructor

# Makes protected the 'COLOR_WHITE' field in Gui
protected net.minecraft.client.gui.Gui f_168667_ # COLOR_WHITE

# Makes protected and removes the final modifier from 'random' field in MinecraftServer
protected-f net.minecraft.server.MinecraftServer f_129758_ # random

# Makes public the 'makeExecutor' method in Util,
# which accepts a String and returns an ExecutorService
public net.minecraft.Util m_137477_(Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; # makeExecutor

# Makes public the 'leastMostToIntArray' method in SerializableUUID,
# which accepts two longs and returns an int[]
public net.minecraft.core.SerializableUUID m_123274_(JJ)[I # leastMostToIntArray
  1. The Java™ Tutorials: The Reflection API: "Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications."
  2. Java 16 javadocs for java.lang.reflect.AccessibleObject.setAccessible(boolean)
  3. Java Virtual Machine Specification, Java SE 16 Edition, 4.3.3. Method Descriptors