/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.painless; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Handle; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.LambdaConversionException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.security.AccessController; import java.security.PrivilegedAction; import static java.lang.invoke.MethodHandles.Lookup; import static org.elasticsearch.painless.Compiler.Loader; import static org.elasticsearch.painless.WriterConstants.CLASS_VERSION; import static org.elasticsearch.painless.WriterConstants.CTOR_METHOD_NAME; import static org.elasticsearch.painless.WriterConstants.DELEGATE_BOOTSTRAP_HANDLE; import static org.objectweb.asm.Opcodes.ACC_FINAL; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_STATIC; import static org.objectweb.asm.Opcodes.ACC_SUPER; import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; import static org.objectweb.asm.Opcodes.H_INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.H_INVOKESTATIC; import static org.objectweb.asm.Opcodes.H_INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.H_NEWINVOKESPECIAL; /** * LambdaBootstrap is used to generate all the code necessary to execute * lambda functions and method references within Painless. The code generation * used here is based upon the following article: * http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html * However, it is a simplified version as Painless has no concept of generics * or serialization. LambdaBootstrap is being used as a replacement for * {@link java.lang.invoke.LambdaMetafactory} since the Painless casting model * cannot be fully supported through this class. * * For each lambda function/method reference used within a Painless script * a class will be generated at link-time using the * {@link LambdaBootstrap#lambdaBootstrap} method that contains the following: * 1. member fields for any captured variables * 2. a constructor that will take in captured variables and assign them to * their respective member fields * 3. a static ctor delegation method, if the lambda function is a ctor. * 4. a method that will load the member fields representing captured variables * and take in any other necessary values based on the arguments passed into the * lambda function/reference method; it will then make a delegated call to the * actual lambda function/reference method * * Take for example the following Painless script: * * {@code * List list1 = new ArrayList(); " * list1.add(2); " * List list2 = new ArrayList(); " * list1.forEach(x -> list2.add(x));" * return list[0]" * } * * The script contains a lambda function with a captured variable. * The following Lambda class would be generated: * * {@code * public static final class $$Lambda0 implements Consumer { * private List arg$0; * * private $$Lambda0(List arg$0) { * this.arg$0 = arg$0; * } * * public static Consumer create$lambda(List arg$0) { * return new $$Lambda0(arg$0); * } * * public void accept(Object val$0) { * Painless$Script.lambda$0(this.arg$0, val$0); * } * } * * public class Painless$Script implements ... { * ... * public static lambda$0(List list2, Object x) { * list2.add(x); * } * ... * } * } * * Also the accept method actually uses an invokedynamic * instruction to call the lambda$0 method so that * {@link MethodHandle#asType} can be used to do the necessary * conversions between argument types without having to hard * code them. For method references to a constructor, a static * wrapper method is created, that creates a class instance and * calls the constructor. This method is used by the * invokedynamic call to initialize the instance. * * When the {@link CallSite} is linked the linked method depends * on whether or not there are captures. If there are no captures * the same instance of the generated lambda class will be * returned each time by the factory method as there are no * changing values other than the arguments, the lambda is a singleton. * If there are captures, a new instance of the generated lambda class * will be returned each time with the captures passed into the * factory method to be stored in the member fields. * Instead of calling the ctor, a static factory method is created * in the lambda class, because a method handle to the ctor directly * is (currently) preventing Hotspot optimizer from correctly doing * escape analysis. Escape analysis is important to optimize the * code in a way, that a new instance is not created on each lambda * invocation with captures, stressing garbage collector (thanks * to Rémi Forax for the explanation about this on Jaxcon 2017!). */ public final class LambdaBootstrap { /** * Metadata for a captured variable used during code generation. */ private static final class Capture { private final String name; private final Type type; private final String desc; /** * Converts incoming parameters into the name, type, and * descriptor for the captured argument. * @param count The captured argument count * @param type The class type of the captured argument */ private Capture(int count, Class<?> type) { this.name = "arg$" + count; this.type = Type.getType(type); this.desc = this.type.getDescriptor(); } } /** * This method name is used to generate a static wrapper method to handle delegation of ctors. */ private static final String DELEGATED_CTOR_WRAPPER_NAME = "delegate$ctor"; /** * This method name is used to generate the static factory for capturing lambdas. */ private static final String LAMBDA_FACTORY_METHOD_NAME = "create$lambda"; /** * Generates a lambda class for a lambda function/method reference * within a Painless script. Variables with the prefix interface are considered * to represent values for code generated for the lambda class. Variables with * the prefix delegate are considered to represent values for code generated * within the Painless script. The interface method delegates (calls) to the * delegate method. * @param lookup Standard {@link MethodHandles#lookup} * @param interfaceMethodName Name of functional interface method that is called * @param factoryMethodType The type of method to be linked to this CallSite; note that * captured types are based on the parameters for this method * @param interfaceMethodType The type of method representing the functional interface method * @param delegateClassName The name of the class to delegate method call to * @param delegateInvokeType The type of method call to be made * (static, virtual, interface, or constructor) * @param delegateMethodName The name of the method to be called in the Painless script class * @param delegateMethodType The type of method call in the Painless script class without * the captured types * @return A {@link CallSite} linked to a factory method for creating a lambda class * that implements the expected functional interface * @throws LambdaConversionException Thrown when an illegal type conversion occurs at link time */ public static CallSite lambdaBootstrap( Lookup lookup, String interfaceMethodName, MethodType factoryMethodType, MethodType interfaceMethodType, String delegateClassName, int delegateInvokeType, String delegateMethodName, MethodType delegateMethodType) throws LambdaConversionException { Loader loader = (Loader)lookup.lookupClass().getClassLoader(); String lambdaClassName = Type.getInternalName(lookup.lookupClass()) + "$$Lambda" + loader.newLambdaIdentifier(); Type lambdaClassType = Type.getObjectType(lambdaClassName); Type delegateClassType = Type.getObjectType(delegateClassName.replace('.', '/')); validateTypes(interfaceMethodType, delegateMethodType); ClassWriter cw = beginLambdaClass(lambdaClassName, factoryMethodType.returnType()); Capture[] captures = generateCaptureFields(cw, factoryMethodType); generateLambdaConstructor(cw, lambdaClassType, factoryMethodType, captures); // Handles the special case where a method reference refers to a ctor (we need a static wrapper method): if (delegateInvokeType == H_NEWINVOKESPECIAL) { assert CTOR_METHOD_NAME.equals(delegateMethodName); generateStaticCtorDelegator(cw, ACC_PRIVATE, DELEGATED_CTOR_WRAPPER_NAME, delegateClassType, delegateMethodType); // replace the delegate with our static wrapper: delegateMethodName = DELEGATED_CTOR_WRAPPER_NAME; delegateClassType = lambdaClassType; delegateInvokeType = H_INVOKESTATIC; } generateInterfaceMethod(cw, factoryMethodType, lambdaClassType, interfaceMethodName, interfaceMethodType, delegateClassType, delegateInvokeType, delegateMethodName, delegateMethodType, captures); endLambdaClass(cw); Class<?> lambdaClass = createLambdaClass(loader, cw, lambdaClassType); if (captures.length > 0) { return createCaptureCallSite(lookup, factoryMethodType, lambdaClass); } else { return createNoCaptureCallSite(factoryMethodType, lambdaClass); } } /** * Validates some conversions at link time. Currently, only ensures that the lambda method * with a return value cannot delegate to a delegate method with no return type. */ private static void validateTypes(MethodType interfaceMethodType, MethodType delegateMethodType) throws LambdaConversionException { if (interfaceMethodType.returnType() != void.class && delegateMethodType.returnType() == void.class) { throw new LambdaConversionException("lambda expects return type [" + interfaceMethodType.returnType() + "], but found return type [void]"); } } /** * Creates the {@link ClassWriter} to be used for the lambda class generation. */ private static ClassWriter beginLambdaClass(String lambdaClassName, Class<?> lambdaInterface) { String baseClass = Type.getInternalName(Object.class); int modifiers = ACC_PUBLIC | ACC_SUPER | ACC_FINAL | ACC_SYNTHETIC; ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); cw.visit(CLASS_VERSION, modifiers, lambdaClassName, null, baseClass, new String[] { Type.getInternalName(lambdaInterface) }); return cw; } /** * Generates member fields for captured variables * based on the parameters for the factory method. * @return An array of captured variable metadata * for generating method arguments later on */ private static Capture[] generateCaptureFields(ClassWriter cw, MethodType factoryMethodType) { int captureTotal = factoryMethodType.parameterCount(); Capture[] captures = new Capture[captureTotal]; for (int captureCount = 0; captureCount < captureTotal; ++captureCount) { captures[captureCount] = new Capture(captureCount, factoryMethodType.parameterType(captureCount)); int modifiers = ACC_PRIVATE | ACC_FINAL; FieldVisitor fv = cw.visitField( modifiers, captures[captureCount].name, captures[captureCount].desc, null, null); fv.visitEnd(); } return captures; } /** * Generates a constructor that will take in captured * arguments if any and store them in their respective * member fields. */ private static void generateLambdaConstructor( ClassWriter cw, Type lambdaClassType, MethodType factoryMethodType, Capture[] captures) { String conDesc = factoryMethodType.changeReturnType(void.class).toMethodDescriptorString(); Method conMeth = new Method(CTOR_METHOD_NAME, conDesc); Type baseConType = Type.getType(Object.class); Method baseConMeth = new Method(CTOR_METHOD_NAME, MethodType.methodType(void.class).toMethodDescriptorString()); int modifiers = (captures.length > 0) ? ACC_PRIVATE : ACC_PUBLIC; GeneratorAdapter constructor = new GeneratorAdapter(modifiers, conMeth, cw.visitMethod(modifiers, CTOR_METHOD_NAME, conDesc, null, null)); constructor.visitCode(); constructor.loadThis(); constructor.invokeConstructor(baseConType, baseConMeth); for (int captureCount = 0; captureCount < captures.length; ++captureCount) { constructor.loadThis(); constructor.loadArg(captureCount); constructor.putField( lambdaClassType, captures[captureCount].name, captures[captureCount].type); } constructor.returnValue(); constructor.endMethod(); // Add a factory method, if lambda takes captures. // @uschindler says: I talked with Rémi Forax about this. Technically, a plain ctor // and a MethodHandle to the ctor would be enough - BUT: Hotspot is unable to // do escape analysis through a MethodHandles.findConstructor generated handle. // Because of this we create a factory method. With this factory method, the // escape analysis can figure out that everything is final and we don't need // an instance, so it can omit object creation on heap! if (captures.length > 0) { generateStaticCtorDelegator(cw, ACC_PUBLIC, LAMBDA_FACTORY_METHOD_NAME, lambdaClassType, factoryMethodType); } } /** * Generates a factory method to delegate to constructors. */ private static void generateStaticCtorDelegator(ClassWriter cw, int access, String delegatorMethodName, Type delegateClassType, MethodType delegateMethodType) { Method wrapperMethod = new Method(delegatorMethodName, delegateMethodType.toMethodDescriptorString()); Method constructorMethod = new Method(CTOR_METHOD_NAME, delegateMethodType.changeReturnType(void.class).toMethodDescriptorString()); int modifiers = access | ACC_STATIC; GeneratorAdapter factory = new GeneratorAdapter(modifiers, wrapperMethod, cw.visitMethod(modifiers, delegatorMethodName, delegateMethodType.toMethodDescriptorString(), null, null)); factory.visitCode(); factory.newInstance(delegateClassType); factory.dup(); factory.loadArgs(); factory.invokeConstructor(delegateClassType, constructorMethod); factory.returnValue(); factory.endMethod(); } /** * Generates the interface method that will delegate (call) to the delegate method * with {@code INVOKEDYNAMIC} using the {@link #delegateBootstrap} type converter. */ private static void generateInterfaceMethod( ClassWriter cw, MethodType factoryMethodType, Type lambdaClassType, String interfaceMethodName, MethodType interfaceMethodType, Type delegateClassType, int delegateInvokeType, String delegateMethodName, MethodType delegateMethodType, Capture[] captures) throws LambdaConversionException { String lamDesc = interfaceMethodType.toMethodDescriptorString(); Method lamMeth = new Method(lambdaClassType.getInternalName(), lamDesc); int modifiers = ACC_PUBLIC; GeneratorAdapter iface = new GeneratorAdapter(modifiers, lamMeth, cw.visitMethod(modifiers, interfaceMethodName, lamDesc, null, null)); iface.visitCode(); // Loads any captured variables onto the stack. for (int captureCount = 0; captureCount < captures.length; ++captureCount) { iface.loadThis(); iface.getField( lambdaClassType, captures[captureCount].name, captures[captureCount].type); } // Loads any passed in arguments onto the stack. iface.loadArgs(); // Handles the case for a lambda function or a static reference method. // interfaceMethodType and delegateMethodType both have the captured types // inserted into their type signatures. This later allows the delegate // method to be invoked dynamically and have the interface method types // appropriately converted to the delegate method types. // Example: Integer::parseInt // Example: something.each(x -> x + 1) if (delegateInvokeType == H_INVOKESTATIC) { interfaceMethodType = interfaceMethodType.insertParameterTypes(0, factoryMethodType.parameterArray()); delegateMethodType = delegateMethodType.insertParameterTypes(0, factoryMethodType.parameterArray()); } else if (delegateInvokeType == H_INVOKEVIRTUAL || delegateInvokeType == H_INVOKEINTERFACE) { // Handles the case for a virtual or interface reference method with no captures. // delegateMethodType drops the 'this' parameter because it will be re-inserted // when the method handle for the dynamically invoked delegate method is created. // Example: Object::toString if (captures.length == 0) { Class<?> clazz = delegateMethodType.parameterType(0); delegateClassType = Type.getType(clazz); delegateMethodType = delegateMethodType.dropParameterTypes(0, 1); // Handles the case for a virtual or interface reference method with 'this' // captured. interfaceMethodType inserts the 'this' type into its // method signature. This later allows the delegate // method to be invoked dynamically and have the interface method types // appropriately converted to the delegate method types. // Example: something::toString } else if (captures.length == 1) { Class<?> clazz = factoryMethodType.parameterType(0); delegateClassType = Type.getType(clazz); interfaceMethodType = interfaceMethodType.insertParameterTypes(0, clazz); } else { throw new LambdaConversionException( "unexpected number of captures [ " + captures.length + "]"); } } else { throw new IllegalStateException( "unexpected invocation type [" + delegateInvokeType + "]"); } Handle delegateHandle = new Handle(delegateInvokeType, delegateClassType.getInternalName(), delegateMethodName, delegateMethodType.toMethodDescriptorString(), delegateInvokeType == H_INVOKEINTERFACE); iface.invokeDynamic(delegateMethodName, Type.getMethodType(interfaceMethodType .toMethodDescriptorString()).getDescriptor(), DELEGATE_BOOTSTRAP_HANDLE, delegateHandle); iface.returnValue(); iface.endMethod(); } /** * Closes the {@link ClassWriter}. */ private static void endLambdaClass(ClassWriter cw) { cw.visitEnd(); } /** * Defines the {@link Class} for the lambda class using the same {@link Loader} * that originally defined the class for the Painless script. */ private static Class<?> createLambdaClass( Loader loader, ClassWriter cw, Type lambdaClassType) { byte[] classBytes = cw.toByteArray(); // DEBUG: // new ClassReader(classBytes).accept(new TraceClassVisitor(new PrintWriter(System.out)), ClassReader.SKIP_DEBUG); return AccessController.doPrivileged((PrivilegedAction<Class<?>>)() -> loader.defineLambda(lambdaClassType.getClassName(), classBytes)); } /** * Creates an {@link ConstantCallSite} that will return the same instance * of the generated lambda class every time this linked factory method is called. */ private static CallSite createNoCaptureCallSite( MethodType factoryMethodType, Class<?> lambdaClass) { try { return new ConstantCallSite(MethodHandles.constant( factoryMethodType.returnType(), lambdaClass.getConstructor().newInstance())); } catch (ReflectiveOperationException exception) { throw new IllegalStateException("unable to instantiate lambda class", exception); } } /** * Creates an {@link ConstantCallSite} */ private static CallSite createCaptureCallSite( Lookup lookup, MethodType factoryMethodType, Class<?> lambdaClass) { try { return new ConstantCallSite( lookup.findStatic(lambdaClass, LAMBDA_FACTORY_METHOD_NAME, factoryMethodType)); } catch (ReflectiveOperationException exception) { throw new IllegalStateException("unable to create lambda class", exception); } } /** * Links the delegate method to the returned {@link CallSite}. The linked * delegate method will use converted types from the interface method. Using * invokedynamic to make the delegate method call allows * {@link MethodHandle#asType} to be used to do the type conversion instead * of either a lot more code or requiring many {@link Definition.Type}s to be looked * up at link-time. */ public static CallSite delegateBootstrap(Lookup lookup, String delegateMethodName, MethodType interfaceMethodType, MethodHandle delegateMethodHandle) { return new ConstantCallSite(delegateMethodHandle.asType(interfaceMethodType)); } }