Avoid factory methods when desugaring lambda expressions and this:: method references for android RELNOTES: no factory methods generated for lambda expressions on android -- PiperOrigin-RevId: 150952237 MOS_MIGRATED_REVID=150952237
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java index e942c95..dea6339 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
@@ -117,10 +117,16 @@ } if (FACTORY_METHOD_NAME.equals(name)) { hasFactory = true; + if (!lambdaInfo.needFactory()) { + return null; // drop generated factory method if we won't call it + } access &= ~Opcodes.ACC_PRIVATE; // make factory method accessible } else if ("<init>".equals(name)) { this.desc = desc; this.signature = signature; + if (!lambdaInfo.needFactory() && !desc.startsWith("()")) { + access &= ~Opcodes.ACC_PRIVATE; // make constructor accessible if we'll call it directly + } } MethodVisitor methodVisitor = new LambdaClassMethodRewriter(super.visitMethod(access, name, desc, signature, exceptions));
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java index c808831..be06e37 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
@@ -39,6 +39,11 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; /** * Visitor that desugars classes with uses of lambdas into Java 7-looking code. This includes @@ -167,7 +172,9 @@ name = uniqueInPackage(internalName, name); } MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions); - return new InvokedynamicRewriter(dest); + return dest != null + ? new InvokedynamicRewriter(dest, access, name, desc, signature, exceptions) + : null; } @Override @@ -328,10 +335,19 @@ * Desugaring that replaces invokedynamics for {@link java.lang.invoke.LambdaMetafactory} with * static factory method invocations and triggers a class to be generated for each invokedynamic. */ - private class InvokedynamicRewriter extends MethodVisitor { + private class InvokedynamicRewriter extends MethodNode { - public InvokedynamicRewriter(MethodVisitor dest) { - super(ASM5, dest); + private final MethodVisitor dest; + + public InvokedynamicRewriter(MethodVisitor dest, + int access, String name, String desc, String signature, String[] exceptions) { + super(ASM5, access, name, desc, signature, exceptions); + this.dest = checkNotNull(dest, "Null destination for %s.%s : %s", internalName, name, desc); + } + + @Override + public void visitEnd() { + accept(dest); } @Override @@ -365,24 +381,42 @@ // Give generated classes to have more stable names (b/35643761). Use BSM's naming scheme // but with separate counter for each surrounding class. String lambdaClassName = internalName + "$$Lambda$" + (lambdaCount++); + Type[] capturedTypes = Type.getArgumentTypes(desc); + boolean needFactory = capturedTypes.length != 0 + && !attemptAllocationBeforeArgumentLoads(lambdaClassName, capturedTypes); lambdas.generateLambdaClass( internalName, LambdaInfo.create( - lambdaClassName, desc, bridgeInfo.methodReference(), bridgeInfo.bridgeMethod()), + lambdaClassName, + desc, + needFactory, + bridgeInfo.methodReference(), + bridgeInfo.bridgeMethod()), bsmMethod, args); if (desc.startsWith("()")) { // For stateless lambda classes we'll generate a singleton instance that we can just load + checkState(capturedTypes.length == 0); super.visitFieldInsn(Opcodes.GETSTATIC, lambdaClassName, LambdaClassFixer.SINGLETON_FIELD_NAME, desc.substring("()".length())); - } else { - // Emit invokestatic that calls the factory method generated in the lambda class + } else if (needFactory) { + // If we were unable to inline the allocation of the generated lambda class then + // invoke factory method of generated lambda class with the arguments on the stack super.visitMethodInsn( Opcodes.INVOKESTATIC, lambdaClassName, LambdaClassFixer.FACTORY_METHOD_NAME, desc, /*itf*/ false); + } else { + // Otherwise we inserted a new/dup pair of instructions above and now just need to invoke + // the constructor of generated lambda class with the arguments on the stack + super.visitMethodInsn( + Opcodes.INVOKESPECIAL, + lambdaClassName, + "<init>", + Type.getMethodDescriptor(Type.VOID_TYPE, capturedTypes), + /*itf*/ false); } } catch (IOException | ReflectiveOperationException e) { throw new IllegalStateException("Couldn't desugar invokedynamic for " + internalName + "." @@ -390,6 +424,76 @@ } } + /** + * Tries to insert a new/dup for the given class name before expected existing instructions that + * set up arguments for an invokedynamic factory method with the given types. + * + * <p>For lambda expressions and simple method references we can assume that arguments are set + * up with loads of the captured (effectively) final variables. But method references, can in + * general capture an expression, such as in {@code myObject.toString()::charAt} (a {@code + * Function<Integer, Character>}), which can also cause null checks to be inserted. In + * such more complicated cases this method may fail to insert a new/dup pair and returns {@code + * false}. + * + * @param internalName internal name of the class to instantiate + * @param paramTypes expected invokedynamic argument types, which also must be the parameters of + * {@code internalName}'s constructor. + * @return {@code true} if we were able to insert a new/dup, {@code false} otherwise + */ + private boolean attemptAllocationBeforeArgumentLoads(String internalName, Type[] paramTypes) { + checkArgument(paramTypes.length > 0, "Expected at least one param for %s", internalName); + // Walk backwards past loads corresponding to constructor arguments to find the instruction + // after which we need to insert our NEW/DUP pair + AbstractInsnNode insn = instructions.getLast(); + for (int i = paramTypes.length - 1; 0 <= i; --i) { + if (insn.getOpcode() == Opcodes.GETFIELD) { + // Lambdas in anonymous inner classes have to load outer scope variables from fields, + // which manifest as an ALOAD followed by one or more GETFIELDs + FieldInsnNode getfield = (FieldInsnNode) insn; + checkState( + getfield.desc.length() == 1 + ? getfield.desc.equals(paramTypes[i].getDescriptor()) + : paramTypes[i].getDescriptor().length() > 1, + "Expected getfield for %s to set up parameter %s for %s but got %s : %s", + paramTypes[i], i, internalName, getfield.name, getfield.desc); + insn = insn.getPrevious(); + + while (insn.getOpcode() == Opcodes.GETFIELD) { + // Nested inner classes can cause a cascade of getfields from the outermost one inwards + checkState(((FieldInsnNode) insn).desc.startsWith("L"), + "expect object type getfields to get to %s to set up parameter %s for %s, not: %s", + paramTypes[i], i, internalName, ((FieldInsnNode) insn).desc); + insn = insn.getPrevious(); + } + + checkState(insn.getOpcode() == Opcodes.ALOAD, // should be a this pointer to be precise + "Expected aload before getfield for %s to set up parameter %s for %s but got %s", + getfield.name, i, internalName, insn.getOpcode()); + } else if (insn.getOpcode() != paramTypes[i].getOpcode(Opcodes.ILOAD)) { + // Otherwise expect load of a (effectively) final local variable. Not seeing that means + // we're dealing with a method reference on some arbitrary expression, <expression>::m. + // In that case we give up and keep using the factory method for now, since inserting + // the NEW/DUP so the new object ends up in the right stack slot is hard in that case. + // Note this still covers simple cases such as this::m or x::m, where x is a local. + checkState(paramTypes.length == 1, + "Expected a load for %s to set up parameter %s for %s but got %s", + paramTypes[i], i, internalName, insn.getOpcode()); + return false; + } + insn = insn.getPrevious(); + } + + TypeInsnNode newInsn = new TypeInsnNode(Opcodes.NEW, internalName); + if (insn == null) { + // Ran off the front of the instruction list + instructions.insert(newInsn); + } else { + instructions.insert(insn, newInsn); + } + instructions.insert(newInsn, new InsnNode(Opcodes.DUP)); + return true; + } + private Lookup createLookup(String lookupClass) throws ReflectiveOperationException { Class<?> clazz = loadFromInternal(lookupClass); Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class);
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java index dad340c..b7e7a3c 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java
@@ -13,6 +13,8 @@ // limitations under the License. package com.google.devtools.build.android.desugar; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.auto.value.AutoValue; import org.objectweb.asm.Handle; @@ -21,14 +23,19 @@ public static LambdaInfo create( String desiredInternalName, String factoryMethodDesc, + boolean needFactory, Handle methodReference, Handle bridgeMethod) { + checkArgument(!needFactory || !factoryMethodDesc.startsWith("()"), + "Shouldn't need a factory method for %s : %s", desiredInternalName, factoryMethodDesc); return new AutoValue_LambdaInfo( - desiredInternalName, factoryMethodDesc, methodReference, bridgeMethod); + desiredInternalName, factoryMethodDesc, needFactory, methodReference, bridgeMethod); } public abstract String desiredInternalName(); public abstract String factoryMethodDesc(); + /** Returns {@code true} if we need the generated class to have a factory method. */ + public abstract boolean needFactory(); public abstract Handle methodReference(); public abstract Handle bridgeMethod(); }