/*
* Copyright (C) 2014 RoboVM AB
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
*/
package org.robovm.compiler.plugin.lambda;
import static org.objectweb.asm.Opcodes.*;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.robovm.compiler.CompilerException;
import org.robovm.compiler.Types;
import soot.DoubleType;
import soot.FloatType;
import soot.LongType;
import soot.PrimType;
import soot.RefType;
import soot.SootClass;
import soot.SootMethodHandle;
import soot.SootMethodRef;
import soot.SootMethodType;
import soot.Type;
import soot.VoidType;
public class LambdaClassGenerator {
private static int CLASS_VERSION = 51;
private int counter = 1;
public LambdaClass generate(SootClass caller, String invokedName, SootMethodRef invokedType,
SootMethodType samMethodType, SootMethodHandle implMethod, SootMethodType instantiatedMethodType,
List<Type> markerInterfaces, List<SootMethodType> bridgeMethods) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
String lambdaClassName = caller.getName().replace('.', '/') + "$$Lambda$" + (counter++);
String functionalInterface = invokedType.returnType().toString().replace('.', '/');
List<String> interfaces = new ArrayList<String>();
interfaces.add(functionalInterface);
for (Type markerInterface : markerInterfaces) {
interfaces.add(markerInterface.toString().replace('.', '/'));
}
cw.visit(CLASS_VERSION, ACC_FINAL + ACC_SUPER + ACC_SYNTHETIC, lambdaClassName, null, "java/lang/Object",
interfaces.toArray(new String[interfaces.size()]));
String targetMethod = "<init>";
createFieldsAndConstructor(lambdaClassName, cw, invokedType, samMethodType, implMethod, instantiatedMethodType);
// if we perform capturing, we can't cache the
// lambda instance. We need to create a factory method
// that returns a new instance of the lambda
// every time the lambda is invoked. That method
// will be invoked instead of the <init> method
// of the lambda by LambdaPlugin.
if (!invokedType.parameterTypes().isEmpty()) {
targetMethod = createFactory(lambdaClassName, cw, invokedType, samMethodType, implMethod,
instantiatedMethodType);
}
// forward the lambda method
createForwardingMethod(caller, lambdaClassName, cw, invokedName, samMethodType.getParameterTypes(),
samMethodType.getReturnType(), invokedType.parameterTypes(), samMethodType, implMethod,
instantiatedMethodType, false);
// create any bridge methods necessary
for (SootMethodType bridgeMethod : bridgeMethods) {
createForwardingMethod(caller, lambdaClassName, cw, invokedName, bridgeMethod.getParameterTypes(),
bridgeMethod.getReturnType(), invokedType.parameterTypes(), samMethodType, implMethod,
instantiatedMethodType, true);
}
cw.visitEnd();
return new LambdaClass(lambdaClassName, cw.toByteArray(), targetMethod, invokedType.parameterTypes(),
invokedType.returnType());
}
private void createForwardingMethod(SootClass caller, String lambdaClassName, ClassWriter cw, String name,
List<Type> parameters, Type returnType, List<Type> invokedParameters, SootMethodType samMethodType,
SootMethodHandle implMethod, SootMethodType instantiatedMethodType, boolean isBridgeMethod) {
String descriptor = Types.getDescriptor(parameters, returnType);
String implClassName = implMethod.getMethodRef().declaringClass().getName().replace('.', '/');
int accessFlags = ACC_PUBLIC | (isBridgeMethod ? ACC_BRIDGE : 0);
MethodVisitor mv = cw.visitMethod(accessFlags, name, descriptor, null, null);
mv.visitCode();
// figure out the invoke op code for the lambda implementation
// as well as if it's an instance method.
int invokeOpCode = INVOKESTATIC;
boolean isInstanceMethod = false;
switch (implMethod.getReferenceKind()) {
case SootMethodHandle.REF_invokeInterface:
invokeOpCode = INVOKEINTERFACE;
isInstanceMethod = true;
break;
case SootMethodHandle.REF_invokeSpecial:
invokeOpCode = INVOKESPECIAL;
isInstanceMethod = true;
break;
case SootMethodHandle.REF_newInvokeSpecial:
invokeOpCode = INVOKESPECIAL;
break;
case SootMethodHandle.REF_invokeStatic:
invokeOpCode = INVOKESTATIC;
break;
case SootMethodHandle.REF_invokeVirtual:
invokeOpCode = INVOKEVIRTUAL;
isInstanceMethod = true;
break;
default:
throw new CompilerException("Unknown invoke type: " + implMethod.getReferenceKind());
}
GeneratorAdapter caster = new GeneratorAdapter(mv, accessFlags, name, descriptor);
// push the arguments
pushArguments(caller, lambdaClassName, mv, caster, parameters, invokedParameters, implMethod,
instantiatedMethodType, isInstanceMethod);
// generate a descriptor for the lambda implementation
// to invoke based on the parameters. If the lambda
// is an instance method, we need to remove the first
// parameter for the descriptor generation as it's
// not part of the method signature.
String implDescriptor = null;
List<Type> paramTypes = new ArrayList<Type>(implMethod.getMethodType().getParameterTypes());
if (isInstanceMethod)
paramTypes.remove(0);
implDescriptor = Types.getDescriptor(paramTypes, implMethod.getMethodType().getReturnType());
// call the lambda implementation
mv.visitMethodInsn(invokeOpCode, implClassName, implMethod.getMethodRef().name(), implDescriptor,
invokeOpCode == INVOKEINTERFACE);
// emit the return instruction based on the return type
createForwardingMethodReturn(mv, caster, returnType, samMethodType, implMethod, instantiatedMethodType);
mv.visitMaxs(-1, -1);
mv.visitEnd();
}
private void pushArguments(SootClass caller, String lambdaClassName, MethodVisitor mv, GeneratorAdapter caster,
List<Type> parameters, List<Type> invokedParameters, SootMethodHandle implMethod,
SootMethodType instantiatedMethodType, boolean isInstanceMethod) {
// if this is a method reference to ::new, we need to
// create the object itself
if (implMethod.getReferenceKind() == SootMethodHandle.REF_newInvokeSpecial) {
mv.visitTypeInsn(NEW, implMethod.getMethodRef().declaringClass().getName().replace('.', '/'));
mv.visitInsn(DUP);
}
// push the captured arguments
for (int i = 0; i < invokedParameters.size(); i++) {
Object obj = invokedParameters.get(i);
Type captureType = (Type) obj;
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, lambdaClassName, "arg$" + (i + 1), Types.getDescriptor(captureType));
}
// push the functional interface parameters
// first check if the parameters include the received, e.g.
// "hello"::contains
// would be called on "hello" and not on the class containing the
// invoke dynamic call. We need to handle that parameter separately as
// it's not part of the method signature of the implementation
boolean paramsContainReceiver = isInstanceMethod
& !caller.getName().equals(implMethod.getMethodRef().declaringClass().getName())
&& parameters.size() > implMethod.getMethodRef().parameterTypes().size();
int paramsIndex = 0;
int localIndex = 1; // we start at slot index 1, because this occupies
// slot 0
if (paramsContainReceiver && !parameters.isEmpty()) {
Type param = parameters.get(0);
mv.visitVarInsn(loadOpcodeForType(param), localIndex);
castOrWiden(mv, caster, param, implMethod.getMethodRef().declaringClass().getType());
localIndex += slotsForType(param);
paramsIndex++;
}
int samParamsOffset = implMethod.getMethodRef().parameterTypes().size() - parameters.size()
+ (paramsContainReceiver ? 1 : 0);
for (int i = 0; paramsIndex < parameters.size(); paramsIndex++, i++) {
Type param = parameters.get(paramsIndex);
mv.visitVarInsn(loadOpcodeForType(param), localIndex);
castOrWiden(mv, caster, param, (Type) implMethod.getMethodRef().parameterTypes().get(samParamsOffset + i));
localIndex += slotsForType(param);
}
}
private void castOrWiden(MethodVisitor mv, GeneratorAdapter caster, Type actual, Type expected) {
if (actual.equals(expected)) {
return;
}
if (actual.equals(VoidType.v()) || expected.equals(VoidType.v())) {
return;
}
// primitive types, either widening or boxing/unboxing
if ((isPrimitiveType(actual) || isBoxedType(actual))
&& ((isPrimitiveType(expected) || isBoxedType(expected)))) {
org.objectweb.asm.Type actualAsmType = getAsmPrimitiveType(actual);
org.objectweb.asm.Type expectedAsmType = getAsmPrimitiveType(expected);
// unbox the actual value
if (isBoxedType(actual)) {
caster.unbox(actualAsmType);
}
// widen to the type of the
// expected value
caster.cast(actualAsmType, expectedAsmType);
// box the value if necessary
if (isBoxedType(expected)) {
caster.box(expectedAsmType);
}
} else if (isPrimitiveType(expected) && actual instanceof RefType) {
// actual could be java.lang.Number or even java.lang.Object when
// expected is a primitive. We need to unbox in this case too.
// unbox() will do the right thing, e.g. ((Number) v).longValue() if
// expected is long.
caster.unbox(getAsmPrimitiveType(expected));
} else if (isPrimitiveType(actual) && expected instanceof RefType) {
// The inverse of the previous "else if". Boxing is needed.
caster.box(getAsmPrimitiveType(actual));
} else {
// simple cast which will throw a ClassCastException at runtime
mv.visitTypeInsn(Opcodes.CHECKCAST, Types.getInternalName(expected));
}
}
private org.objectweb.asm.Type getAsmPrimitiveType(Type type) {
if (isBoxedType(type)) {
String className = type.toString();
if ("java.lang.Boolean".equals(className)) {
return org.objectweb.asm.Type.BOOLEAN_TYPE;
} else if ("java.lang.Byte".equals(className)) {
return org.objectweb.asm.Type.BYTE_TYPE;
} else if ("java.lang.Character".equals(className)) {
return org.objectweb.asm.Type.CHAR_TYPE;
} else if ("java.lang.Short".equals(className)) {
return org.objectweb.asm.Type.SHORT_TYPE;
} else if ("java.lang.Integer".equals(className)) {
return org.objectweb.asm.Type.INT_TYPE;
} else if ("java.lang.Long".equals(className)) {
return org.objectweb.asm.Type.LONG_TYPE;
} else if ("java.lang.Float".equals(className)) {
return org.objectweb.asm.Type.FLOAT_TYPE;
} else if ("java.lang.Double".equals(className)) {
return org.objectweb.asm.Type.DOUBLE_TYPE;
} else {
throw new CompilerException("Unknown primitive type " + type);
}
} else {
return org.objectweb.asm.Type.getType(Types.getDescriptor(type));
}
}
private boolean isPrimitiveType(Type type) {
return type instanceof PrimType;
}
private boolean isBoxedType(Type type) {
String className = type.toString();
return "java.lang.Boolean".equals(className) || "java.lang.Byte".equals(className)
|| "java.lang.Character".equals(className) || "java.lang.Short".equals(className)
|| "java.lang.Integer".equals(className) || "java.lang.Long".equals(className)
|| "java.lang.Float".equals(className) || "java.lang.Double".equals(className);
}
private void createForwardingMethodReturn(MethodVisitor mv, GeneratorAdapter caster, Type returnType,
SootMethodType samMethodType, SootMethodHandle implMethod, SootMethodType instantiatedMethodType) {
castOrWiden(mv, caster, implMethod.getMethodRef().returnType(), instantiatedMethodType.getReturnType());
if (returnType.equals(VoidType.v())) {
mv.visitInsn(RETURN);
} else if (returnType instanceof PrimType) {
if (returnType.equals(LongType.v())) {
mv.visitInsn(LRETURN);
} else if (returnType.equals(FloatType.v())) {
mv.visitInsn(FRETURN);
} else if (returnType.equals(DoubleType.v())) {
mv.visitInsn(DRETURN);
} else {
mv.visitInsn(IRETURN);
}
} else {
mv.visitInsn(ARETURN);
}
}
private void createFieldsAndConstructor(String lambdaClassName, ClassWriter cw, SootMethodRef invokedType,
SootMethodType samMethodType, SootMethodHandle implMethod, SootMethodType instantiatedMethodType) {
StringBuffer constructorDescriptor = new StringBuffer();
// create the fields on the class
int i = 0;
for (Object obj : invokedType.parameterTypes()) {
Type captureType = (Type) obj;
String typeDesc = Types.getDescriptor(captureType);
cw.visitField(ACC_PRIVATE + ACC_FINAL, "arg$" + (i + 1), typeDesc, null, null);
constructorDescriptor.append(typeDesc);
i++;
}
// create constructor
MethodVisitor mv = cw.visitMethod(0, "<init>", "(" + constructorDescriptor.toString() + ")V", null, null);
mv.visitCode();
// calls super
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
// store the captures into the fields
i = 0;
int localIndex = 1; // we start at slot 1, because this occupies slot 0
for (Object obj : invokedType.parameterTypes()) {
Type captureType = (Type) obj;
// load this for put field
mv.visitVarInsn(ALOAD, 0);
// load capture from argument slot
mv.visitVarInsn(loadOpcodeForType(captureType), localIndex);
localIndex += slotsForType(captureType);
// store the capture into the field
mv.visitFieldInsn(PUTFIELD, lambdaClassName, "arg$" + (i + 1), Types.getDescriptor(captureType));
i++;
}
mv.visitInsn(RETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
}
private String createFactory(String lambdaClassName, ClassWriter cw, SootMethodRef invokedType,
SootMethodType samMethodType, SootMethodHandle implMethod, SootMethodType instantiatedMethodType) {
MethodVisitor mv = cw.visitMethod(ACC_STATIC, "get$Lambda",
Types.getDescriptor(invokedType.parameterTypes(), invokedType.returnType()), null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, lambdaClassName);
mv.visitInsn(DUP);
int i = 0;
for (Object obj : invokedType.parameterTypes()) {
Type captureType = (Type) obj;
mv.visitVarInsn(loadOpcodeForType(captureType), i);
i += slotsForType(captureType);
}
mv.visitMethodInsn(INVOKESPECIAL, lambdaClassName, "<init>",
Types.getDescriptor(invokedType.parameterTypes(), VoidType.v()), false);
mv.visitInsn(ARETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
return "get$Lambda";
}
public int loadOpcodeForType(Type type) {
if (type instanceof PrimType) {
if (type.equals(LongType.v())) {
return LLOAD;
} else if (type.equals(FloatType.v())) {
return FLOAD;
} else if (type.equals(DoubleType.v())) {
return DLOAD;
} else {
return ILOAD;
}
} else {
return ALOAD;
}
}
public int slotsForType(Type type) {
if (type.equals(LongType.v()) || type.equals(DoubleType.v())) {
return 2;
} else {
return 1;
}
}
}