All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opalj.br.reader.BootstrapArgumentLoading.scala Maven / Gradle / Ivy

The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package br
package reader

import org.opalj.bi.ACC_PRIVATE
import org.opalj.bi.ACC_STATIC
import org.opalj.bi.ACC_SYNTHETIC
import org.opalj.br.collection.mutable.InstructionsBuilder
import org.opalj.br.instructions.AASTORE
import org.opalj.br.instructions.ACONST_NULL
import org.opalj.br.instructions.ALOAD_1
import org.opalj.br.instructions.ANEWARRAY
import org.opalj.br.instructions.CHECKCAST
import org.opalj.br.instructions.DUP
import org.opalj.br.instructions.GETSTATIC
import org.opalj.br.instructions.IAND
import org.opalj.br.instructions.ICONST_0
import org.opalj.br.instructions.ICONST_1
import org.opalj.br.instructions.IFNE
import org.opalj.br.instructions.INVOKESTATIC
import org.opalj.br.instructions.INVOKEVIRTUAL
import org.opalj.br.instructions.LoadClass_W
import org.opalj.br.instructions.LoadConstantInstruction
import org.opalj.br.instructions.LoadDynamic2_W
import org.opalj.br.instructions.LoadDynamic_W
import org.opalj.br.instructions.LoadMethodHandle_W
import org.opalj.br.instructions.LoadString_W
import org.opalj.br.instructions.TypeConversionInstructions
import org.opalj.br.MethodDescriptor.JustReturnsString
import org.opalj.br.cp.Constant_Pool
import org.opalj.br.instructions.ALOAD_0
import org.opalj.br.instructions.ALOAD_2
import org.opalj.br.instructions.ARETURN
import org.opalj.br.instructions.ASTORE_1
import org.opalj.br.instructions.ASTORE_2
import org.opalj.br.instructions.BIPUSH
import org.opalj.br.instructions.IADD
import org.opalj.br.instructions.IFEQ
import org.opalj.br.instructions.IMUL
import org.opalj.br.instructions.INSTANCEOF
import org.opalj.br.instructions.INVOKESPECIAL
import org.opalj.br.instructions.IRETURN
import org.opalj.br.instructions.LoadInt_W
import org.opalj.br.instructions.NEW

import scala.collection.immutable.ArraySeq

/**
 * Provides functionality to produce bytecode that loads a bootstrap argument. Loading of dynamic
 * constants is rewritten to not use ldc/ldc_w/ldc2_w instructions where possible.
 *
 * @author Dominik Helm
 */
trait BootstrapArgumentLoading {

    /**
     * Generate instructions to load a constant argument for a bootstrap method.
     *
     * @param argument The argument to be loaded
     * @param instructions The instruction builder the instructions are appended to
     * @param classFile The class file the constant is in
     * @param boxed If true, ensures the argument is of a reference type, boxing it if necessary
     *
     * @return The maximum stack height the generated instructions require and the (potentially
     *         new!) class file
     */
    def loadBootstrapArgument(
        argument:     ConstantValue[_],
        instructions: InstructionsBuilder,
        classFile:    ClassFile,
        boxed:        Boolean             = false
    ): (Int, ClassFile) = {
        argument match {
            case DynamicConstant(bootstrapMethod, name, descriptor) =>
                loadDynamicConstant(
                    bootstrapMethod, name, descriptor, instructions, classFile, boxed
                )
            case _ =>
                instructions ++= LoadConstantInstruction(argument, wide = true)
                if (boxed && argument.runtimeValueType.isBaseType)
                    instructions ++= argument.runtimeValueType.asBaseType.boxValue
                (argument.runtimeValueType.computationalType.operandSize, classFile)
        }
    }

    /**
     * Generate instructions to load a dynamic constant.
     *
     * @param bootstrapMethod The constant's bootstrap method
     * @param name The constant's name information, passed as the bootstrap method's 2. argument
     * @param descriptor The constant's type, passed as the bootstrap method's 3. argument
     * @param instructions The instruction builder the instructions are appended to
     * @param classFile The class file the constant is in
     * @param boxed If true, ensures the constant is of a reference type, boxing it if necessary
     *
     * @return The maximum stack height the generated instructions require and the (potentially
     *         new!) class file
     */
    def loadDynamicConstant(
        bootstrapMethod: BootstrapMethod,
        name:            String,
        descriptor:      FieldType,
        instructions:    InstructionsBuilder,
        classFile:       ClassFile,
        boxed:           Boolean             = false
    ): (Int, ClassFile) = {

        def dynamicLoad(): LoadConstantInstruction[_] = {
            if (descriptor.computationalType.isCategory2) LoadDynamic2_W(bootstrapMethod, name, descriptor)
            else LoadDynamic_W(bootstrapMethod, name, descriptor)
        }

        bootstrapMethod match {
            case BootstrapMethod(InvokeStaticMethodHandle(ObjectType.ConstantBootstraps, false, methodName, _), args: ArraySeq[ConstantValue[_]] @unchecked) =>

                methodName match {
                    case "arrayVarHandle" =>
                        val maxStackAndClassFile = loadClassType(args.head, instructions, classFile)

                        instructions ++= INVOKESTATIC(
                            ObjectType.MethodHandles,
                            isInterface = false,
                            "arrayElementVarHandle",
                            MethodDescriptor(ObjectType.Class, ObjectType.VarHandle)
                        )

                        maxStackAndClassFile

                    case "enumConstant" =>
                        instructions ++= GETSTATIC(descriptor.asObjectType, name, descriptor)
                        (1, classFile)

                    case "explicitCast" =>
                        val argument = args.head
                        val argType = argument.runtimeValueType
                        val (bsaStack, updatedClassFile) =
                            loadBootstrapArgument(argument, instructions, classFile)

                        val maxStack = descriptor match {
                            case ObjectType.Object => // Nothing to do here, but box if required
                                if (boxed && argType.isBaseType)
                                    instructions ++= argType.asBaseType.boxValue
                                bsaStack

                            case rt: ReferenceType =>
                                instructions ++= CHECKCAST(rt)
                                bsaStack

                            case bt: BaseType =>
                                val isBool = bt.isBooleanType
                                val targetType = if (isBool) IntegerType else bt

                                val baseType = if (argType.isReferenceType) {
                                    instructions ++= ObjectType.unboxValue(argType)
                                    ObjectType.primitiveType(argType.asObjectType).get.asNumericType
                                } else argType.asNumericType

                                if (baseType != targetType) {
                                    instructions ++= baseType.convertTo(targetType.asNumericType)
                                }

                                // explicitCast requires booleans to be converted from ints based on
                                // the least significant bit
                                if (isBool) {
                                    instructions ++= ICONST_1
                                    instructions ++= IAND
                                }

                                if (boxed)
                                    instructions ++= bt.boxValue

                                bsaStack + (if (isBool) 1 else 0)
                        }

                        (maxStack, updatedClassFile)

                    case "fieldVarHandle" =>
                        fieldVarHandle(
                            name,
                            args.head,
                            cf => loadClassType(args(1), instructions, cf),
                            isStaticField = false,
                            instructions,
                            classFile
                        )

                    case "getStaticFinal" =>
                        if (args.isEmpty) { // Simple version loads field from class of field type
                            val declClass = if (descriptor.isObjectType) descriptor.asObjectType
                            else descriptor.asBaseType.WrapperType
                            instructions ++= GETSTATIC(declClass, name, descriptor)
                            if (boxed && descriptor.isBaseType)
                                instructions ++= descriptor.asBaseType.boxValue
                            (descriptor.computationalType.operandSize, classFile)
                        } else args.head match {
                            case ConstantClass(ct) =>
                                instructions ++= GETSTATIC(ct.asObjectType, name, descriptor)
                                if (boxed && descriptor.isBaseType)
                                    instructions ++= descriptor.asBaseType.boxValue
                                (descriptor.computationalType.operandSize, classFile)
                            case dc: DynamicConstant =>
                                doGetStatic(dc, name, descriptor, instructions, classFile, boxed)
                        }

                    case "invoke" =>
                        val methodHandle = args.head
                        val arguments = args.tail
                        invokeMethodHandle(
                            methodHandle, arguments, descriptor, instructions, classFile, boxed
                        )

                    case "nullConstant" =>
                        instructions ++= ACONST_NULL
                        (1, classFile)

                    case "primitiveClass" =>
                        val classType = FieldType(name).asBaseType.WrapperType
                        instructions ++= GETSTATIC(classType, "TYPE", ObjectType.Class)
                        (1, classFile)

                    case "staticFieldVarHandle" =>
                        fieldVarHandle(
                            name,
                            args.head,
                            cf => loadClassType(args(1), instructions, cf),
                            isStaticField = true,
                            instructions,
                            classFile
                        )

                    case _ =>
                        instructions ++= dynamicLoad()
                        (descriptor.computationalType.operandSize, classFile)
                }

            case BootstrapMethod(InvokeStaticMethodHandle(ObjectType.ObjectMethods, false, _, _), _) =>
                val newMethodName = s"$$object_methods$$${classFile.thisType.simpleName}:pc"
                val (updatedClassFile, newMethodDescriptor) = createObjectMethodsTarget(
                    bootstrapMethod.arguments, name, newMethodName, classFile
                ).getOrElse {
                        instructions ++= dynamicLoad()
                        return (descriptor.computationalType.operandSize, classFile);
                    }

                instructions ++= LoadMethodHandle_W(InvokeStaticMethodHandle(
                    classFile.thisType,
                    classFile.isInterfaceDeclaration,
                    newMethodName,
                    newMethodDescriptor
                ))

                (1, updatedClassFile)

            case _ =>
                instructions ++= dynamicLoad()
                (descriptor.computationalType.operandSize, classFile)
        }
    }

    /**
     * Generate Instructions to get a VarHandle for a field.
     */
    private def fieldVarHandle(
        fieldName:      String,
        declaringClass: BootstrapArgument,
        loadFieldType:  ClassFile => (Int, ClassFile),
        isStaticField:  Boolean,
        instructions:   InstructionsBuilder,
        classFile:      ClassFile
    ): (Int, ClassFile) = {
        instructions ++= INVOKESTATIC( // Get suitable MethodHandles$Lookup object
            ObjectType.MethodHandles,
            isInterface = false,
            "lookup",
            MethodDescriptor.withNoArgs(ObjectType.MethodHandles$Lookup)
        )

        val (loadDeclClassStack, updatedClassFile1) =
            loadClassType(declaringClass, instructions, classFile)

        instructions ++= LoadString_W(fieldName)

        val (loadFieldTypeStack, updatedClassFile2) = loadFieldType(updatedClassFile1)

        instructions ++= INVOKEVIRTUAL(
            ObjectType.MethodHandles$Lookup,
            if (isStaticField) "findStaticVarHandle" else "findVarHandle",
            MethodDescriptor.FindVarHandleDescriptor
        )

        (Math.max(1 + loadDeclClassStack, 3 + loadFieldTypeStack), updatedClassFile2)
    }

    /**
     * Generate instructions to load a ClassObject from the given BootstrapArgument.
     */
    private def loadClassType(
        classType:    BootstrapArgument,
        instructions: InstructionsBuilder,
        classFile:    ClassFile
    ): (Int, ClassFile) = classType match {
        case ConstantClass(tpe) =>
            instructions ++= LoadClass_W(tpe)
            (1, classFile)

        case DynamicConstant(constantBSM, constantName, constantDescriptor) =>
            loadDynamicConstant(
                constantBSM, constantName, constantDescriptor, instructions, classFile
            )
    }

    /**
     * Generate instruction to load a Class object for the given FieldType.
     */
    private def loadFieldType(fieldType: FieldType, instructions: InstructionsBuilder): Int = {
        if (fieldType.isBaseType)
            instructions ++= GETSTATIC(fieldType.asBaseType.WrapperType, "TYPE", ObjectType.Class)
        else
            instructions ++= LoadClass_W(fieldType.asReferenceType)

        1
    }

    /**
     * Generate instructions to read a static field from a VarHandle for a dynamically loaded class.
     */
    private def doGetStatic(
        classConstant: DynamicConstant,
        name:          String,
        fieldType:     FieldType,
        instructions:  InstructionsBuilder,
        classFile:     ClassFile,
        boxed:         Boolean
    ): (Int, ClassFile) = {
        val (varHandleStack, updatedClassFile) = fieldVarHandle( // Get a suitable Varhandle
            name,
            classConstant,
            cf => (loadFieldType(fieldType, instructions), cf),
            isStaticField = true,
            instructions,
            classFile
        )
        instructions ++= ICONST_0 // The get method for a static VarHandle takes empty varargs
        instructions ++= ANEWARRAY(ObjectType.Object)
        instructions ++= INVOKEVIRTUAL(
            ObjectType.VarHandle,
            "get",
            MethodDescriptor(ArrayType.ArrayOfObject, ObjectType.Object)
        )

        adaptReturnType(fieldType, boxed, instructions)

        (Math.max(varHandleStack, 2), updatedClassFile)
    }

    /**
     * Generate instructions to invoke a given MethodHandle.
     */
    private def invokeMethodHandle(
        methodHandle: ConstantValue[_],
        arguments:    ArraySeq[ConstantValue[_]],
        returnType:   FieldType,
        instructions: InstructionsBuilder,
        classFile:    ClassFile,
        boxed:        Boolean
    ): (Int, ClassFile) = {
        // Load the MethodHandle and keep track of its return type if we know it
        val (actualReturnType, mhStack, newClassFile) = methodHandle match {
            case mh: MethodCallMethodHandle =>
                instructions ++= LoadMethodHandle_W(mh)
                (Some(mh.methodDescriptor.returnType), 1, classFile)

            case mh: FieldReadAccessMethodHandle =>
                instructions ++= LoadMethodHandle_W(mh)
                (Some(mh.fieldType), 1, classFile)

            case mh: FieldWriteAccessMethodHandle =>
                instructions ++= LoadMethodHandle_W(mh)
                (Some(VoidType), 1, classFile)

            case DynamicConstant(constantBSM, constantName, constantDescriptor) =>
                val (loadConstantStack, newClassFile) = loadDynamicConstant(
                    constantBSM, constantName, constantDescriptor, instructions, classFile
                )
                (None, loadConstantStack, newClassFile)
        }

        var updatedClassFile = newClassFile

        // If the return type is unknown or doesn't match the required, adapt it
        val returnTypeStack = if (actualReturnType.isEmpty || actualReturnType.get != returnType) {
            instructions ++= DUP

            instructions ++= INVOKEVIRTUAL( // Get the full method type
                ObjectType.MethodHandle,
                "type",
                MethodDescriptor.withNoArgs(ObjectType.MethodType)
            )

            val fieldTypeStack = loadFieldType(returnType, instructions) // Load the new return type

            instructions ++= INVOKEVIRTUAL( // Adapt the method type
                ObjectType.MethodType,
                "changeReturnType",
                MethodDescriptor(ObjectType.Class, ObjectType.MethodType)
            )

            instructions ++= INVOKEVIRTUAL( // Adapt the method handle
                ObjectType.MethodHandle,
                "asType",
                MethodDescriptor(ObjectType.MethodType, ObjectType.MethodHandle)
            )

            1 + fieldTypeStack
        } else 0

        // Load the bootstrap arguments into a varargs array
        instructions ++= LoadConstantInstruction(arguments.size)

        instructions ++= ANEWARRAY(ObjectType.Object)

        var argumentStack = 0
        arguments.iterator.zipWithIndex foreach {
            case (argument, index) =>
                instructions ++= DUP
                instructions ++= LoadConstantInstruction(index)
                val (bsaStack, newClassFile) =
                    loadBootstrapArgument(argument, instructions, updatedClassFile, boxed = true)
                updatedClassFile = newClassFile
                instructions ++= AASTORE
                argumentStack = Math.max(argumentStack, 2 + bsaStack)
        }

        instructions ++= INVOKEVIRTUAL( // Actually invoke the method handle
            ObjectType.MethodHandle,
            "invokeWithArguments",
            MethodDescriptor(ArrayType.ArrayOfObject, ObjectType.Object)
        )

        adaptReturnType(returnType, boxed, instructions)

        (Iterator(mhStack, 1 + returnTypeStack, 2 + argumentStack).max, updatedClassFile)
    }

    private def adaptReturnType(
        returnType:   FieldType,
        boxed:        Boolean,
        instructions: InstructionsBuilder
    ): Unit = {
        if (returnType.isBaseType) {
            val wrapper = returnType.asBaseType.WrapperType
            instructions ++= CHECKCAST(wrapper)
            if (!boxed)
                instructions ++= ObjectType.unboxValue(wrapper)
        } else
            instructions ++= CHECKCAST(returnType.asReferenceType)
    }

    /**
     * Creates the code required to perform the action of a method created by
     * java.lang.runtime.ObjectMethods.bootstrap.
     *
     * @param bootstrapArgs The arguments to the bootstrap method
     * @param methodName Name of the represented method (either equals, toString or hashCode)
     * @param newMethodName The name for the newly created method
     * @return Either a tuple of the updated class file and the descriptor of the new method or None
     */
    protected[this] def createObjectMethodsTarget(
        bootstrapArgs: BootstrapArguments,
        methodName:    String,
        newMethodName: String,
        classFile:     ClassFile
    ): Option[(ClassFile, MethodDescriptor)] = {
        val recordType = bootstrapArgs.head match {
            case ConstantClass(rt) => rt.asObjectType
            case _                 => return None;
        }

        val getters = bootstrapArgs.drop(2) map {
            case mh: GetFieldMethodHandle => mh
            case _                        => return None;
        }

        /*
        Make enough space for any of the methods. Exact sizes are (-5 if there is no getter):
        - equals:   16 + getters.size * 28
        - toString: 25 + getters.size * 20 + (getters.size - 1) * 6
        - hashCode:  7 + getters.size * 18
         */
        val body = new InstructionsBuilder(20 + getters.size * 28)

        def prepareArgumentArray(hasTwoParameters: Boolean = false): Unit = {
            body ++= ICONST_1
            body ++= ANEWARRAY(ObjectType.Object)
            body ++= (if (hasTwoParameters) ASTORE_2 else ASTORE_1)
        }

        // Loads a component (boxed), maxStack = 5
        def loadComponent(
            getter:           MethodHandle,
            hasTwoParameters: Boolean      = false,
            forSecondRecord:  Boolean      = false
        ): Unit = {
            body ++= LoadConstantInstruction(getter, wide = true)

            body ++= (if (hasTwoParameters) ALOAD_2 else ALOAD_1)
            body ++= DUP
            body ++= ICONST_0
            body ++= (if (forSecondRecord) ALOAD_1 else ALOAD_0)
            body ++= AASTORE

            body ++= INVOKEVIRTUAL(
                ObjectType.MethodHandle,
                "invokeWithArguments",
                MethodDescriptor(ArrayType.ArrayOfObject, ObjectType.Object)
            )
        }

        val targetDescriptor = methodName match {
            case "equals" =>
                body ++= ALOAD_1 // if(!other instanceof recordType) return false;
                body ++= INSTANCEOF(recordType)
                body ++= IFNE(5)
                body ++= ICONST_0
                body ++= IRETURN

                if (getters.nonEmpty)
                    prepareArgumentArray(hasTwoParameters = true)

                getters.iterator.zipWithIndex foreach {
                    case (getter, index) =>
                        loadComponent(getter, hasTwoParameters = true)
                        loadComponent(getter, hasTwoParameters = true, forSecondRecord = true)

                        body ++= INVOKESTATIC(
                            ObjectType.Objects,
                            isInterface = false,
                            "equals",
                            MethodDescriptor(
                                ArraySeq(ObjectType.Object, ObjectType.Object),
                                BooleanType
                            )
                        )
                        // if(!this.component().equals(other.component()) return false;
                        body ++= IFEQ(-32 - index * 28)
                }

                body ++= ICONST_1 // return true;
                body ++= IRETURN

                MethodDescriptor(ArraySeq(recordType, ObjectType.Object), BooleanType)

            case "toString" =>
                val components = bootstrapArgs(1) match {
                    case ConstantString(cs) => cs.split(';')
                    case _                  => return None;
                }

                def appendString(s: String): Unit = {
                    body ++= LoadString_W(s)
                    body ++= INVOKEVIRTUAL(
                        ObjectType.StringBuilder,
                        "append",
                        MethodDescriptor(ObjectType.String, ObjectType.StringBuilder)
                    )
                }

                body ++= NEW(ObjectType.StringBuilder)
                body ++= DUP

                body ++= LoadString_W(recordType.simpleName + '[')
                body ++= INVOKESPECIAL(
                    ObjectType.StringBuilder,
                    isInterface = false,
                    "",
                    MethodDescriptor.JustTakes(ObjectType.String)
                )

                if (getters.nonEmpty)
                    prepareArgumentArray()

                var first = true
                components.zip(getters) foreach {
                    case (component, getter) =>
                        if (first) first = false
                        else appendString(", ")

                        appendString(component + '=')

                        loadComponent(getter)
                        body ++= INVOKEVIRTUAL(
                            ObjectType.StringBuilder,
                            "append",
                            MethodDescriptor(ObjectType.Object, ObjectType.StringBuilder)
                        )
                }

                body ++= LoadInt_W(']')
                body ++= INVOKEVIRTUAL(
                    ObjectType.StringBuilder,
                    "append",
                    MethodDescriptor(CharType, ObjectType.StringBuilder)
                )

                body ++= INVOKEVIRTUAL(ObjectType.StringBuilder, "toString", JustReturnsString)
                body ++= ARETURN

                MethodDescriptor(recordType, ObjectType.String)

            case "hashCode" =>
                body ++= ICONST_0 // int hashCode = 0;

                if (getters.nonEmpty)
                    prepareArgumentArray()

                getters foreach { getter =>
                    body ++= BIPUSH(31) // hashCode = hashCode * 31 + component.hashCode();
                    body ++= IMUL

                    loadComponent(getter)

                    body ++= INVOKESTATIC(
                        ObjectType.Objects,
                        isInterface = false,
                        "hashCode",
                        MethodDescriptor.apply(ObjectType.Object, IntegerType)
                    )
                    body ++= IADD
                }

                body ++= IRETURN // return hashCode;

                MethodDescriptor(recordType, IntegerType)

            case _ =>
                return None;
        }

        val maxStack = 6 // in all methods, the max stack is 1 value + the maxStack of loadComponent
        val maxLocals = targetDescriptor.requiredRegisters + (if (getters.isEmpty) 0 else 1)

        val attributes = if ("equals" == methodName) {
            ArraySeq(StackMapTable(ArraySeq(SameFrame(7), SameFrame(1))))
        } else {
            ArraySeq.empty
        }

        val code = Code(maxStack, maxLocals, body.result(), NoExceptionHandlers, attributes)

        // Access flags for the target method are `/* SYNTHETIC */ private static`
        val accessFlags = ACC_SYNTHETIC.mask | ACC_PRIVATE.mask | ACC_STATIC.mask

        val targetMethod = Method(accessFlags, newMethodName, targetDescriptor, ArraySeq(code))

        val updatedClassFile = classFile._UNSAFE_addMethod(targetMethod)

        Some((updatedClassFile, targetDescriptor))
    }

    /**
     * Replaces each of several characters in a String with a given corresponding character.
     */
    protected[this] def replaceChars(in: String, oldChars: String, newChars: String): String = {
        var result = in
        for ((oldC, newC) <- oldChars.zip(newChars)) {
            result = result.replace(oldC, newC)
        }
        result
    }

    /**
     * Generates a new, internal name for a method to be inserted.
     *
     * It follows the pattern:
     * `$targetMethodName${surroundingMethodName}{surroundingMethodDescriptor}:pc`, where
     * surroundingMethodDescriptor is the JVM descriptor of the method sanitized to not contain
     * characters illegal in method names (replacing /, [, ;, < and > by $, ], :, _ and _
     * respectively) and where pc is the pc of the invokedynamic that is rewritten.
     */
    protected[this] def newTargetMethodName(
        cp:                               Constant_Pool,
        surroundingMethodNameIndex:       Int,
        surroundingMethodDescriptorIndex: Int,
        pc:                               Int,
        targetMethodName:                 String
    ): String = {
        val methodName = cp(surroundingMethodNameIndex).asString match {
            case ""   => "$constructor$"
            case "" => "$static_initializer"
            case name       => name
        }
        val descriptor = cp(surroundingMethodDescriptorIndex).asMethodDescriptor.toJVMDescriptor
        val sanitizedDescriptor = replaceChars(descriptor, "/[;<>", "$]:__")
        s"$$$targetMethodName$$$methodName$sanitizedDescriptor:$pc"
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy