/*
* 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.
* <p/>
* Given a method {@code SomeClass.MethodName()}, this generates 1 or 2 methods:
* <ul>
* <li> A copy of the original method named {@code SomeClass.MethodName_Original()}.
* The content is the original method as-is from the reader.
* This step is omitted if the method is native, since it has no Java implementation.
* <li> A brand new implementation of {@code SomeClass.MethodName()} which calls to a
* non-existing method named {@code SomeClass_Delegate.MethodName()}.
* The implementation of this 'delegate' method is done in layoutlib_brigde.
* </ul>
* 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 <em>directly</em> call {@link #generateDelegateCode()}
* for native methods.
* <p/>
* When generating the 'delegate', the implementation generates a call to a class
* class named <code><className>_Delegate</code> 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.
* <p/>
* A new annotation is added to these 'delegate' methods so that we can easily find them
* for automated testing.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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;
/** The internal class name (e.g. <code>com/android/SomeClass$InnerClass</code>.) */
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.
* <p/>
* 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. <code>com/android/SomeClass$InnerClass</code>.
* @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) {
super(Opcodes.ASM4);
mLog = log;
mOrgWriter = mvOriginal;
mDelWriter = mvDelegate;
mClassName = className;
mMethodName = methodName;
mDesc = desc;
mIsStatic = isStatic;
}
/**
* Generates the new code for the method.
* <p/>
* For native methods, this must be invoked directly by {@link DelegateClassAdapter}
* (since they have no code to visit).
* <p/>
* 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<String> 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<Type> paramTypes = new ArrayList<Type>();
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) {
// 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);
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) {
if (mOrgWriter != null) {
mOrgWriter.visitMethodInsn(opcode, owner, name, desc);
}
}
@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);
}
}
}