Looking for sage advice on Access Transformer and bytecode injection

In Sponge, we have utilities to work with Access Transformers and bytecode injection.

From what I’ve read the AT provides the capabilities to expose fields from objects in Forge/Minecraft. And bytecode injection provides the capabilities to modify the behavior of a Forge object (like an Event).

The question I need to ask is why? Why do we need this (this is not a rhetorical question).

I’m asking for someone with some sage experience in AT and bytecode to run through the proceess and explain why this is necessary and perhaps explain how, with the bytecode injection functions already in the Sponge code base we get additional features in Sponge we wouldn’t otherwise have.

I ask because I’m working on a “fork” of sorts of the Sponge engine and as I read the source carefully, I’m still missing some key issues in my understanding.

So, if you’re adept at AT and bytecode, please reply directly so we can start a dialog. I’ll repost a summary of the knowledge gained.


1 Like

Let me split it out as 2 separate topics, Access transformers and ASM in general

  1. Access transformers

Access transformers are a limited form of ASM that allow you to change accessor of elements(classes, fields and methods) and also allows you to change the final flag.

Minecraft was made with(at least some) normal OOP principles which means that data is often private and classes are made final when you want to make sure that no class extends it. This is great if you have full control over the source, because if you later find you need to change it, you can just change it. This is not great for modding, because it limits you in what and how you can do things.

This is why access transformers were made. They allow you to unlock the closed of parts of minecraft and interact with it. This could also have been done by altering the (decompiled) source of minecraft but that would require a redistribution of the changed source/binary. With ATs it is done on the fly and you never have to distribute any MC code(compiled or not). This solves the problem with the EULA, because the EULA forbids that.(e.g. craftbukkit was in violation of that)

  1. ASM

ASM allows you to edit classes on a bytecode level. This means it allows you to insert, remove or replace instructions in compiled code. This is useful, for example for forge, to insert event posts into existing minecraft code. This is more powerful than ATs because it allows a lot more flexibility(and also ways to screw stuff up).

For similar reasons as the previous point, patching the server at runtime(or just before it starts) allows you to patch in your own logic without needing to redistribute any MC code/binaries. This again solves the problem with the EULA.

So in short: Because the EULA forbids redistribution of minecraft code(altered or not, compiled or not) you can’t directly modify the minecraft source and use that(this was one of the things that craftbukkit violated). ATs and ASM both solve that problem. ATs solve a subset of it in a safe way, ASM solves it in a very flexible but potential dangerous/breaking way.

So far I understood sponge doesn’t want to go the wrapper aproch craftbukkit dit. Also when using wrappers, plugins can’t share or get objects from or to forge mods. And forge support is on the list…

The problem it solves is that BlockBreak is a forge event, and because of that it can’t be exposed to the sponge API. But we do have a VoxelEvent in the API. So for the sponge implementation we either need to duplicate the BlockBreak event(by inserting the event posts for VoxelEvent at the correct positions, potentially breaking stuff and separating the forge mods from sponge plugins) or to extend the BreakBlock event so that it functions as a VoxelEvent.

For the other events currently named in those transformers it again is re-purposing existing fml lifecycle events by adding sponge specific interfaces and methods. E.g. the getGame method that every sponge event has.

The reason why this is done using asm is that it allows you to run sponge on a normal forge install. If it was done by directly editing forge/fml, then people would need to run specific sponge versions of forge to be able to run sponge plugins.

Basically the use of ASM there is to make it easy to reuse existing forge functionality without needing to duplicate everything.

Ok, let’s go through that function:

ClassWriter cwRaw = new ClassWriter(0);
CheckClassAdapter cw = new CheckClassAdapter(cwRaw);

This creates a classwriter which will be used to create a new class. The CheckClassAdapter is used to verify that the internal logic is sound.

    String className = getClassName(eventClass, priority, canceled);
    String classNameInternal = className.replace('.', '/');

    String invokeMethodDesc = "(" + Type.getDescriptor(eventClass) + ")V";

    String eventPriorityName = priority.name();

These collect the name information needed. Java classes have an internal name which has a slightly different format than normally used in java code. ‘.’ are ‘/’ in the internal name.
The invokeMethodDesc is creating the descriptor needed for the method that will be created later. This descriptor means it is a method that returns void and has 1 parameter of the eventClass type.

cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, classNameInternal, null, "org/spongepowered/mod/event/EventListenerHolder", null);

Creates the new class. The superclass is EventListenerHolder.

cw.visitInnerClass("net/minecraftforge/event/world/BlockEvent$BreakEvent", "net/minecraftforge/event/world/BlockEvent", "BreakEvent", 
            ACC_PUBLIC + ACC_STATIC);

External inner classes need to be declared in the class that uses them. That is what this line does. However, this line smells of prototype code. It is a single hard-coded inner class event but this should be changed so that the correct inner class is declared when needed.

        mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "org/spongepowered/mod/event/EventListenerHolder", "<init>", "()V", false);
        mv.visitMaxs(1, 1);

This creates an “empty” constructor. Empty constructors will by default call the super constructor. So that is why it has the INVOKESPECIAL instruction there. If that was missing the validator would give an error.

        mv = cw.visitMethod(ACC_PUBLIC, "invoke", invokeMethodDesc, null, null);
            av0 = mv.visitAnnotation("Lcpw/mods/fml/common/eventhandler/SubscribeEvent;", true);
            av0.visitEnum("priority", "Lcpw/mods/fml/common/eventhandler/EventPriority;", eventPriorityName);
            av0.visit("receiveCanceled", (Boolean) canceled);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKESPECIAL, "org/spongepowered/mod/event/EventListenerHolder", "invoke", "(Ljava/lang/Object;)V", false);
        mv.visitMaxs(2, 2);

This creates a method called invoke using the descriptor that was created earlier. It also adds the FML SubscribeEvent annotation to it. The method it self calls invoke on the parent class(EventListenerHolder).


    if (loaderAccess == null) {
        loaderAccess = new ClassLoaderAccess();
    return loaderAccess.defineClass(className, cwRaw.toByteArray());

This finalizes the class and calls defineClass on the classloader making the newly created class available to the jvm.

So what this function does, is creating a new class that will hold Sponge event listeners and is subscribed to the given forge event and will invoke the sponge event listeners when the forge event is posted.

1 Like

I know this is an old post, but I noticed something that I thought was incorrect.

With access transformers, doesn’t forge make ALL members public at runtime, and that the only reason access transformers are even needed is for dev-time testing / compiling?

EPIC NECRO! Yes, forge does make all members public at runtime and access-transformers are only so one can compile and run in development without any errors.