/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.layoutlib.create; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.util.ArrayList; /** * This method adapter generates delegate methods. *

* Given a method {@code SomeClass.MethodName()}, this generates 1 or 2 methods: *

* A method visitor is generally constructed to generate a single method; however * here we might want to generate one or two depending on the context. To achieve * that, the visitor here generates the 'original' method and acts as a no-op if * no such method exists (e.g. when the original is a native method). * The delegate method is generated after the {@code visitEnd} of the original method * or by having the class adapter directly call {@link #generateDelegateCode()} * for native methods. *

* When generating the 'delegate', the implementation generates a call to a class * class named <className>_Delegate with static methods matching * the methods to be overridden here. The methods have the same return type. * The argument type list is the same except the "this" reference is passed first * for non-static methods. *

* A new annotation is added to these 'delegate' methods so that we can easily find them * for automated testing. *

* This class isn't intended to be generic or reusable. * It is called by {@link DelegateClassAdapter}, which takes care of properly initializing * the two method writers for the original and the delegate class, as needed, with their * expected names. *

* The class adapter also takes care of calling {@link #generateDelegateCode()} directly for * a native and use the visitor pattern for non-natives. * Note that native methods have, by definition, no code so there's nothing a visitor * can visit. *

* Instances of this class are not re-usable. * The class adapter creates a new instance for each method. */ class DelegateMethodAdapter extends MethodVisitor { /** Suffix added to delegate classes. */ public static final String DELEGATE_SUFFIX = "_Delegate"; /** The parent method writer to copy of the original method. * Null when dealing with a native original method. */ private MethodVisitor mOrgWriter; /** The parent method writer to generate the delegating method. Never null. */ private MethodVisitor mDelWriter; /** The original method descriptor (return type + argument types.) */ private String mDesc; /** True if the original method is static. */ private final boolean mIsStatic; /** True if the method is contained in a static inner class */ private final boolean mIsStaticInnerClass; /** The internal class name (e.g. com/android/SomeClass$InnerClass.) */ private final String mClassName; /** The method name. */ private final String mMethodName; /** Logger object. */ private final Log mLog; /** Array used to capture the first line number information from the original method * and duplicate it in the delegate. */ private Object[] mDelegateLineNumber; /** * Creates a new {@link DelegateMethodAdapter} that will transform this method * into a delegate call. *

* See {@link DelegateMethodAdapter} for more details. * * @param log The logger object. Must not be null. * @param mvOriginal The parent method writer to copy of the original method. * Must be {@code null} when dealing with a native original method. * @param mvDelegate The parent method writer to generate the delegating method. * Must never be null. * @param className The internal class name of the class to visit, * e.g. com/android/SomeClass$InnerClass. * @param methodName The simple name of the method. * @param desc A method descriptor (c.f. {@link Type#getReturnType(String)} + * {@link Type#getArgumentTypes(String)}) * @param isStatic True if the method is declared static. */ public DelegateMethodAdapter(Log log, MethodVisitor mvOriginal, MethodVisitor mvDelegate, String className, String methodName, String desc, boolean isStatic, boolean isStaticClass) { super(Main.ASM_VERSION); mLog = log; mOrgWriter = mvOriginal; mDelWriter = mvDelegate; mClassName = className; mMethodName = methodName; mDesc = desc; mIsStatic = isStatic; mIsStaticInnerClass = isStaticClass; } /** * Generates the new code for the method. *

* For native methods, this must be invoked directly by {@link DelegateClassAdapter} * (since they have no code to visit). *

* Otherwise for non-native methods the {@link DelegateClassAdapter} simply needs to * return this instance of {@link DelegateMethodAdapter} and let the normal visitor pattern * invoke it as part of the {@link ClassReader#accept(ClassVisitor, int)} workflow and then * this method will be invoked from {@link MethodVisitor#visitEnd()}. */ public void generateDelegateCode() { /* * The goal is to generate a call to a static delegate method. * If this method is non-static, the first parameter will be 'this'. * All the parameters must be passed and then the eventual return type returned. * * Example, let's say we have a method such as * public void myMethod(int a, Object b, ArrayList c) { ... } * * We'll want to create a body that calls a delegate method like this: * TheClass_Delegate.myMethod(this, a, b, c); * * If the method is non-static and the class name is an inner class (e.g. has $ in its * last segment), we want to push the 'this' of the outer class first: * OuterClass_InnerClass_Delegate.myMethod( * OuterClass.this, * OuterClass$InnerClass.this, * a, b, c); * * Only one level of inner class is supported right now, for simplicity and because * we don't need more. * * The generated class name is the current class name with "_Delegate" appended to it. * One thing to realize is that we don't care about generics -- since generic types * are erased at build time, they have no influence on the method name being called. */ // Add our annotation AnnotationVisitor aw = mDelWriter.visitAnnotation( Type.getObjectType(Type.getInternalName(LayoutlibDelegate.class)).toString(), true); // visible at runtime if (aw != null) { aw.visitEnd(); } mDelWriter.visitCode(); if (mDelegateLineNumber != null) { Object[] p = mDelegateLineNumber; mDelWriter.visitLineNumber((Integer) p[0], (Label) p[1]); } ArrayList paramTypes = new ArrayList<>(); String delegateClassName = mClassName + DELEGATE_SUFFIX; boolean pushedArg0 = false; int maxStack = 0; // Check if the last segment of the class name has inner an class. // Right now we only support one level of inner classes. Type outerType = null; int slash = mClassName.lastIndexOf('/'); int dol = mClassName.lastIndexOf('$'); if (dol != -1 && dol > slash && dol == mClassName.indexOf('$')) { String outerClass = mClassName.substring(0, dol); outerType = Type.getObjectType(outerClass); // Change a delegate class name to "com/foo/Outer_Inner_Delegate" delegateClassName = delegateClassName.replace('$', '_'); } // For an instance method (e.g. non-static), push the 'this' preceded // by the 'this' of any outer class, if any. if (!mIsStatic) { if (outerType != null && !mIsStaticInnerClass) { // The first-level inner class has a package-protected member called 'this$0' // that points to the outer class. // Push this.getField("this$0") on the call stack. mDelWriter.visitVarInsn(Opcodes.ALOAD, 0); // var 0 = this mDelWriter.visitFieldInsn(Opcodes.GETFIELD, mClassName, // class where the field is defined "this$0", // field name outerType.getDescriptor()); // type of the field maxStack++; paramTypes.add(outerType); } // Push "this" for the instance method, which is always ALOAD 0 mDelWriter.visitVarInsn(Opcodes.ALOAD, 0); maxStack++; pushedArg0 = true; paramTypes.add(Type.getObjectType(mClassName)); } // Push all other arguments. Start at arg 1 if we already pushed 'this' above. Type[] argTypes = Type.getArgumentTypes(mDesc); int maxLocals = pushedArg0 ? 1 : 0; for (Type t : argTypes) { int size = t.getSize(); mDelWriter.visitVarInsn(t.getOpcode(Opcodes.ILOAD), maxLocals); maxLocals += size; maxStack += size; paramTypes.add(t); } // Construct the descriptor of the delegate based on the parameters // we pushed on the call stack. The return type remains unchanged. String desc = Type.getMethodDescriptor( Type.getReturnType(mDesc), paramTypes.toArray(new Type[paramTypes.size()])); // Invoke the static delegate mDelWriter.visitMethodInsn(Opcodes.INVOKESTATIC, delegateClassName, mMethodName, desc, false); Type returnType = Type.getReturnType(mDesc); mDelWriter.visitInsn(returnType.getOpcode(Opcodes.IRETURN)); mDelWriter.visitMaxs(maxStack, maxLocals); mDelWriter.visitEnd(); // For debugging now. Maybe we should collect these and store them in // a text file for helping create the delegates. We could also compare // the text file to a golden and break the build on unsupported changes // or regressions. Even better we could fancy-print something that looks // like the expected Java method declaration. mLog.debug("Delegate: %1$s # %2$s %3$s", delegateClassName, mMethodName, desc); } /* Pass down to visitor writer. In this implementation, either do nothing. */ @Override public void visitCode() { if (mOrgWriter != null) { mOrgWriter.visitCode(); } } /* * visitMaxs is called just before visitEnd if there was any code to rewrite. */ @Override public void visitMaxs(int maxStack, int maxLocals) { if (mOrgWriter != null) { mOrgWriter.visitMaxs(maxStack, maxLocals); } } /** End of visiting. Generate the delegating code. */ @Override public void visitEnd() { if (mOrgWriter != null) { mOrgWriter.visitEnd(); } generateDelegateCode(); } /* Writes all annotation from the original method. */ @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (mOrgWriter != null) { return mOrgWriter.visitAnnotation(desc, visible); } else { return null; } } /* Writes all annotation default values from the original method. */ @Override public AnnotationVisitor visitAnnotationDefault() { if (mOrgWriter != null) { return mOrgWriter.visitAnnotationDefault(); } else { return null; } } @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { if (mOrgWriter != null) { return mOrgWriter.visitParameterAnnotation(parameter, desc, visible); } else { return null; } } /* Writes all attributes from the original method. */ @Override public void visitAttribute(Attribute attr) { if (mOrgWriter != null) { mOrgWriter.visitAttribute(attr); } } /* * Only writes the first line number present in the original code so that source * viewers can direct to the correct method, even if the content doesn't match. */ @Override public void visitLineNumber(int line, Label start) { // Capture the first line values for the new delegate method if (mDelegateLineNumber == null) { mDelegateLineNumber = new Object[] { line, start }; } if (mOrgWriter != null) { mOrgWriter.visitLineNumber(line, start); } } @Override public void visitInsn(int opcode) { if (mOrgWriter != null) { mOrgWriter.visitInsn(opcode); } } @Override public void visitLabel(Label label) { if (mOrgWriter != null) { mOrgWriter.visitLabel(label); } } @Override public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { if (mOrgWriter != null) { mOrgWriter.visitTryCatchBlock(start, end, handler, type); } } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (mOrgWriter != null) { mOrgWriter.visitMethodInsn(opcode, owner, name, desc, itf); } } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { if (mOrgWriter != null) { mOrgWriter.visitFieldInsn(opcode, owner, name, desc); } } @Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { if (mOrgWriter != null) { mOrgWriter.visitFrame(type, nLocal, local, nStack, stack); } } @Override public void visitIincInsn(int var, int increment) { if (mOrgWriter != null) { mOrgWriter.visitIincInsn(var, increment); } } @Override public void visitIntInsn(int opcode, int operand) { if (mOrgWriter != null) { mOrgWriter.visitIntInsn(opcode, operand); } } @Override public void visitJumpInsn(int opcode, Label label) { if (mOrgWriter != null) { mOrgWriter.visitJumpInsn(opcode, label); } } @Override public void visitLdcInsn(Object cst) { if (mOrgWriter != null) { mOrgWriter.visitLdcInsn(cst); } } @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { if (mOrgWriter != null) { mOrgWriter.visitLocalVariable(name, desc, signature, start, end, index); } } @Override public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { if (mOrgWriter != null) { mOrgWriter.visitLookupSwitchInsn(dflt, keys, labels); } } @Override public void visitMultiANewArrayInsn(String desc, int dims) { if (mOrgWriter != null) { mOrgWriter.visitMultiANewArrayInsn(desc, dims); } } @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { if (mOrgWriter != null) { mOrgWriter.visitTableSwitchInsn(min, max, dflt, labels); } } @Override public void visitTypeInsn(int opcode, String type) { if (mOrgWriter != null) { mOrgWriter.visitTypeInsn(opcode, type); } } @Override public void visitVarInsn(int opcode, int var) { if (mOrgWriter != null) { mOrgWriter.visitVarInsn(opcode, var); } } }