このページは、ASMの利用方法をサンプルを通じて察してもらうことを目的としている。
目次
メソッドの単純な書き換え
ここでは、TileEntityFurnaceの書き換えをサンプルとして、メソッドの書き換えを解説する。
ただしcoremodの基礎や1.7のクラス書き換えに含まれている解説はここでは省略する。
今回の例ではかまどで焼きあがるまでの時間を200(棒2つ分)から400(棒4つ分)に変更する。
ソースコード
- SampleTransformer.java
import net.minecraft.launchwrapper.IClassTransformer; import net.minecraftforge.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Type; 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}型を入れる。 @param rawDesc {@link String}型か、{@link Class}型を入れる。 @throws IllegalArgumentException 引数に{@link String}型か、{@link Class}型以外が入ったら投げられる。 */ 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}型を入れる。 @throws IllegalArgumentException {@param raw}に{@link String}型か、{@link Class}型以外が入ったら投げられる。 */ 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
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import static com.mods.kina.asm.SampleTransformer.*; import static org.objectweb.asm.Opcodes.*; 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配列となってこのメソッドにたどり着いてくる。
ここでの流れとしては
- 流れ着いてきたクラスがここでの改変対象に当てはまるか判断する。(当てはまらなければ改変せず返す)
- ClassReaderでbyte配列を解釈。(byte[]→ASM)
- ClassWriterを用意。(ASM→byte[])
- ClassWriterをClassAdapterで包む。
- ClassReaderでClassWriterをacceptする。
- ClassWriterをbyte配列にして返す。
ClassWriterはそもそも、visitXXXを呼ぶことによって内部に情報が溜まっていき、その情報を元にtoByteArrayでクラスを生成することを目的とするクラスである。
そしてClassReader#acceptは解釈したbyte配列を元に、渡されたClassVisitorのvisitメソッドを淡々と呼んでいくものである。
ClassWriterは一部メソッドがオーバーライドできなくなっているが、ラップしてあげることが簡単にできるようになっているのでラップする。
SampleAdapter.java
ClassAdapterもMethodAdapterもやってることは同じなので、同時に解説する。
これらのVisitorは、visit系のメソッドの機能を差し替える、Adapterとして働くように書かれている。
今回の例で言うと
- ClassReader#acceptで順々にvisit系メソッドが呼ばれていく。
- その流れでvisitMethodが呼ばれる。
- そのメソッドが書き換え対象であったなら、通常のVisitorとすり替える形で独自の物を返す。
- 返したMethodVisitorのvisit系メソッドも順々に呼ばれていく。
- その流れでvisitIntInsnが呼ばれる。
- 値だけ差し替えた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", "(Lnet/minecraft/item/ItemStack;)I", 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", "(Lnet/minecraft/item/ItemStack;)I", 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公式] [Javaバイトコード]