• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 The Bazel Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //    http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 package com.google.devtools.build.android.desugar;
15 
16 import static com.google.common.base.Preconditions.checkArgument;
17 import static com.google.common.base.Preconditions.checkNotNull;
18 import static com.google.common.base.Preconditions.checkState;
19 
20 import com.google.common.collect.ImmutableList;
21 import com.google.common.collect.ImmutableSet;
22 import com.google.devtools.build.android.desugar.io.BitFlags;
23 import java.util.HashSet;
24 import java.util.LinkedHashSet;
25 import org.objectweb.asm.AnnotationVisitor;
26 import org.objectweb.asm.ClassReader;
27 import org.objectweb.asm.ClassVisitor;
28 import org.objectweb.asm.FieldVisitor;
29 import org.objectweb.asm.MethodVisitor;
30 import org.objectweb.asm.Opcodes;
31 import org.objectweb.asm.tree.AbstractInsnNode;
32 import org.objectweb.asm.tree.MethodNode;
33 import org.objectweb.asm.tree.TypeInsnNode;
34 
35 /**
36  * Visitor intended to fix up lambda classes to match assumptions made in {@link LambdaDesugaring}.
37  * Specifically this includes fixing visibilities and generating any missing factory methods.
38  *
39  * <p>Each instance can only visit one class. This is because the signature of the needed factory
40  * method is passed into the constructor.
41  */
42 class LambdaClassFixer extends ClassVisitor {
43 
44   /** Magic method name used by {@link java.lang.invoke.LambdaMetafactory}. */
45   public static final String FACTORY_METHOD_NAME = "get$Lambda";
46   /** Field name we'll use to hold singleton instances where possible. */
47   public static final String SINGLETON_FIELD_NAME = "$instance";
48 
49   private final LambdaInfo lambdaInfo;
50   private final ClassReaderFactory factory;
51   private final ImmutableSet<String> interfaceLambdaMethods;
52   private final boolean allowDefaultMethods;
53   private final boolean copyBridgeMethods;
54   private final ClassLoader classLoader;
55   private final HashSet<String> implementedMethods = new HashSet<>();
56   private final LinkedHashSet<String> methodsToMoveIn = new LinkedHashSet<>();
57 
58   private String originalInternalName;
59   private ImmutableList<String> interfaces;
60 
61   private boolean hasState;
62   private boolean hasFactory;
63 
64   private String desc;
65   private String signature;
66 
LambdaClassFixer( ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory, ClassLoader classLoader, ImmutableSet<String> interfaceLambdaMethods, boolean allowDefaultMethods, boolean copyBridgeMethods)67   public LambdaClassFixer(
68       ClassVisitor dest,
69       LambdaInfo lambdaInfo,
70       ClassReaderFactory factory,
71       ClassLoader classLoader,
72       ImmutableSet<String> interfaceLambdaMethods,
73       boolean allowDefaultMethods,
74       boolean copyBridgeMethods) {
75     super(Opcodes.ASM6, dest);
76     checkArgument(!allowDefaultMethods || interfaceLambdaMethods.isEmpty());
77     checkArgument(allowDefaultMethods || copyBridgeMethods);
78     this.lambdaInfo = lambdaInfo;
79     this.factory = factory;
80     this.classLoader = classLoader;
81     this.interfaceLambdaMethods = interfaceLambdaMethods;
82     this.allowDefaultMethods = allowDefaultMethods;
83     this.copyBridgeMethods = copyBridgeMethods;
84   }
85 
86   @Override
visit( int version, int access, String name, String signature, String superName, String[] interfaces)87   public void visit(
88       int version,
89       int access,
90       String name,
91       String signature,
92       String superName,
93       String[] interfaces) {
94     checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE), "Not a class: %s", name);
95     checkState(this.originalInternalName == null, "not intended for reuse but reused for %s", name);
96     originalInternalName = name;
97     hasState = false;
98     hasFactory = false;
99     desc = null;
100     this.signature = null;
101     this.interfaces = ImmutableList.copyOf(interfaces);
102     // Rename to desired name
103     super.visit(version, access, getInternalName(), signature, superName, interfaces);
104   }
105 
106   @Override
visitField( int access, String name, String desc, String signature, Object value)107   public FieldVisitor visitField(
108       int access, String name, String desc, String signature, Object value) {
109     hasState = true;
110     return super.visitField(access, name, desc, signature, value);
111   }
112 
113   @Override
visitMethod( int access, String name, String desc, String signature, String[] exceptions)114   public MethodVisitor visitMethod(
115       int access, String name, String desc, String signature, String[] exceptions) {
116     if (name.equals("writeReplace")
117         && BitFlags.noneSet(access, Opcodes.ACC_STATIC)
118         && desc.equals("()Ljava/lang/Object;")) {
119       // Lambda serialization hooks use java/lang/invoke/SerializedLambda, which isn't available on
120       // Android. Since Jack doesn't do anything special for serializable lambdas we just drop these
121       // serialization hooks.
122       // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/output.html#a5324 gives
123       // details on the role and signature of this method.
124       return null;
125     }
126     if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)) {
127       // Keep track of instance methods implemented in this class for later.  Since this visitor
128       // is intended for lambda classes, no need to look at the superclass.
129       implementedMethods.add(name + ":" + desc);
130     }
131     if (FACTORY_METHOD_NAME.equals(name)) {
132       hasFactory = true;
133       if (!lambdaInfo.needFactory()) {
134         return null; // drop generated factory method if we won't call it
135       }
136       access &= ~Opcodes.ACC_PRIVATE; // make factory method accessible
137     } else if ("<init>".equals(name)) {
138       this.desc = desc;
139       this.signature = signature;
140       if (!lambdaInfo.needFactory() && !desc.startsWith("()")) {
141         access &= ~Opcodes.ACC_PRIVATE; // make constructor accessible if we'll call it directly
142       }
143     }
144     MethodVisitor methodVisitor =
145         new LambdaClassMethodRewriter(super.visitMethod(access, name, desc, signature, exceptions));
146     if (!lambdaInfo.bridgeMethod().equals(lambdaInfo.methodReference())) {
147       // Skip UseBridgeMethod unless we actually need it
148       methodVisitor =
149           new UseBridgeMethod(
150               methodVisitor, lambdaInfo, classLoader, access, name, desc, signature, exceptions);
151     }
152     if (!FACTORY_METHOD_NAME.equals(name) && !"<init>".equals(name)) {
153       methodVisitor = new LambdaClassInvokeSpecialRewriter(methodVisitor);
154     }
155     return methodVisitor;
156   }
157 
158   @Override
visitEnd()159   public void visitEnd() {
160     checkState(
161         !hasState || hasFactory,
162         "Expected factory method for capturing lambda %s",
163         getInternalName());
164     if (!hasState) {
165       checkState(
166           signature == null,
167           "Didn't expect generic constructor signature %s %s",
168           getInternalName(),
169           signature);
170       checkState(
171           lambdaInfo.factoryMethodDesc().startsWith("()"),
172           "Expected 0-arg factory method for %s but found %s",
173           getInternalName(),
174           lambdaInfo.factoryMethodDesc());
175       // Since this is a stateless class we populate and use a static singleton field "$instance".
176       // Field is package-private so we can read it from the class that had the invokedynamic.
177       String singletonFieldDesc = lambdaInfo.factoryMethodDesc().substring("()".length());
178       super.visitField(
179               Opcodes.ACC_STATIC | Opcodes.ACC_FINAL,
180               SINGLETON_FIELD_NAME,
181               singletonFieldDesc,
182               (String) null,
183               (Object) null)
184           .visitEnd();
185 
186       MethodVisitor codeBuilder =
187           super.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", (String) null, new String[0]);
188       codeBuilder.visitTypeInsn(Opcodes.NEW, getInternalName());
189       codeBuilder.visitInsn(Opcodes.DUP);
190       codeBuilder.visitMethodInsn(
191           Opcodes.INVOKESPECIAL,
192           getInternalName(),
193           "<init>",
194           checkNotNull(desc, "didn't see a constructor for %s", getInternalName()),
195           /*itf=*/ false);
196       codeBuilder.visitFieldInsn(
197           Opcodes.PUTSTATIC, getInternalName(), SINGLETON_FIELD_NAME, singletonFieldDesc);
198       codeBuilder.visitInsn(Opcodes.RETURN);
199       codeBuilder.visitMaxs(2, 0); // two values are pushed onto the stack
200       codeBuilder.visitEnd();
201     }
202 
203     copyRewrittenLambdaMethods();
204     if (copyBridgeMethods) {
205       copyBridgeMethods(interfaces);
206     }
207     super.visitEnd();
208   }
209 
getInternalName()210   private String getInternalName() {
211     return lambdaInfo.desiredInternalName();
212   }
213 
copyRewrittenLambdaMethods()214   private void copyRewrittenLambdaMethods() {
215     for (String rewritten : methodsToMoveIn) {
216       String interfaceInternalName = rewritten.substring(0, rewritten.indexOf('#'));
217       String methodName = rewritten.substring(interfaceInternalName.length() + 1);
218       ClassReader bytecode =
219           checkNotNull(
220               factory.readIfKnown(interfaceInternalName),
221               "Couldn't load interface with lambda method %s",
222               rewritten);
223       CopyOneMethod copier = new CopyOneMethod(methodName);
224       // TODO(kmb): Set source file attribute for lambda classes so lambda debug info makes sense
225       bytecode.accept(copier, ClassReader.SKIP_DEBUG);
226       checkState(copier.copied(), "Didn't find %s", rewritten);
227     }
228   }
229 
copyBridgeMethods(ImmutableList<String> interfaces)230   private void copyBridgeMethods(ImmutableList<String> interfaces) {
231     for (String implemented : interfaces) {
232       ClassReader bytecode = factory.readIfKnown(implemented);
233       if (bytecode != null) {
234         // Don't copy line numbers and local variable tables.  They would be misleading or wrong
235         // and other methods in generated lambda classes don't have debug info either.
236         bytecode.accept(new CopyBridgeMethods(), ClassReader.SKIP_DEBUG);
237       } // else the interface is defined in a different Jar, which we can ignore here
238     }
239   }
240 
241   /** Rewriter for methods in generated lambda classes. */
242   private class LambdaClassMethodRewriter extends MethodVisitor {
LambdaClassMethodRewriter(MethodVisitor dest)243     public LambdaClassMethodRewriter(MethodVisitor dest) {
244       super(Opcodes.ASM6, dest);
245     }
246 
247     @Override
visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)248     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
249       String method = owner + "#" + name;
250       if (interfaceLambdaMethods.contains(method)) {
251         // Rewrite invocations of lambda methods in interfaces to anticipate the lambda method being
252         // moved into the lambda class (i.e., the class being visited here).
253         checkArgument(opcode == Opcodes.INVOKESTATIC, "Cannot move instance method %s", method);
254         owner = getInternalName();
255         itf = false; // owner was interface but is now a class
256         methodsToMoveIn.add(method);
257       } else if (originalInternalName.equals(owner)) {
258         // Reflect renaming of lambda classes
259         owner = getInternalName();
260       }
261 
262       if (name.startsWith("lambda$")) {
263         // Reflect renaming of lambda$ instance methods in LambdaDesugaring.  Do this even if we'll
264         // move the method into the lambda class we're processing so the renaming done in
265         // LambdaDesugaring doesn't kick in if the class were desugared a second time.
266         name = LambdaDesugaring.uniqueInPackage(owner, name);
267       }
268       super.visitMethodInsn(opcode, owner, name, desc, itf);
269     }
270 
271     @Override
visitTypeInsn(int opcode, String type)272     public void visitTypeInsn(int opcode, String type) {
273       if (originalInternalName.equals(type)) {
274         // Reflect renaming of lambda classes
275         type = getInternalName();
276       }
277       super.visitTypeInsn(opcode, type);
278     }
279 
280     @Override
visitFieldInsn(int opcode, String owner, String name, String desc)281     public void visitFieldInsn(int opcode, String owner, String name, String desc) {
282       if (originalInternalName.equals(owner)) {
283         // Reflect renaming of lambda classes
284         owner = getInternalName();
285       }
286       super.visitFieldInsn(opcode, owner, name, desc);
287     }
288 
289     @Override
visitAnnotation(String desc, boolean visible)290     public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
291       // Drop annotation that's part of the generated lambda class that's not available on Android.
292       // Proguard complains about this otherwise.
293       if ("Ljava/lang/invoke/LambdaForm$Hidden;".equals(desc)) {
294         return null;
295       }
296       return super.visitAnnotation(desc, visible);
297     }
298   }
299 
300   /** Rewriter for invokespecial in generated lambda classes. */
301   private static class LambdaClassInvokeSpecialRewriter extends MethodVisitor {
302 
LambdaClassInvokeSpecialRewriter(MethodVisitor dest)303     public LambdaClassInvokeSpecialRewriter(MethodVisitor dest) {
304       super(Opcodes.ASM6, dest);
305     }
306 
307     @Override
visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)308     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
309       if (opcode == Opcodes.INVOKESPECIAL && name.startsWith("lambda$")) {
310         opcode = itf ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL;
311       }
312 
313       super.visitMethodInsn(opcode, owner, name, desc, itf);
314     }
315   }
316 
317   /**
318    * Visitor that copies bridge methods from the visited interface into the class visited by the
319    * surrounding {@link LambdaClassFixer}. Descends recursively into interfaces extended by the
320    * visited interface.
321    */
322   private class CopyBridgeMethods extends ClassVisitor {
323 
324     @SuppressWarnings("hiding")
325     private ImmutableList<String> interfaces;
326 
CopyBridgeMethods()327     public CopyBridgeMethods() {
328       // No delegate visitor; instead we'll add methods to the outer class's delegate where needed
329       super(Opcodes.ASM6);
330     }
331 
332     @Override
visit( int version, int access, String name, String signature, String superName, String[] interfaces)333     public void visit(
334         int version,
335         int access,
336         String name,
337         String signature,
338         String superName,
339         String[] interfaces) {
340       checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
341       checkState(this.interfaces == null);
342       this.interfaces = ImmutableList.copyOf(interfaces);
343     }
344 
345     @Override
visitMethod( int access, String name, String desc, String signature, String[] exceptions)346     public MethodVisitor visitMethod(
347         int access, String name, String desc, String signature, String[] exceptions) {
348       if ((access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC))
349           == Opcodes.ACC_BRIDGE) {
350         // Only copy bridge methods--hand-written default methods are not supported--and only if
351         // we haven't seen the method already.
352         if (implementedMethods.add(name + ":" + desc)) {
353           MethodVisitor result =
354               LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions);
355           return allowDefaultMethods ? result : new AvoidJacocoInit(result);
356         }
357       }
358       return null;
359     }
360 
361     @Override
visitEnd()362     public void visitEnd() {
363       copyBridgeMethods(this.interfaces);
364     }
365   }
366 
367   private class CopyOneMethod extends ClassVisitor {
368 
369     private final String methodName;
370     private int copied = 0;
371 
CopyOneMethod(String methodName)372     public CopyOneMethod(String methodName) {
373       // No delegate visitor; instead we'll add methods to the outer class's delegate where needed
374       super(Opcodes.ASM6);
375       checkState(!allowDefaultMethods, "Couldn't copy interface lambda bodies");
376       this.methodName = methodName;
377     }
378 
copied()379     public boolean copied() {
380       return copied > 0;
381     }
382 
383     @Override
visit( int version, int access, String name, String signature, String superName, String[] interfaces)384     public void visit(
385         int version,
386         int access,
387         String name,
388         String signature,
389         String superName,
390         String[] interfaces) {
391       checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
392     }
393 
394     @Override
visitMethod( int access, String name, String desc, String signature, String[] exceptions)395     public MethodVisitor visitMethod(
396         int access, String name, String desc, String signature, String[] exceptions) {
397       if (name.equals(methodName)) {
398         checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc);
399         ++copied;
400         // Rename for consistency with what we do in LambdaClassMethodRewriter
401         name = LambdaDesugaring.uniqueInPackage(getInternalName(), name);
402         return new AvoidJacocoInit(
403             LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions));
404       }
405       return null;
406     }
407   }
408 
409   /**
410    * Method visitor that rewrites {@code $jacocoInit()} calls to equivalent field accesses.
411    *
412    * <p>This class should only be used to visit interface methods and assumes that the code in
413    * {@code $jacocoInit()} is always executed in the interface's static initializer, which is the
414    * case in the absence of hand-written static or default interface methods (which {@link
415    * Java7Compatibility} makes sure of).
416    */
417   private static class AvoidJacocoInit extends MethodVisitor {
AvoidJacocoInit(MethodVisitor dest)418     public AvoidJacocoInit(MethodVisitor dest) {
419       super(Opcodes.ASM6, dest);
420     }
421 
422     @Override
visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)423     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
424       if (opcode == Opcodes.INVOKESTATIC && "$jacocoInit".equals(name)) {
425         // Rewrite $jacocoInit() calls to just read the $jacocoData field
426         super.visitFieldInsn(Opcodes.GETSTATIC, owner, "$jacocoData", "[Z");
427       } else {
428         super.visitMethodInsn(opcode, owner, name, desc, itf);
429       }
430     }
431   }
432 
433   private static class UseBridgeMethod extends MethodNode {
434 
435     private final MethodVisitor dest;
436     private final LambdaInfo lambdaInfo;
437     private final ClassLoader classLoader;
438 
UseBridgeMethod( MethodVisitor dest, LambdaInfo lambdaInfo, ClassLoader classLoader, int access, String name, String desc, String signature, String[] exceptions)439     public UseBridgeMethod(
440         MethodVisitor dest,
441         LambdaInfo lambdaInfo,
442         ClassLoader classLoader,
443         int access,
444         String name,
445         String desc,
446         String signature,
447         String[] exceptions) {
448       super(Opcodes.ASM6, access, name, desc, signature, exceptions);
449       this.dest = dest;
450       this.lambdaInfo = lambdaInfo;
451       this.classLoader = classLoader;
452       checkArgument(
453           !lambdaInfo.methodReference().equals(lambdaInfo.bridgeMethod()),
454           "This class only works for a lambda that has a bridge method. lambdaInfo=%s, bridge=%s",
455           lambdaInfo.methodReference(),
456           lambdaInfo.bridgeMethod());
457     }
458 
459     @Override
visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)460     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
461       if (!name.equals(lambdaInfo.methodReference().getName())
462           || !desc.equals(lambdaInfo.methodReference().getDesc())) {
463         super.visitMethodInsn(opcode, owner, name, desc, itf);
464         return;
465       }
466 
467       boolean useBridgeMethod = false;
468       if (owner.equals(lambdaInfo.methodReference().getOwner())) {
469         if (lambdaInfo.methodReference().getTag() == Opcodes.H_NEWINVOKESPECIAL
470             && lambdaInfo.bridgeMethod().getTag() != Opcodes.H_NEWINVOKESPECIAL) {
471           // We're changing a constructor call to a factory method call, so we unfortunately need
472           // to go find the NEW/DUP pair preceding the constructor call and remove it
473           removeLastAllocation();
474         }
475         useBridgeMethod = true;
476       } else if ((lambdaInfo.methodReference().getTag() == Opcodes.H_INVOKEVIRTUAL
477               || lambdaInfo.methodReference().getTag() == Opcodes.H_INVOKESPECIAL)
478           && hasAssignableRelation(owner, lambdaInfo.methodReference().getOwner())) {
479         // For rewriting instance methods calls, we consider the class hierarchy.
480         // This is for JDK 9: (b/62218600).
481         // TODO(cnsun): revisit this to make sure Desugar is fully compatible with this change
482         // in JDK: http://hg.openjdk.java.net/jdk9/dev/jdk/rev/a3b3c7b6464d
483         useBridgeMethod = true;
484       }
485       if (useBridgeMethod) {
486         super.visitMethodInsn(
487             LambdaDesugaring.invokeOpcode(lambdaInfo.bridgeMethod()),
488             lambdaInfo.bridgeMethod().getOwner(),
489             lambdaInfo.bridgeMethod().getName(),
490             lambdaInfo.bridgeMethod().getDesc(),
491             lambdaInfo.bridgeMethod().isInterface());
492       } else {
493         super.visitMethodInsn(opcode, owner, name, desc, itf);
494       }
495     }
496 
removeLastAllocation()497     private void removeLastAllocation() {
498       AbstractInsnNode insn = instructions.getLast();
499       while (insn != null && insn.getPrevious() != null) {
500         AbstractInsnNode prev = insn.getPrevious();
501         if (prev.getOpcode() == Opcodes.NEW
502             && insn.getOpcode() == Opcodes.DUP
503             && ((TypeInsnNode) prev).desc.equals(lambdaInfo.methodReference().getOwner())) {
504           instructions.remove(prev);
505           instructions.remove(insn);
506           return;
507         }
508         insn = prev;
509       }
510       throw new IllegalStateException(
511           "Couldn't find allocation to rewrite ::new reference " + lambdaInfo.methodReference());
512     }
513 
hasAssignableRelation(String ownerOfMethodInsn, String ownerOfMethodReference)514     private boolean hasAssignableRelation(String ownerOfMethodInsn, String ownerOfMethodReference) {
515       try {
516         Class<?> methodInsnOwnerClass = classLoader.loadClass(ownerOfMethodInsn.replace('/', '.'));
517         Class<?> methodReferenceOwnerClass =
518             classLoader.loadClass(ownerOfMethodReference.replace('/', '.'));
519         return methodInsnOwnerClass.isAssignableFrom(methodReferenceOwnerClass)
520             || methodReferenceOwnerClass.isAssignableFrom(methodInsnOwnerClass);
521       } catch (ClassNotFoundException e) {
522         throw new IllegalStateException(
523             "Failed to load method owners for inserting bridge method: " + lambdaInfo, e);
524       }
525     }
526 
527     @Override
visitEnd()528     public void visitEnd() {
529       accept(dest);
530     }
531   }
532 }
533