Difference between revisions of "Access Transformers"

From Forge Community Wiki
m (parameters, man.)
(Rewrite page to be more 'wiki' in tone and more descriptive in nature, remove under construction header)
Line 1: Line 1:
{{Under construction}}
+
'''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.
  
Access Transformers are Forge's native way of allowing you to access (read and write) functions that have a lower-than-ideal visibility, and/or removing finality.
+
The [https://github.com/MinecraftForge/AccessTransformers 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.
  
== Visibility ==
+
== 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 [[:wikipedia:encapsulation|encapsulation]] or private methods which are part of the implementation of the class and should not be exposed to callers.
  
Here's a quick rundown of the visibility options offered by Java 16:
+
A member can be declared with any of the modifier keywords <code>public</code>, <code>private</code>, <code>protected</code> to set the access level of the member, or none of the keywords, which defaults to a ''package-private'' access level.
  
* <code>public</code> visible to all classes inside and outside its package
+
The following table shows the different access levels and their modifiers, and what classes may access members with that access level:
* <code>protected</code> visible only to classes inside the package and subclasses
+
{| class="wikitable" style="text-align:center;"
* <code>default</code> visible only to classes inside the package
+
|+ Access levels
* <code>private</code> visible only to inside the class
+
! Access modifier
 +
! Within the same class
 +
! Subclasses
 +
! Classes in the same package
 +
! All other classes
 +
|-
 +
| <code>public</code>
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:limegreen" | '''Yes'''
 +
|-
 +
| <code>protected</code>
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:indianred" | '''No'''
 +
|-
 +
| ''no modifier''
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:indianred" | '''No'''
 +
| style="color:indianred" | '''No'''
 +
|-
 +
| <code>private</code>
 +
| style="color:limegreen" | '''Yes'''
 +
| style="color:indianred" | '''No'''
 +
| style="color:indianred" | '''No'''
 +
| style="color:indianred" | '''No'''
 +
|}
  
Additionally, there are final variants of each of these, each represented by the phrase "-f" on the end of the access specifier.
+
For example, a field marked <code>protected</code> 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.
  
== How It Works ==
+
Additionally to the access modifiers, there exists the <code>final</code> 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.
  
As Forge is loading, it scans the jar file for the META-INF/accesstransformers.cfg file. if it's found, it is parsed according to the specification:
+
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 <code>private</code> while providing setter and getter methods to access the field (a technique known as encapsulation).
  
<code ->NewAccessSpecifier ClassSignature MemberSignature # comment</code>
+
However, this convention of restricting access levels and marking members as <code>final</code> 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).
  
If you're targetting a class itself, you may omit the Member Signature.<br>
+
One solution is to use Java [[:wikipedia:reflection|reflection]] to reflectively access these fields and methods during runtime, but they suffer from some problems:
However, it is required if you're targetting a method or field inside the class.<br>
+
* Reflection is slower than regular method calls or field accesses, as certain optimizations done by the JVM cannot be applied.<ref>[https://docs.oracle.com/javase/tutorial/reflect/index.html 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."''</ref> This makes reflection unsuitable for performance-critical code, such as during rendering.
<code>MemberSignature</code>s use the Java specification for parts.<br>
+
* Reflection cannot allow the modification of <code>final</code> fields which are either marked <code>static</code> or belong to a record class.<ref>[https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/reflect/AccessibleObject.html#setAccessible(boolean) Java 16 javadocs] for <code>java.lang.reflect.AccessibleObject.setAccessible(boolean)</code></ref>
Importantly, method descriptors are <code>name(parameters)return</code>, classes in the parameters are in the form <code>Lpath/to/Class;</code>,and <code>V</code> is void.<br>
+
* 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.
  
A couple random examples to understand the syntax:
+
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.
  
<code>
+
== Usage ==
public net.minecraft.world.level.chunk.PaletteResize # makes the PaletteResize class public<br>
 
protected net.minecraft.client.gui.Gui f_168667_ # COLOR_WHITE<br>
 
public net.minecraft.client.renderer.GameRenderer m_109128_(Lnet/minecraft/resources/ResourceLocation;)V # loadEffect </code>
 
  
 +
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 <code>META-INF/accesstransformer.cfg</code> 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 <code>src/main/resources/META-INF/accesstransformer.cfg</code>.
  
If a valid entry is found on a line, Forge will look into the bytecode of the file where that member is defined, and change its access to whatever you desire it to be.
+
To configure the location of the access transformer configuration file with ForgeGradle:
 +
<syntaxhighlight lang="gradle">
 +
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")
 +
}
 +
</syntaxhighlight>
  
This startup modification means accessing is O(1) for all accesses after this initial change, which makes it a great option for performance if you're looking at something a lot
+
[[File:Access-transformers-uncomment-this-line-in-build-gradle.png|thumb|300px]]
  
=== Removing Finality ===
+
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.  
Access transformers can also be used to remove the '''final''' keyword on code elements. To do this, append <code>-f</code> to the access specifier.
 
  
An example of this in place:
+
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.
  
<code ->public-f net.minecraft.world.level.chunk.PaletteResize # makes PaletteResize public</code>
+
== Declarations ==
  
Note: You can also use this to ''add'' a final keyword via <code>+f</code>. This is never recommended however, and should be avoided at all costs unless absolutely necessary.
+
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.
  
== Using Access Transformers in your mod ==
+
The <code>#</code> 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.
  
{{Tip/Important|You should avoid using ATs if a private variable / function you're looking at has a getter or a setter. Variables are usually private for a reason.}}
+
An access transformer declaration has three different variations, depending on the target to be transformed:
  
 +
* for '''classes''': <code>''<modifier>'' ''<class name>''</code>
 +
* for '''methods''': <code>''<modifier>'' ''<class name>'' ''<method name>'''''('''''[parameter descriptors]''''')'''''&lt;return descriptor>''</code>
 +
* for '''fields''': <code>''<modifier>'' ''<class name>'' ''<field name>''</code>
  
Step 1: Uncomment the access transformer line in your build.gradle
+
The modifier consists of one of the following words:
  
[[File:Access-transformers-uncomment-this-line-in-build-gradle.png|600px|frameless]]
+
* <code>public</code> for the ''public'' access level (denoted by the modifier keyword of the same name)
 +
* <code>protected</code> for the ''protected'' access level (denoted by the modifier keyword of the same name)
 +
* <code>default</code> for the ''package-private'' access level (denoted by the absence of an access modifier keyword)
 +
* <code>private</code> for the ''private'' access level (denoted by the modifier keyword of the same name)
 +
* For any of the preceding words, the <code>-f</code> suffix removes the <code>final</code> modifier, while the <code>+f</code> suffix adds the modifier.
  
Step 2: Create a new file called "accesstransformer.cfg" in the <code><nowiki>src/main/resources/META-INF/</nowiki></code> folder.
+
The class name is the fully qualified name of the class, using <code>.</code> (dots) to separate between (sub)packages and classes (for example, <code>java.lang.Object</code>).  
  
Step 3: Refresh the Gradle project. This can be done natively in IDEA and Eclipse.
+
For methods, the method descriptor<ref>''[https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-4.html#jvms-4.3.3 Java Virtual Machine Specification, Java SE 16 Edition]'', 4.3.3. Method Descriptors </ref> 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.
  
== When not to use Access Transformers ==
+
The following are the matching descriptors for each type as seen in the method declaration:
* Unnecessary access transformations (e.g. using ATs to make something <code>public</code> when it's already <code>public</code>)
+
{| class="wikitable"
 +
|+ Matching type descriptors
 +
! Descriptor
 +
! Source form
 +
! Description
 +
|-
 +
| <code>Z</code> || <code>boolean</code> || a <code>true</code> or <code>false</code> value
 +
|-
 +
| <code>B</code> || <code>byte</code> || a 8-bit signed number
 +
|-
 +
| <code>S</code> || <code>short</code> || a 16-bit signed number
 +
|-
 +
| <code>C</code> || <code>char</code> || a Unicode character code point in UTF-16
 +
|-
 +
| <code>I</code> || <code>int</code> || a 32-bit signed number
 +
|-
 +
| <code>F</code> || <code>float</code> || a single-precision floating-point value
 +
|-
 +
| <code>J</code> || <code>long</code> || a 64-bit signed number
 +
|-
 +
| <code>D</code> || <code>double</code> || a double-precision floating-point value
 +
|-
 +
| <code>[''I''</code> || <code>''int''[]</code> || one dimension of an array; this can be repeated, and must end with another descriptor other than an array
 +
|-
 +
| <code>L''java/lang/Object'';</code> || <code>java.lang.Object</code> || a reference type; the internal form of the class' binary name must be present (in the given example, the class referenced is <code>java.lang.Object</code>
 +
|-
 +
| <code>V</code> || <code>void</code> || a void or no-value return type; only available for the return type of a method descriptor
 +
|}
 +
 
 +
== Examples ==
 +
 
 +
The following are some examples of access transformer declarations:
 +
 
 +
<syntaxhighlight lang="java">
 +
# 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
 +
</syntaxhighlight>

Revision as of 02:28, 13 August 2021

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")
}
Access-transformers-uncomment-this-line-in-build-gradle.png

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
V void a void or no-value return type; only available for the return type of a method descriptor

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