提供: Minecraft Modding Wiki
移動先: 案内検索

このページは、ASMの利用方法をサンプルを通じて察してもらうことを目的としている。
そのためCoremodsの基礎に含まれている解説はここでは省略する。

メソッドの単純な書き換え

ここでは、TileEntityFurnaceの書き換えをサンプルとして、メソッドの書き換えを解説する。
今回の例ではかまどで焼きあがるまでの時間を200(棒2つ分)から400(棒4つ分)に変更する。

ソースコード

  • SampleTransformer.java
//略

public class SampleTransformer implements IClassTransformer{
    public static final String TARGET = "net.minecraft.tileentity.TileEntityFurnace";

    /**
     クラスが最初に読み込まれた時に呼ばれる。

     @param  name クラスの難読化名
     @param  transformedName クラスの易読化名
     @param  bytes オリジナルのクラス
     */
    @Override
    public byte[] transform(String name, String transformedName, byte[] bytes){
        if(!accept(transformedName))return bytes;
        //byte配列を読み込み、利用しやすい形にする。
        ClassReader cr = new ClassReader(bytes);
        //これのvisitを呼ぶことによって情報が溜まっていく。
        ClassWriter cw = new ClassWriter(1);
        //Adapterを通して書き換え出来るようにする。
        ClassVisitor cv = new SampleAdapter.ClassAdapter(cw);
        //元のクラスと同様の順番でvisitメソッドを呼んでくれる
        cr.accept(cv,0);

        //Writer内の情報をbyte配列にして返す。
        return cw.toByteArray();
    }

    /**
     書き換え対象かどうかを判定する。今回はClass名のみで。
     */
    private boolean accept(String className){
        return TARGET.equals(className);
    }

    /**
     クラスの名前を難読化(obfuscation)する。
     */
    public static String unmapClassName(String name){
        return FMLDeobfuscatingRemapper.INSTANCE.unmap(name.replace('.','/')).replace('/', '.');
    }

    /**
     メソッドの名前を易読化(deobfuscation)する。
     */
    public static String mapMethodName(String owner,String methodName,String desc){
        return FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(unmapClassName(owner), methodName, desc);
    }

    /**
     フィールドの名前を易読化(deobfuscation)する。
     */
    public static String mapFieldName(String owner,String methodName,String desc){
        return FMLDeobfuscatingRemapper.INSTANCE.mapFieldName(unmapClassName(owner), methodName, desc);
    }

    /**
     下の{@link #toDesc(Object)}をMethodのDescriptor用に使えるようにしたもの。
     下手なクラスをここに入れようとするとまずいので確信がない限りStringで入れるべき。

     @param returnType {@link String}型か、{@link Class}型で目的のMethodの返り値の型を指定する。
     @param rawDesc {@link String}型か、{@link Class}型でMethodの引数たちの型を指定する。
     @throws IllegalArgumentException 引数に{@link String}型か、{@link Class}型以外が入ったら投げられる。
     @return Javaバイトコードで扱われる形の文字列に変換されたDescriptor。
     */
    public static String toDesc(Object returnType,Object... rawDesc){
        StringBuilder sb = new StringBuilder("(");
        for(Object o : rawDesc){
            sb.append(toDesc(o));
        }
        sb.append(')');
        sb.append(toDesc(returnType));
        return sb.toString();
    }

    /**
     {@link Class#forName}とか{@link Class#getCanonicalName()}したりするとまだ読み込まれてなかったりしてまずいので安全策。
     下手なクラスをここに入れようとするとまずいので確信がない限りStringで入れるべき。

     @param raw {@link String}型か、{@link Class}型でASM用の文字列に変換したいクラスを指定する。
     @throws IllegalArgumentException {@param raw}に{@link String}型か、{@link Class}型以外が入ったら投げられる。
     @return Javaバイトコードで扱われる形の文字列に変換されたクラス。
     */
    public static String toDesc(Object raw){
        if(raw instanceof Class){
            Class<?> clazz = (Class<?>) raw;
            return Type.getDescriptor(clazz);
        }else if(raw instanceof String){
            String desc = (String) raw;
            desc = desc.replace('.','/');
            desc = desc.matches("L.+;")?desc:"L"+desc+";";
            return desc;
        }else {
            throw new IllegalArgumentException();
        }
    }
}
  • SampleAdapter.java
//略

public class SampleAdapter{
    public static class ClassAdapter extends ClassVisitor{
        public ClassAdapter(ClassVisitor cv){
            super(ASM4, cv);
        }

        /**
         メソッドについて呼ばれる。

         @param access
         {@link Opcodes}に載ってるやつ。publicとかstaticとかの状態がわかる。
         @param name
         メソッドの名前。
         @param desc
         メソッドの(引数と返り値を合わせた)型。{@link Type#getArgumentTypes(String)},{@link Type#getClassName()}
         @param signature
         ジェネリック部分を含むメソッドの(引数と返り値を合わせた)型。ジェネリック付きでなければおそらくnull。{@link Type#getArgumentTypes(String)},{@link
         Type#getClassName()}
         @param exceptions
         throws句にかかれているクラスが列挙される。Lと;で囲われていないので{@link String#replace(char, char)}で'/'と'.'を置換してやればOK。
         @return ここで返したMethodVisitorのメソッド群が適応される。ClassWriterがセットされていればMethodWriterがsuperから降りてくる。
         */
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions){
            //ターゲット(ここではfunc_174904_a)かどうかを判定し、
            if("func_174904_a".equals(mapMethodName(TARGET, name, desc)) && toDesc(int.class, "net.minecraft.item.ItemStack").equals(desc)){
                //そうであるならばAdapterを差し込む。
                return new MethodAdapter(super.visitMethod(access, name, desc, signature, exceptions));
            }
            return super.visitMethod(access, name, desc, signature, exceptions);
        }
    }

    public static class MethodAdapter extends MethodVisitor{
        public MethodAdapter(MethodVisitor mv){
            super(ASM4, mv);
        }

        /**
         int型変数等の操作時に呼ばれる。

         @param opcode byteの範囲で扱えるならBIPUSH、shortの範囲で扱えるならSIPUSHが入っている。
         @param operand shortの範囲に収まる値が入っている。
         */
        @Override
        public void visitIntInsn(int opcode, int operand){
            //本来200が入るところを400にすり替えて渡す。
            super.visitIntInsn(opcode, 400);
        }
    }
}

解説

SampleTransformer.java

transform

特に対処されてないクラスたちが、読み込まれたタイミングにbyte配列となってこのメソッドにたどり着いてくる。
ここでの流れとしては

  1. 流れ着いてきたクラスがここでの改変対象に当てはまるか判断する。(当てはまらなければ改変せず返す)
  2. ClassReaderでbyte配列を解釈。(byte[]→ASM)
  3. ClassWriterを用意。(ASM→byte[])
  4. ClassWriterをClassAdapterで包む。
  5. ClassReaderでClassWriterをacceptする。
  6. ClassWriterをbyte配列にして返す。

ClassWriterはそもそも、visitXXXを呼ぶことによって内部に情報が溜まっていき、その情報を元にtoByteArrayでクラスを生成することを目的とするクラスである。
そしてClassReader#acceptは解釈したbyte配列を元に、渡されたClassVisitorのvisitメソッドを淡々と呼んでいくものである。
ClassWriterは一部メソッドがオーバーライドできなくなっているが、ラップしてあげることが簡単にできるようになっているのでラップする。

SampleAdapter.java

ClassAdapterもMethodAdapterもやってることは同じなので、同時に解説する。
これらのVisitorは、visit系のメソッドの機能を差し替える、Adapterとして働くように書かれている。
今回の例で言うと

  1. ClassReader#acceptで順々にvisit系メソッドが呼ばれていく。
  2. その流れでvisitMethodが呼ばれる。
  3. そのメソッドが書き換え対象であったなら、通常のVisitorとすり替える形で独自の物を返す。
  4. 返したMethodVisitorのvisit系メソッドも順々に呼ばれていく。
  5. その流れでvisitIntInsnが呼ばれる。
  6. 値だけ差し替えたvisitIntInsnが呼ばれたことになる。

こうしてWriterで情報として入るはずだった200が400になりtoByteArrayにも反映され、書き換えが遂行される。

メソッドにフックを仕込む

ここでは、TileEntityFurnaceの書き換えをサンプルとして、メソッドの書き換えを解説する。
前章の例の改良として行うので、改変した部分だけの解説とする。
なお、この例をそのまま持って行っても表面上の動作は変わらない。

ソースコード

  • SampleAdapter.java
//省略
public class SampleAdapter{
    //ClassAdapter省略
    public static class MethodAdapter extends MethodVisitor{
        public MethodAdapter(MethodVisitor mv){
            super(ASM4, mv);
        }

        /**
         int型変数等の操作時に呼ばれる。

         @param opcode byteの範囲で扱えるならBIPUSH、shortの範囲で扱えるならSIPUSHが入っている。
         @param operand shortの範囲に収まる値が入っている。
         */
        @Override
        public void visitIntInsn(int opcode, int operand){
            //前章のやつ。
            //super.visitIntInsn(opcode, 400);
            super.visitVarInsn(ALOAD, 1);
            super.visitMethodInsn(INVOKESTATIC, "SampleHooks", "totalCookTime", SampleTransformer.toDesc(int.class, "net.minecraft.item.ItemStack"), false);
        }
    }
}
  • SampleHooks.java
import net.minecraft.item.ItemStack;

public class SampleHooks{
    public static int totalCookTime(ItemStack stack){
        return 400;
    }
}

解説

MethodAdapter

public void visitIntInsn(int opcode, int operand){
    super.visitVarInsn(ALOAD, 1);
    super.visitMethodInsn(INVOKESTATIC, "SampleHooks", "totalCookTime", SampleTransformer.toDesc(int.class, "net.minecraft.item.ItemStack"), false);
}
super.visitVarInsn(ALOAD, 1);
スタックに引数1を積む。(静的でないメソッドの場合、ALOAD 0はthisである。)
super.visitMethodInsn(INVOKESTATIC, "SampleHooks", "totalCookTime", SampleTransformer.toDesc(int.class, "net.minecraft.item.ItemStack"), false);
用意したメソッドを呼ぶ。

肝心の呼び出したメソッドがハードコーディングだが、このメソッドにEvent発火を仕込むなどすれば利用のしやすさが大きく変わるだろう。

メソッドを増やす

(書いている途中ですが、上で説明したのを応用すればなんとかなると思います。)

Nodeの利用

(書いている途中ですが、上で説明したのを応用すればなんとかなると思います。)

書き換え以外の利用

(書いている途中ですが、上で説明したのを応用すればなんとかなると思います。)

その他

ASM Bytecode Outlineというプラグインを使うと、バイトコードを見れる

メソッドやフィールドのMCPは以下のディレクトリ C:\Users\ユーザー名\.gradle\caches\minecraft\de\oceanlabs\mcp\mcp_snapshot

参考

ASM公式 Javaバイトコード