org.elasticsearch.painless.LambdaBootstrap.java Source code

Java tutorial

Introduction

Here is the source code for org.elasticsearch.painless.LambdaBootstrap.java

Source

/*
 * 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 Rmi 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 Rmi 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));
    }
}