Skip to content

Commit

Permalink
Fixed incompatibility with JDK 8 build b121, fixes #3
Browse files Browse the repository at this point in the history
There were multiple breaking changes at play:

- The ClassFileTransformer.transform method now gets className=null as parameter for lambda classes. The class name must be parsed from bytecode.

- The naming pattern of lambda implementation methods was changed from lambda$1 to lambda$invokingMethod$1

- The lambda class now calls private instance methods (i.e. lambda accesses the invoker's instance variables) using the invokespecial instruction, to avoid a subclass accidentally overriding the generated lambda implementation method. But using invokespecial to call methods of other classes is not allowed in bytecode (see the spec http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokespecial) and lambdas can get away with it only because JDK 8 relaxes the verification of lambda classes. Our workaround is to change the opcode to invokevirtual at the risk of calling a malicious subclass method.
  • Loading branch information
luontola committed Jan 7, 2014
1 parent 0a984bc commit fe772bd
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 21 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,19 @@ May break if a future JDK 8 build stops generating a new class for each
that `java.lang.invoke.LambdaMetafactory` generates dynamically, so
optimizations to that mechanism may break Retrolambda.

Does not implement JDK 8's safety measure of using `invokespecial` to call
lambda implementation methods that are private instance methods (i.e. when
the lambda accesses an instance variable).


Version History
---------------

### Upcoming

- Updated to work with JDK 8 Early Access Build b121 (2013-12-19)
([Issue #3](https://github.com/orfjackal/retrolambda/issues/3))

### Retrolambda 1.1.1 (2013-11-27)

- Show help if the `-javaagent` parameter is missing
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2013 Esko Luontola <www.orfjackal.net>
// Copyright © 2013-2014 Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

Expand All @@ -11,8 +11,6 @@
public class LambdaClassBackporter {

private static final String SINGLETON_FIELD_NAME = "instance";

private static final String MAGIC_LAMBDA_IMPL = "java/lang/invoke/MagicLambdaImpl";
private static final String JAVA_LANG_OBJECT = "java/lang/Object";

public static byte[] transform(byte[] bytecode, int targetVersion) {
Expand Down Expand Up @@ -41,7 +39,7 @@ public void visit(int version, int access, String name, String signature, String
if (version > targetVersion) {
version = targetVersion;
}
if (superName.equals(MAGIC_LAMBDA_IMPL)) {
if (superName.equals(LambdaNaming.MAGIC_LAMBDA_IMPL)) {
superName = JAVA_LANG_OBJECT;
}
super.visit(version, access, name, signature, superName, interfaces);
Expand All @@ -53,7 +51,9 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si
constructor = Type.getMethodType(desc);
}
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
return new MagicLambdaRemovingMethodVisitor(mv);
mv = new MagicLambdaRemovingMethodVisitor(mv);
mv = new PrivateMethodInvocationFixingMethodVisitor(mv, factoryMethod.getInvoker());
return mv;
}

@Override
Expand Down Expand Up @@ -124,12 +124,32 @@ public MagicLambdaRemovingMethodVisitor(MethodVisitor mv) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
if (opcode == INVOKESPECIAL
&& owner.equals(MAGIC_LAMBDA_IMPL)
&& owner.equals(LambdaNaming.MAGIC_LAMBDA_IMPL)
&& name.equals("<init>")
&& desc.equals("()V")) {
owner = JAVA_LANG_OBJECT;
}
super.visitMethodInsn(opcode, owner, name, desc);
}
}

private static class PrivateMethodInvocationFixingMethodVisitor extends MethodVisitor {

private final String invoker;

public PrivateMethodInvocationFixingMethodVisitor(MethodVisitor mv, Class<?> invoker) {
super(ASM4, mv);
this.invoker = Type.getInternalName(invoker);
}

@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
if (opcode == INVOKESPECIAL
&& owner.equals(invoker)
&& LambdaNaming.LAMBDA_IMPL_METHOD.matcher(name).matches()) {
opcode = INVOKEVIRTUAL;
}
super.visitMethodInsn(opcode, owner, name, desc);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2013 Esko Luontola <www.orfjackal.net>
// Copyright © 2013-2014 Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

Expand All @@ -10,10 +10,12 @@ public class LambdaFactoryMethod {

private final String owner;
private final String desc;
private final Class<?> invoker;

public LambdaFactoryMethod(String lambdaClass, Type invokedType) {
public LambdaFactoryMethod(String lambdaClass, Type invokedType, Class<?> invoker) {
owner = lambdaClass;
desc = invokedType.getDescriptor();
this.invoker = invoker;
}

public String getOwner() {
Expand All @@ -27,4 +29,8 @@ public String getName() {
public String getDesc() {
return desc;
}

public Class<?> getInvoker() {
return invoker;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright © 2013-2014 Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

package net.orfjackal.retrolambda;

import java.util.regex.Pattern;

public class LambdaNaming {

public static final String LAMBDA_METAFACTORY = "java/lang/invoke/LambdaMetafactory";
public static final String MAGIC_LAMBDA_IMPL = "java/lang/invoke/MagicLambdaImpl";
public static final Pattern LAMBDA_CLASS = Pattern.compile("^.+\\$\\$Lambda\\$\\d+$");
public static final Pattern LAMBDA_IMPL_METHOD = Pattern.compile("^lambda\\$.*\\$\\d+$");
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2013 Esko Luontola <www.orfjackal.net>
// Copyright © 2013-2014 Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

Expand All @@ -19,11 +19,13 @@ public class LambdaReifier {
// is spying on the LambdaMetafactory's dynamically generated bytecode.
// We expect only one class being processed at a time, so it should
// be an error if these collections contain more than one element.
private static final BlockingDeque<Class<?>> currentInvoker = new LinkedBlockingDeque<>(1);
private static final BlockingDeque<Type> currentInvokedType = new LinkedBlockingDeque<>(1);
private static final BlockingDeque<String> currentLambdaClass = new LinkedBlockingDeque<>(1);

public static LambdaFactoryMethod reifyLambdaClass(Class<?> invoker, String invokedName, Type invokedType, Handle bsm, Object[] bsmArgs) {
try {
setInvoker(invoker);
setInvokedType(invokedType);
callBootstrapMethod(invoker, invokedName, invokedType, bsm, bsmArgs);
return getLambdaFactoryMethod();
Expand All @@ -35,6 +37,10 @@ public static LambdaFactoryMethod reifyLambdaClass(Class<?> invoker, String invo
}
}

public static void setInvoker(Class<?> invoker) {
currentInvoker.push(invoker);
}

private static void setInvokedType(Type invokedType) {
currentInvokedType.push(invokedType);
}
Expand All @@ -46,10 +52,12 @@ public static void setLambdaClass(String lambdaClass) {
public static LambdaFactoryMethod getLambdaFactoryMethod() {
String lambdaClass = currentLambdaClass.getFirst();
Type invokedType = currentInvokedType.getFirst();
return new LambdaFactoryMethod(lambdaClass, invokedType);
Class<?> invoker = currentInvoker.getFirst();
return new LambdaFactoryMethod(lambdaClass, invokedType, invoker);
}

private static void resetGlobals() {
currentInvoker.clear();
currentInvokedType.clear();
currentLambdaClass.clear();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
// Copyright © 2013 Esko Luontola <www.orfjackal.net>
// Copyright © 2013-2014 Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

package net.orfjackal.retrolambda;

import org.objectweb.asm.ClassReader;

import java.io.IOException;
import java.lang.instrument.*;
import java.nio.file.*;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.regex.Pattern;

public class LambdaSavingClassFileTransformer implements ClassFileTransformer {

private static final Pattern LAMBDA_CLASS = Pattern.compile("^.+\\$\\$Lambda\\$\\d+$");

private final Path outputDir;
private final int targetVersion;
private final List<ClassLoader> ignoredClassLoaders = new ArrayList<>();
Expand All @@ -35,6 +34,11 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
// The transformed application classes have their own class loader.
return null;
}
if (className == null) {
// Since JDK 8 build b121 or so, lambda classes have a null class name,
// but we can read it from the bytecode where the name still exists.
className = new ClassReader(classfileBuffer).getClassName();
}
if (!isLambdaClass(className)) {
return null;
}
Expand All @@ -52,6 +56,6 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
}

private static boolean isLambdaClass(String className) {
return LAMBDA_CLASS.matcher(className).matches();
return LambdaNaming.LAMBDA_CLASS.matcher(className).matches();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import static org.objectweb.asm.Opcodes.*;

public class LambdaUsageBackporter {

private static final String LAMBDA_METAFACTORY = "java/lang/invoke/LambdaMetafactory";
private static final Pattern LAMBDA_IMPL_METHOD = Pattern.compile("^lambda\\$\\d+$");

public static byte[] transform(byte[] bytecode, int targetVersion) {
resetLambdaClassSequenceNumber();
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
Expand Down Expand Up @@ -58,7 +54,7 @@ public void visit(int version, int access, String name, String signature, String

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (LAMBDA_IMPL_METHOD.matcher(name).matches()) {
if (LambdaNaming.LAMBDA_IMPL_METHOD.matcher(name).matches()) {
access = Flags.makeNonPrivate(access);
}
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
Expand All @@ -76,7 +72,7 @@ public InvokeDynamicInsnConvertingMethodVisitor(int api, MethodVisitor mv, Strin

@Override
public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
if (bsm.getOwner().equals(LAMBDA_METAFACTORY)) {
if (bsm.getOwner().equals(LambdaNaming.LAMBDA_METAFACTORY)) {
backportLambda(name, Type.getType(desc), bsm, bsmArgs);
} else {
super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
Expand Down

0 comments on commit fe772bd

Please sign in to comment.