org.spongepowered.mod.asm.transformers.MixinTransformer.java Source code

Java tutorial

Introduction

Here is the source code for org.spongepowered.mod.asm.transformers.MixinTransformer.java

Source

/**
 * This file is part of Sponge, licensed under the MIT License (MIT).
 *
 * Copyright (c) SpongePowered.org <http://www.spongepowered.org>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.spongepowered.mod.asm.transformers;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.spongepowered.mod.asm.util.ASMHelper;
import org.spongepowered.mod.mixin.InvalidMixinException;
import org.spongepowered.mod.mixin.Overwrite;
import org.spongepowered.mod.mixin.Shadow;

/**
 * Transformer which applies Mixin classes to their declared target classes
 */
public class MixinTransformer extends TreeTransformer {

    /**
     * Log all the things
     */
    private final Logger logger = LogManager.getLogger("sponge");

    /**
     * Mixin configuration bundle
     */
    private final MixinConfig config;

    /**
     * ctor 
     */
    public MixinTransformer() {
        this.config = MixinConfig.create("mixins.sponge.json");
    }

    /* (non-Javadoc)
     * @see net.minecraft.launchwrapper.IClassTransformer#transform(java.lang.String, java.lang.String, byte[])
     */
    @Override
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
        if (transformedName != null && transformedName.startsWith(this.config.getMixinPackage())) {
            throw new RuntimeException(
                    String.format("%s is a mixin class and cannot be referenced directly", transformedName));
        }

        if (this.config.hasMixinsFor(transformedName)) {
            try {
                return this.applyMixins(transformedName, basicClass);
            } catch (InvalidMixinException th) {
                this.logger.warn(
                        String.format("Class mixin failed: %s %s", th.getClass().getName(), th.getMessage()), th);
                th.printStackTrace();
            }
        }

        return basicClass;
    }

    /**
     * Apply mixins for specified target class to the class described by the supplied byte array
     * 
     * @param transformedName 
     * @param basicClass
     * @return
     */
    private byte[] applyMixins(String transformedName, byte[] basicClass) {
        // Tree for target class
        ClassNode targetClass = this.readClass(basicClass, true);

        // Get and sort mixins for the class
        List<MixinInfo> mixins = this.config.getMixinsFor(transformedName);
        Collections.sort(mixins);

        for (MixinInfo mixin : mixins) {
            this.logger.info("Applying mixin {} to {}", mixin.getClassName(), transformedName);
            this.applyMixin(targetClass, mixin.getData());
        }

        // Extension point
        this.postTransform(transformedName, targetClass, mixins);

        // Collapse tree to bytes
        return this.writeClass(targetClass);
    }

    /**
     * @param transformedName
     * @param targetClass
     * @param mixins
     */
    protected void postTransform(String transformedName, ClassNode targetClass, List<MixinInfo> mixins) {
        // Stub for subclasses
    }

    /**
     * Apply the mixin described by mixin to the supplied classNode
     * 
     * @param targetClass
     * @param mixinInfo
     */
    protected void applyMixin(ClassNode targetClass, MixinData mixin) {

        try {
            this.verifyClasses(targetClass, mixin);
            this.applyMixinInterfaces(targetClass, mixin);
            this.applyMixinAttributes(targetClass, mixin);
            this.applyMixinFields(targetClass, mixin);
            this.applyMixinMethods(targetClass, mixin);
        } catch (Exception ex) {
            throw new InvalidMixinException("Unexpecteded error whilst applying the mixin class", ex);
        }
    }

    /**
     * Perform pre-flight checks on the mixin and target classes
     * 
     * @param targetClass
     * @param mixin
     */
    protected void verifyClasses(ClassNode targetClass, MixinData mixin) {
        String superName = mixin.getClassNode().superName;
        if (targetClass.superName == null || superName == null || !targetClass.superName.equals(superName)) {
            throw new InvalidMixinException("Mixin classes must have the same superclass as their target class");
        }
    }

    /**
     * Mixin interfaces implemented by the mixin class onto the target class
     * 
     * @param targetClass
     * @param mixin
     */
    private void applyMixinInterfaces(ClassNode targetClass, MixinData mixin) {
        for (String interfaceName : mixin.getClassNode().interfaces) {
            if (!targetClass.interfaces.contains(interfaceName)) {
                targetClass.interfaces.add(interfaceName);
            }
        }
    }

    /**
     * Mixin misc attributes from mixin class onto the target class
     * 
     * @param targetClass
     * @param mixin
     */
    private void applyMixinAttributes(ClassNode targetClass, MixinData mixin) {
        if (this.config.shouldSetSourceFile()) {
            targetClass.sourceFile = mixin.getClassNode().sourceFile;
        }
    }

    /**
     * Mixin fields from mixin class into the target class. It is vital that this is done before mixinMethods because we need to compute renamed
     * fields so that transformMethod can rename field references in the method body
     * 
     * @param targetClass
     * @param mixin
     */
    private void applyMixinFields(ClassNode targetClass, MixinData mixin) {
        for (FieldNode field : mixin.getClassNode().fields) {
            // Public static fields will fall foul of early static binding in java, including them in a mixin is an error condition
            if (MixinTransformer.hasFlag(field, Opcodes.ACC_STATIC)
                    && !MixinTransformer.hasFlag(field, Opcodes.ACC_PRIVATE)) {
                throw new InvalidMixinException(String.format(
                        "Mixin classes cannot contain visible static methods or fields, found %s", field.name));
            }

            FieldNode target = this.findTargetField(targetClass, field);
            if (target == null) {
                // If this field is a shadow field but is NOT found in the target class, that's bad, mmkay
                boolean isShadow = ASMHelper.getVisibleAnnotation(field, Shadow.class) != null;
                if (isShadow) {
                    throw new InvalidMixinException(
                            String.format("Shadow field %s was not located in the target class", field.name));
                }

                // This is just a local field, so add it
                targetClass.fields.add(field);
            } else {
                // Check that the shadow field has a matching descriptor
                if (!target.desc.equals(field.desc)) {
                    throw new InvalidMixinException(String
                            .format("The field %s in the target class has a conflicting signature", field.name));
                }
            }
        }
    }

    /**
     * Mixin methods from the mixin class into the target class
     * 
     * @param targetClass
     * @param mixin
     */
    private void applyMixinMethods(ClassNode targetClass, MixinData mixin) {
        for (MethodNode mixinMethod : mixin.getClassNode().methods) {
            // Reparent all mixin methods into the target class
            this.transformMethod(mixinMethod, mixin.getClassNode().name, targetClass.name);

            boolean isShadow = ASMHelper.getVisibleAnnotation(mixinMethod, Shadow.class) != null;
            boolean isOverwrite = ASMHelper.getVisibleAnnotation(mixinMethod, Overwrite.class) != null;
            boolean isAbstract = MixinTransformer.hasFlag(mixinMethod, Opcodes.ACC_ABSTRACT);

            if (isShadow || isAbstract) {
                // For shadow (and abstract, which can be used as a shorthand for Shadow) methods, we just check they're present
                MethodNode target = this.findTargetMethod(targetClass, mixinMethod);
                if (target == null) {
                    throw new InvalidMixinException(String
                            .format("Shadow method %s was not located in the target class", mixinMethod.name));
                }
            } else if (!mixinMethod.name.startsWith("<")) {
                if (MixinTransformer.hasFlag(mixinMethod, Opcodes.ACC_STATIC)
                        && !MixinTransformer.hasFlag(mixinMethod, Opcodes.ACC_PRIVATE) && !isOverwrite) {
                    throw new InvalidMixinException(
                            String.format("Mixin classes cannot contain visible static methods or fields, found %s",
                                    mixinMethod.name));
                }

                MethodNode target = this.findTargetMethod(targetClass, mixinMethod);
                if (target != null) {
                    targetClass.methods.remove(target);
                } else if (isOverwrite) {
                    throw new InvalidMixinException(String
                            .format("Overwrite target %s was not located in the target class", mixinMethod.name));
                }
                targetClass.methods.add(mixinMethod);
            } else if ("<clinit>".equals(mixinMethod.name)) {
                // Class initialiser insns get appended
                this.appendInsns(targetClass, mixinMethod.name, mixinMethod);
            }
        }
    }

    /**
     * Handles "re-parenting" the method supplied, changes all references to the mixin class to refer to the target class (for field accesses and
     * method invokations) and also renames fields accesses to their obfuscated versions
     * 
     * @param method
     * @param fromClass
     * @param toClass
     * @return
     */
    private void transformMethod(MethodNode method, String fromClass, String toClass) {
        Iterator<AbstractInsnNode> iter = method.instructions.iterator();
        while (iter.hasNext()) {
            AbstractInsnNode insn = iter.next();

            if (insn instanceof MethodInsnNode) {
                MethodInsnNode methodInsn = (MethodInsnNode) insn;
                if (methodInsn.owner.equals(fromClass)) {
                    methodInsn.owner = toClass;
                }
            }
            if (insn instanceof FieldInsnNode) {
                FieldInsnNode fieldInsn = (FieldInsnNode) insn;
                if (fieldInsn.owner.equals(fromClass)) {
                    fieldInsn.owner = toClass;
                }
            }
        }
    }

    /**
     * Handles appending instructions from the source method to the target method
     * 
     * @param targetClass
     * @param targetMethodName
     * @param sourceMethod
     */
    private void appendInsns(ClassNode targetClass, String targetMethodName, MethodNode sourceMethod) {
        if (Type.getReturnType(sourceMethod.desc) != Type.VOID_TYPE) {
            throw new IllegalArgumentException("Attempted to merge insns into a method which does not return void");
        }

        if (targetMethodName == null || targetMethodName.length() == 0) {
            targetMethodName = sourceMethod.name;
        }

        for (MethodNode method : targetClass.methods) {
            if ((targetMethodName.equals(method.name)) && sourceMethod.desc.equals(method.desc)) {
                AbstractInsnNode returnNode = null;
                Iterator<AbstractInsnNode> findReturnIter = method.instructions.iterator();
                while (findReturnIter.hasNext()) {
                    AbstractInsnNode insn = findReturnIter.next();
                    if (insn.getOpcode() == Opcodes.RETURN) {
                        returnNode = insn;
                        break;
                    }
                }

                Iterator<AbstractInsnNode> injectIter = sourceMethod.instructions.iterator();
                while (injectIter.hasNext()) {
                    AbstractInsnNode insn = injectIter.next();
                    if (!(insn instanceof LineNumberNode) && insn.getOpcode() != Opcodes.RETURN) {
                        method.instructions.insertBefore(returnNode, insn);
                    }
                }
            }
        }
    }

    /**
     * Finds a method in the target class
     * 
     * @param targetClass
     * @param searchFor
     * @return
     */
    private MethodNode findTargetMethod(ClassNode targetClass, MethodNode searchFor) {
        for (MethodNode target : targetClass.methods) {
            if (target.name.equals(searchFor.name) && target.desc.equals(searchFor.desc)) {
                return target;
            }
        }

        return null;
    }

    /**
     * Finds a field in the target class
     * 
     * @param targetClass
     * @param searchFor
     * @return
     */
    private FieldNode findTargetField(ClassNode targetClass, FieldNode searchFor) {
        for (FieldNode target : targetClass.fields) {
            if (target.name.equals(searchFor.name)) {
                return target;
            }
        }

        return null;
    }

    /**
     * Check whether the specified flag is set on the specified method
     * 
     * @param method
     * @param flag 
     * @return
     */
    private static boolean hasFlag(MethodNode method, int flag) {
        return (method.access & flag) == flag;
    }

    /**
     * Check whether the specified flag is set on the specified field
     * 
     * @param field
     * @param flag 
     * @return
     */
    private static boolean hasFlag(FieldNode field, int flag) {
        return (field.access & flag) == flag;
    }
}