summaryrefslogtreecommitdiffstats
path: root/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java
blob: cca9e574b7ea2c28f05daa1950fa91b23be28531 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
/*
 * 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>&lt;className&gt;_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;
    /** True if the method is contained in a static inner class */
    private final boolean mIsStaticInnerClass;
    /** 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,
            boolean isStaticClass) {
        super(Opcodes.ASM4);
        mLog = log;
        mOrgWriter = mvOriginal;
        mDelWriter = mvDelegate;
        mClassName = className;
        mMethodName = methodName;
        mDesc = desc;
        mIsStatic = isStatic;
        mIsStaticInnerClass = isStaticClass;
    }

    /**
     * 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 && !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);

        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);
        }
    }

}