Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 03245a8e authored by Raphael's avatar Raphael Committed by Android (Google) Code Review
Browse files

Merge "LayoutLib.Create: support Outer_Inner_Delegate renaming." into honeycomb

parents 53fe1045 811820f4
Loading
Loading
Loading
Loading
+61 −22
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.util.ArrayList;

/**
 * This method adapter rewrites a method by discarding the original code and generating
 * a call to a delegate. Original annotations are passed along unchanged.
@@ -124,7 +126,7 @@ class DelegateMethodAdapter implements MethodVisitor {
    public void generateCode() {
        /*
         * The goal is to generate a call to a static delegate method.
         * If this method is not-static, the first parameter will be this.
         * 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
@@ -133,9 +135,19 @@ class DelegateMethodAdapter implements MethodVisitor {
         * We'll want to create a body that calls a delegate method like this:
         *   TheClass_Delegate.method_1(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.method_1(
         *     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 runtime, they have no influence on the method being called.
         * are erased at runtime, they have no influence on the method name being called.
         */

        // Add our annotation
@@ -151,34 +163,61 @@ class DelegateMethodAdapter implements MethodVisitor {
            mVisitCodeCalled = true;
        }

        int numVars = 0;
        ArrayList<Type> paramTypes = new ArrayList<Type>();
        String delegateClassName = mClassName + DELEGATE_SUFFIX;
        boolean pushedArg0 = false;
        int maxStack = 0;

        // Push "this" for an instance method, which is always ALOAD 0
        // For an instance method (e.g. non-static), push the 'this' preceded
        // by the 'this' of any outer class, if any.
        if (!mIsStatic) {
            mParentVisitor.visitVarInsn(Opcodes.ALOAD, numVars++);
        }

        // Push all other arguments
            // Check if the last segment of the class name has inner an class.
            // Right now we only support one level of inner classes.
            int slash = mClassName.lastIndexOf('/');
            int dol = mClassName.lastIndexOf('$');
            if (dol != -1 && dol > slash && dol == mClassName.indexOf('$')) {
                String outerClass = mClassName.substring(0, dol);
                Type outerType = Type.getObjectType(outerClass);

                // Change a delegate class name to "com/foo/Outer_Inner_Delegate"
                delegateClassName = delegateClassName.replace('$', '_');

                // 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.
                mParentVisitor.visitVarInsn(Opcodes.ALOAD, 0); // var 0 = this
                mParentVisitor.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
            mParentVisitor.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();
            mParentVisitor.visitVarInsn(t.getOpcode(Opcodes.ILOAD), numVars);
            numVars += size;
            mParentVisitor.visitVarInsn(t.getOpcode(Opcodes.ILOAD), maxLocals);
            maxLocals += size;
            maxStack += size;
            paramTypes.add(t);
        }

        // Construct the descriptor of the delegate. For a static method, it's the same
        // however for an instance method we need to pass the 'this' reference first
        String desc = mDesc;
        if (!mIsStatic) {
            Type[] argTypes2 = new Type[argTypes.length + 1];

            argTypes2[0] = Type.getObjectType(mClassName);
            System.arraycopy(argTypes, 0, argTypes2, 1, argTypes.length);

            desc = Type.getMethodDescriptor(Type.getReturnType(mDesc), argTypes2);
        }

        String delegateClassName = mClassName + DELEGATE_SUFFIX;
        // 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
        mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
@@ -189,7 +228,7 @@ class DelegateMethodAdapter implements MethodVisitor {
        Type returnType = Type.getReturnType(mDesc);
        mParentVisitor.visitInsn(returnType.getOpcode(Opcodes.IRETURN));

        mParentVisitor.visitMaxs(numVars, numVars);
        mParentVisitor.visitMaxs(maxStack, maxLocals);
        mParentVisitor.visitEnd();

        // For debugging now. Maybe we should collect these and store them in
+198 −93
Original line number Diff line number Diff line
@@ -24,25 +24,38 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.android.tools.layoutlib.create.dataclass.ClassWithNative;
import com.android.tools.layoutlib.create.dataclass.OuterClass;
import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass;

import org.junit.Before;
import org.junit.Test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class DelegateClassAdapterTest {

    private MockLog mLog;

    private static final String CLASS_NAME =
        DelegateClassAdapterTest.class.getCanonicalName() + "$" +
        ClassWithNative.class.getSimpleName();
    private static final String NATIVE_CLASS_NAME = ClassWithNative.class.getCanonicalName();
    private static final String OUTER_CLASS_NAME = OuterClass.class.getCanonicalName();
    private static final String INNER_CLASS_NAME = OuterClass.class.getCanonicalName() + "$" +
                                                   InnerClass.class.getSimpleName();

    @Before
    public void setUp() throws Exception {
@@ -55,11 +68,11 @@ public class DelegateClassAdapterTest {
     */
    @SuppressWarnings("unchecked")
    @Test
    public void testNoOp() throws Exception {
    public void testNoOp() throws Throwable {
        // create an instance of the class that will be modified
        // (load the class in a distinct class loader so that we can trash its definition later)
        ClassLoader cl1 = new ClassLoader(this.getClass().getClassLoader()) { };
        Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(CLASS_NAME);
        Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(NATIVE_CLASS_NAME);
        ClassWithNative instance1 = clazz1.newInstance();
        assertEquals(42, instance1.add(20, 22));
        try {
@@ -73,20 +86,21 @@ public class DelegateClassAdapterTest {
        ClassWriter cw = new ClassWriter(0 /*flags*/);

        HashSet<String> delegateMethods = new HashSet<String>();
        String internalClassName = CLASS_NAME.replace('.', '/');
        String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
        DelegateClassAdapter cv = new DelegateClassAdapter(
                mLog, cw, internalClassName, delegateMethods);

        ClassReader cr = new ClassReader(CLASS_NAME);
        ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
        cr.accept(cv, 0 /* flags */);

        // Load the generated class in a different class loader and try it again
        final byte[] bytes = cw.toByteArray();

        ClassLoader2 cl2 = new ClassLoader2(bytes) {
        ClassLoader2 cl2 = null;
        try {
            cl2 = new ClassLoader2() {
                @Override
                public void testModifiedInstance() throws Exception {
                Class<?> clazz2 = loadClass(CLASS_NAME);
                    Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
                    Object i2 = clazz2.newInstance();
                    assertNotNull(i2);
                    assertEquals(42, callAdd(i2, 20, 22));
@@ -108,7 +122,11 @@ public class DelegateClassAdapterTest {
                    assertEquals(0, a.length);
                }
            };
            cl2.add(NATIVE_CLASS_NAME, cw);
            cl2.testModifiedInstance();
        } catch (Throwable t) {
            throw dumpGeneratedClass(t, cl2);
        }
    }

    /**
@@ -122,38 +140,37 @@ public class DelegateClassAdapterTest {
    public void testConstructorsNotSupported() throws IOException {
        ClassWriter cw = new ClassWriter(0 /*flags*/);

        String internalClassName = CLASS_NAME.replace('.', '/');
        String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');

        HashSet<String> delegateMethods = new HashSet<String>();
        delegateMethods.add("<init>");
        DelegateClassAdapter cv = new DelegateClassAdapter(
                mLog, cw, internalClassName, delegateMethods);

        ClassReader cr = new ClassReader(CLASS_NAME);
        ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
        cr.accept(cv, 0 /* flags */);
    }

    @Test
    public void testDelegateNative() throws Exception {
    public void testDelegateNative() throws Throwable {
        ClassWriter cw = new ClassWriter(0 /*flags*/);
        String internalClassName = CLASS_NAME.replace('.', '/');
        String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');

        HashSet<String> delegateMethods = new HashSet<String>();
        delegateMethods.add(DelegateClassAdapter.ALL_NATIVES);
        DelegateClassAdapter cv = new DelegateClassAdapter(
                mLog, cw, internalClassName, delegateMethods);

        ClassReader cr = new ClassReader(CLASS_NAME);
        ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
        cr.accept(cv, 0 /* flags */);

        // Load the generated class in a different class loader and try it
        final byte[] bytes = cw.toByteArray();

        ClassLoader2 cl2 = null;
        try {
            ClassLoader2 cl2 = new ClassLoader2(bytes) {
            cl2 = new ClassLoader2() {
                @Override
                public void testModifiedInstance() throws Exception {
                    Class<?> clazz2 = loadClass(CLASS_NAME);
                    Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
                    Object i2 = clazz2.newInstance();
                    assertNotNull(i2);

@@ -173,48 +190,105 @@ public class DelegateClassAdapterTest {
                     assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName());
                }
            };
            cl2.add(NATIVE_CLASS_NAME, cw);
            cl2.testModifiedInstance();
        } catch (Throwable t) {
            throw dumpGeneratedClass(t, cl2);
        }
    }

        // This code block is useful for debugging. However to make it work you need to
        // pull in the org.objectweb.asm.util.TraceClassVisitor class and associated
        // utilities which are found in the ASM source jar.
        //
        // } catch (Throwable t) {
        //     For debugging, dump the bytecode of the class in case of unexpected error.
        //     StringWriter sw = new StringWriter();
        //     PrintWriter pw = new PrintWriter(sw);
        //     TraceClassVisitor tcv = new TraceClassVisitor(pw);
        //     ClassReader cr2 = new ClassReader(bytes);
        //     cr2.accept(tcv, 0 /* flags */);
        //     String msg = "\n" + t.getClass().getCanonicalName();
        //     if (t.getMessage() != null) {
        //       msg += ": " + t.getMessage();
        //     }
        //     msg = msg + "\nBytecode dump:\n" + sw.toString();
        //  // Re-throw exception with new message
        //     RuntimeException ex = new RuntimeException(msg, t);
        //     throw ex;
        } finally {
    @Test
    public void testDelegateInner() throws Throwable {
        // We'll delegate the "get" method of both the inner and outer class.
        HashSet<String> delegateMethods = new HashSet<String>();
        delegateMethods.add("get");

        // Generate the delegate for the outer class.
        ClassWriter cwOuter = new ClassWriter(0 /*flags*/);
        String outerClassName = OUTER_CLASS_NAME.replace('.', '/');
        DelegateClassAdapter cvOuter = new DelegateClassAdapter(
                mLog, cwOuter, outerClassName, delegateMethods);
        ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
        cr.accept(cvOuter, 0 /* flags */);

        // Generate the delegate for the inner class.
        ClassWriter cwInner = new ClassWriter(0 /*flags*/);
        String innerClassName = INNER_CLASS_NAME.replace('.', '/');
        DelegateClassAdapter cvInner = new DelegateClassAdapter(
                mLog, cwInner, innerClassName, delegateMethods);
        cr = new ClassReader(INNER_CLASS_NAME);
        cr.accept(cvInner, 0 /* flags */);

        // Load the generated classes in a different class loader and try them
        ClassLoader2 cl2 = null;
        try {
            cl2 = new ClassLoader2() {
                @Override
                public void testModifiedInstance() throws Exception {

                    // Check the outer class
                    Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
                    Object o2 = outerClazz2.newInstance();
                    assertNotNull(o2);

                    // The original Outer.get returns 1+10+20,
                    // but the delegate makes it return 4+10+20
                    assertEquals(4+10+20, callGet(o2, 10, 20));

                    // Check the inner class. Since it's not a static inner class, we need
                    // to use the hidden constructor that takes the outer class as first parameter.
                    Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME);
                    Constructor<?> innerCons = innerClazz2.getConstructor(
                                                                new Class<?>[] { outerClazz2 });
                    Object i2 = innerCons.newInstance(new Object[] { o2 });
                    assertNotNull(i2);

                    // The original Inner.get returns 3+10+20,
                    // but the delegate makes it return 6+10+20
                    assertEquals(6+10+20, callGet(i2, 10, 20));
                }
            };
            cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray());
            cl2.add(INNER_CLASS_NAME, cwInner.toByteArray());
            cl2.testModifiedInstance();
        } catch (Throwable t) {
            throw dumpGeneratedClass(t, cl2);
        }
    }

    //-------

    /**
     * A class loader than can define and instantiate our dummy {@link ClassWithNative}.
     * A class loader than can define and instantiate our modified classes.
     * <p/>
     * The trick here is that this class loader will test our <em>modified</em> version
     * of the classes, the one with the delegate calls.
     * <p/>
     * The trick here is that this class loader will test our modified version of ClassWithNative.
     * Trying to do so in the original class loader generates all sort of link issues because
     * there are 2 different definitions of the same class name. This class loader will
     * define and load the class when requested by name and provide helpers to access the
     * instance methods via reflection.
     */
    private abstract class ClassLoader2 extends ClassLoader {
        private final byte[] mClassWithNative;

        public ClassLoader2(byte[] classWithNative) {
        private final Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>();

        public ClassLoader2() {
            super(null);
            mClassWithNative = classWithNative;
        }

        public ClassLoader2 add(String className, byte[] definition) {
            mClassDefs.put(className, definition);
            return this;
        }

        public ClassLoader2 add(String className, ClassWriter rewrittenClass) {
            mClassDefs.put(className, rewrittenClass.toByteArray());
            return this;
        }

        private Set<Entry<String, byte[]>> getByteCode() {
            return mClassDefs.entrySet();
        }

        @SuppressWarnings("unused")
@@ -224,9 +298,10 @@ public class DelegateClassAdapterTest {
                return super.findClass(name);
            } catch (ClassNotFoundException e) {

                if (CLASS_NAME.equals(name)) {
                byte[] def = mClassDefs.get(name);
                if (def != null) {
                    // Load the modified ClassWithNative from its bytes representation.
                    return defineClass(CLASS_NAME, mClassWithNative, 0, mClassWithNative.length);
                    return defineClass(name, def, 0, def.length);
                }

                try {
@@ -243,6 +318,17 @@ public class DelegateClassAdapterTest {
            }
        }

        /**
         * Accesses {@link OuterClass#get()} or {@link InnerClass#get() }via reflection.
         */
        public int callGet(Object instance, int a, long b) throws Exception {
            Method m = instance.getClass().getMethod("get",
                    new Class<?>[] { int.class, long.class } );

            Object result = m.invoke(instance, new Object[] { a, b });
            return ((Integer) result).intValue();
        }

        /**
         * Accesses {@link ClassWithNative#add(int, int)} via reflection.
         */
@@ -271,34 +357,53 @@ public class DelegateClassAdapterTest {
    }

    /**
     * Dummy test class with a native method.
     * The native method is not defined and any attempt to invoke it will
     * throw an {@link UnsatisfiedLinkError}.
     * For debugging, it's useful to dump the content of the generated classes
     * along with the exception that was generated.
     *
     * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor
     * class and associated utilities which are found in the ASM source jar. Since we don't
     * want that dependency in the source code, we only put it manually for development and
     * access the TraceClassVisitor via reflection if present.
     *
     * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()}
     * @param cl2 The {@link ClassLoader2} instance with the generated bytecode.
     * @return Either original {@code t} or a new wrapper {@link Throwable}
     */
    public static class ClassWithNative {
        public ClassWithNative() {
    private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) {
        try {
            // For debugging, dump the bytecode of the class in case of unexpected error
            // if we can find the TraceClassVisitor class.
            Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor");

            StringBuilder sb = new StringBuilder();
            sb.append('\n').append(t.getClass().getCanonicalName());
            if (t.getMessage() != null) {
                sb.append(": ").append(t.getMessage());
              }

        public int add(int a, int b) {
            return a + b;
        }
            for (Entry<String, byte[]> entry : cl2.getByteCode()) {
                String className = entry.getKey();
                byte[] bytes = entry.getValue();

        public int callNativeInstance(int a, double d, Object[] o) {
            return native_instance(a, d, o);
        }
                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw);
                // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw);
                Constructor<?> cons = tcvClass.getConstructor(new Class<?>[] { pw.getClass() });
                Object tcv = cons.newInstance(new Object[] { pw });
                ClassReader cr2 = new ClassReader(bytes);
                cr2.accept((ClassVisitor) tcv, 0 /* flags */);

        private native int native_instance(int a, double d, Object[] o);
                sb.append("\nBytecode dump: <").append(className).append(">:\n")
                  .append(sw.toString());
            }

    /**
     * The delegate that receives the call to {@link ClassWithNative}'s overridden methods.
     */
    public static class ClassWithNative_Delegate {
        public static int native_instance(ClassWithNative instance, int a, double d, Object[] o) {
            if (o != null && o.length > 0) {
                o[0] = instance;
            }
            return (int)(a + d);
            // Re-throw exception with new message
            RuntimeException ex = new RuntimeException(sb.toString(), t);
            return ex;
        } catch (Throwable ignore) {
            // In case of problem, just throw the original exception as-is.
            return t;
        }
    }

}
+45 −0
Original line number Diff line number Diff line
/*
 * 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.dataclass;

import com.android.tools.layoutlib.create.DelegateClassAdapterTest;

/**
 * Dummy test class with a native method.
 * The native method is not defined and any attempt to invoke it will
 * throw an {@link UnsatisfiedLinkError}.
 *
 * Used by {@link DelegateClassAdapterTest}.
 */
public class ClassWithNative {
    public ClassWithNative() {
    }

    public int add(int a, int b) {
        return a + b;
    }

    // Note: it's good to have a long or double for testing parameters since they take
    // 2 slots in the stack/locals maps.

    public int callNativeInstance(int a, double d, Object[] o) {
        return native_instance(a, d, o);
    }

    private native int native_instance(int a, double d, Object[] o);
}
+34 −0
Original line number Diff line number Diff line
/*
 * 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.dataclass;

import com.android.tools.layoutlib.create.DelegateClassAdapterTest;

/**
 * The delegate that receives the call to {@link ClassWithNative_Delegate}'s overridden methods.
 *
 * Used by {@link DelegateClassAdapterTest}.
 */
public class ClassWithNative_Delegate {
    public static int native_instance(ClassWithNative instance, int a, double d, Object[] o) {
        if (o != null && o.length > 0) {
            o[0] = instance;
        }
        return (int)(a + d);
    }
}
+48 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading