/* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj package br package reader import java.lang.invoke.LambdaMetafactory import com.typesafe.config.Config import com.typesafe.config.ConfigValueFactory import org.opalj.log.Info import org.opalj.log.OPALLogger import org.opalj.log.OPALLogger.error import org.opalj.log.OPALLogger.info import org.opalj.log.StandardLogMessage import org.opalj.collection.immutable.UIDSet import org.opalj.bi.ACC_PRIVATE import org.opalj.bi.ACC_PUBLIC import org.opalj.bi.ACC_STATIC import org.opalj.bi.ACC_SYNTHETIC import org.opalj.bi.AccessFlags import org.opalj.br.MethodDescriptor.JustReturnsString import org.opalj.br.MethodDescriptor.LambdaAltMetafactoryDescriptor import org.opalj.br.MethodDescriptor.LambdaMetafactoryDescriptor import org.opalj.br.collection.mutable.InstructionsBuilder import org.opalj.br.instructions._ import org.opalj.br.instructions.ClassFileFactory.AlternativeFactoryMethodName import org.opalj.br.instructions.ClassFileFactory.DefaultFactoryMethodName import scala.collection.IndexedSeqView import scala.collection.immutable.ArraySeq /** * Provides support for rewriting Java 8/Scala lambda or method reference expressions that * were compiled to [[org.opalj.br.instructions.INVOKEDYNAMIC]] instructions. * This trait should be mixed in alongside a [[BytecodeReaderAndBinding]], which extracts * basic `invokedynamic` information from the [[BootstrapMethodTable]]. * * Specifically, whenever an `invokedynamic` instruction is encountered that is the result * of a lambda/method reference expression compiled by Oracle's JDK8, it creates a proxy * class file that represents the synthetic object that the JVM generates after executing * the `invokedynamic` call site. This proxy is then stored in the temporary ClassFile * attribute [[SynthesizedClassFiles]]. All such ClassFiles will * be picked up later for inclusion in the project. * * @see [[https://mydailyjava.blogspot.de/2015/03/dismantling-invokedynamic.html DismantlingInvokeDynamic]] * for further information. * * @author Arne Lottmann * @author Michael Eichberg * @author Andreas Muttscheller * @author Dominik Helm */ trait InvokedynamicRewriting extends DeferredInvokedynamicResolution with BootstrapArgumentLoading { this: ClassFileBinding => import InvokedynamicRewriting._ val performInvokedynamicRewriting: Boolean = { import InvokedynamicRewriting.{InvokedynamicRewritingConfigKey => Key} val rewrite: Boolean = try { config.getBoolean(Key) } catch { case t: Throwable => error("class file reader", s"couldn't read: $Key", t) false } if (rewrite) { info( "class file reader", "invokedynamics are rewritten" ) } else { info( "class file reader", "invokedynamics are not rewritten" ) } rewrite } val logLambdaExpressionsRewrites: Boolean = { import InvokedynamicRewriting.{LambdaExpressionsLogRewritingsConfigKey => Key} val logRewrites: Boolean = try { config.getBoolean(Key) } catch { case t: Throwable => error("class file reader", s"couldn't read: $Key", t) false } if (logRewrites) { info( "class file reader", "rewrites of LambdaMetaFactory based invokedynamics are logged" ) } else { info( "class file reader", "rewrites of LambdaMetaFactory based invokedynamics are not logged" ) } logRewrites } val logStringConcatRewrites: Boolean = { import InvokedynamicRewriting.{StringConcatLogRewritingsConfigKey => Key} val logRewrites: Boolean = try { config.getBoolean(Key) } catch { case t: Throwable => error("class file reader", s"couldn't read: $Key", t) false } if (logRewrites) { info( "class file reader", "rewrites of StringConcatFactory based invokedynamics are logged" ) } else { info( "class file reader", "rewrites of StringConcatFactory based invokedynamics are not logged" ) } logRewrites } val logObjectMethodsRewrites: Boolean = { import InvokedynamicRewriting.{ObjectMethodsLogRewritingsConfigKey => Key} val logRewrites: Boolean = try { config.getBoolean(Key) } catch { case t: Throwable => error("class file reader", s"couldn't read: $Key", t) false } if (logRewrites) { info( "class file reader", "rewrites of StringConcatFactory based invokedynamics are logged" ) } else { info( "class file reader", "rewrites of StringConcatFactory based invokedynamics are not logged" ) } logRewrites } val logUnknownInvokeDynamics: Boolean = { import InvokedynamicRewriting.{InvokedynamicLogUnknownInvokeDynamicsConfigKey => Key} val logUnknownInvokeDynamics: Boolean = try { config.getBoolean(Key) } catch { case t: Throwable => error("class file reader", s"couldn't read: $Key", t) false } if (logUnknownInvokeDynamics) { info("class file reader", "unknown invokedynamics are logged") } else { info("class file reader", "unknown invokedynamics are not logged") } logUnknownInvokeDynamics } val ScalaRuntimeObject: ObjectType = ObjectType("scala/runtime/ScalaRunTime$") /** * Generates a new, internal name for the proxy class for a rewritten invokedynamic. * * It follows the pattern: * `{surroundingType.simpleName}${surroundingMethodName}{surroundingMethodDescriptor}:pc$Lambda` * where surroundingMethodDescriptor is the JVM descriptor of the method sanitized to not * contain characters illegal in type names (replacing /, [ and ; by $, ] and : respectively) * and where pc is the pc of the invokedynamic that is rewritten. * For example: `Foo$bar()I:5` would refer to the invokedynamic instruction at pc 5 of the * method `bar` in class `Foo` that takes no parameters and returns an int. * * @param surroundingType the type in which the Lambda expression has been found */ private def newLambdaTypeName( surroundingType: ObjectType, surroundingMethodName: String, surroundingMethodDescriptor: MethodDescriptor, pc: Int ): String = { val descriptor = surroundingMethodDescriptor.toJVMDescriptor val sanitizedDescriptor = replaceChars(descriptor, "/[;", "$]:") s"${surroundingType.packageName}/${surroundingType.simpleName}$$"+ s"$surroundingMethodName$sanitizedDescriptor:$pc$$Lambda" } override def deferredInvokedynamicResolution( classFile: ClassFile, cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, invokeDynamicInfo: CONSTANT_InvokeDynamic_info, instructions: Array[Instruction], pc: PC ): ClassFile = { // gather complete information about invokedynamic instructions from the bootstrap // method table val updatedClassFile = super.deferredInvokedynamicResolution( classFile, cp, methodNameIndex, methodDescriptorIndex, invokeDynamicInfo, instructions, pc ) if (!performInvokedynamicRewriting) return updatedClassFile; val invokedynamic = instructions(pc).asInstanceOf[INVOKEDYNAMIC] if (InvokedynamicRewriting.isJava8LikeLambdaExpression(invokedynamic)) { java8LambdaResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, updatedClassFile, instructions, pc, invokedynamic ) } else if (isJava10StringConcatInvokedynamic(invokedynamic)) { java10StringConcatResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, updatedClassFile, instructions, pc, invokedynamic ) } else if (isObjectMethodsInvokedynamic(invokedynamic)) { objectMethodsResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, updatedClassFile, instructions, pc, invokedynamic ) } else if (isScalaLambdaDeserializeExpression(invokedynamic)) { scalaLambdaDeserializeResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, updatedClassFile, instructions, pc, invokedynamic ) } else if (isScalaSymbolExpression(invokedynamic)) { scalaSymbolResolution( updatedClassFile, instructions, pc, invokedynamic, cp, methodNameIndex, methodDescriptorIndex ) } else if (isScalaStructuralCallSite(invokedynamic, instructions, pc)) { scalaStructuralCallSiteResolution( updatedClassFile, instructions, pc, invokedynamic, cp, methodNameIndex, methodDescriptorIndex ) } else if (isGroovyInvokedynamic(invokedynamic)) { if (logUnknownInvokeDynamics) { OPALLogger.logOnce(StandardLogMessage( Info, Some("load-time transformation"), "Groovy's INVOKEDYNAMICs are not rewritten" )) } updatedClassFile } else if (isDynamoInvokedynamic(invokedynamic)) { if (logUnknownInvokeDynamics) { OPALLogger.logOnce(StandardLogMessage( Info, Some("load-time transformation"), "org.dynamo's INVOKEDYNAMICs are not rewritten" )) } updatedClassFile } else { if (logUnknownInvokeDynamics) { val t = classFile.thisType.toJava info("load-time transformation", s"$t - unresolvable INVOKEDYNAMIC: $invokedynamic") } updatedClassFile } } /** * Resolution of Java 10 string concat expressions. * * @see More information about string concat factory: * [https://docs.oracle.com/javase/10/docs/api/java/lang/invoke/StringConcatFactory.html] * * @param classFile The classfile to parse. * @param instructions The instructions of the method we are currently parsing. * @param pc The program counter of the current instruction. * @param invokedynamic The INVOKEDYNAMIC instruction we want to replace. * @return A classfile which has the INVOKEDYNAMIC instruction replaced. */ private def java10StringConcatResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, classFile: ClassFile, instructions: Array[Instruction], pc: PC, invokedynamic: INVOKEDYNAMIC ): ClassFile = { val INVOKEDYNAMIC(bootstrapMethod, _, factoryDescriptor) = invokedynamic val args = bootstrapMethod.arguments // Extract the recipe and static arguments if present (if not, the dynamic arguments are // simply concatenated) val (recipe, staticArgs) = if (args.isEmpty) ( None, ArraySeq.empty[ConstantValue[_]].view ) else args.head match { case recipe: ConstantString => ( Some(recipe), args.view.slice(from = 1, until = args.length).asInstanceOf[IndexedSeqView[ConstantValue[_]]] ) case _ => if (logUnknownInvokeDynamics) { val t = classFile.thisType.toJava info( "load-time transformation", s"$t - unresolvable INVOKEDYNAMIC (unknown string recipe): "+ invokedynamic ) } return classFile; } var updatedClassFile = classFile // Creates concat method def createConcatMethod( name: String, descriptor: MethodDescriptor, recipeO: Option[ConstantString], constants: IndexedSeqView[ConstantValue[_]] ): MethodTemplate = { // A guess on the number of append operations required, need not be precise val numEntries = if (recipeO.isDefined) recipeO.get.value.length else descriptor.parametersCount val body: InstructionsBuilder = new InstructionsBuilder(11 + 6 * numEntries) body ++= NEW(ObjectType.StringBuilder) body ++= DUP body ++= INVOKESPECIAL( ObjectType.StringBuilder, isInterface = false, "", MethodDescriptor.NoArgsAndReturnVoid ) def appendType(t: Type): FieldType = { if (t.isBaseType) t.asBaseType else if (t eq ObjectType.String) ObjectType.String else ObjectType.Object } // Generate instructions to append a parameter to the StringBuilder def appendParam(paramType: FieldType, index: Int): Unit = { val isWide = index > 255 if (isWide) body ++= WIDE val load = LoadLocalVariableInstruction(paramType, index) body ++= (load, load.indexOfNextInstruction(0, modifiedByWide = isWide)) body ++= INVOKEVIRTUAL( ObjectType.StringBuilder, "append", MethodDescriptor(appendType(paramType), ObjectType.StringBuilder) ) } // Generate instructions to append a static constant to the StringBuilder def appendConstant(constant: ConstantValue[_]): Int = { val (constantStack, newClassFile) = loadBootstrapArgument(constant, body, updatedClassFile) updatedClassFile = newClassFile body ++= INVOKEVIRTUAL( ObjectType.StringBuilder, "append", MethodDescriptor( appendType(constant.runtimeValueType), ObjectType.StringBuilder ) ) constantStack } // Generate code to append a substring of the recipe verbatim to the StringBuilder def appendString(startIndex: Int, endIndex: Int): Unit = { // Use `substring` to extract the relevant part of the recipe // Loading the recipe with `LDC` is safe, as the removal of the bootstrapMethod will // free one index in the constant pool where the recipe can be moved on // serialization of the class body ++= LDC(recipeO.get) body ++= LoadConstantInstruction(startIndex) body ++= LoadConstantInstruction(endIndex) body ++= INVOKEVIRTUAL( ObjectType.String, "substring", MethodDescriptor(ArraySeq(IntegerType, IntegerType), ObjectType.String) ) body ++= INVOKEVIRTUAL( ObjectType.StringBuilder, "append", MethodDescriptor(ObjectType.String, ObjectType.StringBuilder) ) } var lvIndex = 0 // Local variable index for the next parameter // At least 2 (for StringBuilder + param for append), but increased below if necessary var maxStack = 2 if (recipeO.isDefined) { val recipe = recipeO.get.value // Index of next parameter (counting +1, while lvIndex respects operand sizes var paramIndex = 0 // Index of next constant (from bootstrap arguments) var constantIndex = 0 // Character position in the recipe where processing continues var recipeIndex = 0 var nextParam = recipe.indexOf('\u0001') // Next parameter insertion point var nextConstant = recipe.indexOf('\u0002') // Next constant insertion point var nextInsert = 0 // Next insertion point (parameter or constant) while (recipeIndex < recipe.length) { // Next insertion point is smaller of nextParam/nextConstant if each of those // exist. If they don't exist, use recipe.length to append last verbatim part // (if any) and terminate nextInsert = if (nextParam == -1) if (nextConstant == -1) recipe.length else nextConstant else if (nextConstant == -1) nextParam else Math.min(nextParam, nextConstant) // Append parts from recipe between insertion points verbatim if (nextInsert > recipeIndex) { appendString(recipeIndex, nextInsert) maxStack = 4 // StringBuilder, String, BeginIndex, EndIndex } if (nextInsert < recipe.length) { if (nextInsert == nextParam) { assert(recipe.charAt(nextInsert) == '\u0001') val paramType = descriptor.parameterType(paramIndex) appendParam(paramType, lvIndex) val opSize = paramType.computationalType.operandSize if (maxStack == 2 && opSize == 2) maxStack = 3 lvIndex += opSize paramIndex += 1 nextParam = recipe.indexOf('\u0001', nextParam + 1) } else { assert(recipe.charAt(nextInsert) == '\u0002') val constant = constants(constantIndex) val constantStack = appendConstant(constant) maxStack = Math.max(maxStack, constantStack + 1) constantIndex += 1 nextConstant = recipe.indexOf('\u0002', nextConstant + 1) } } recipeIndex = nextInsert + 1 // skip after current insertion point } } else { descriptor.parameterTypes.foreach { paramType => appendParam(paramType, lvIndex) val opSize = paramType.computationalType.operandSize if (maxStack == 2 && opSize == 2) maxStack = 3 lvIndex += opSize } } // Finally, turn StringBuilder to String body ++= INVOKEVIRTUAL(ObjectType.StringBuilder, "toString", JustReturnsString) body ++= ARETURN val maxLocals = descriptor.requiredRegisters val code = Code(maxStack, maxLocals, body.result(), NoExceptionHandlers, NoAttributes) // Access flags for the concat are `/* SYNTHETIC */ private static` val accessFlags = ACC_SYNTHETIC.mask | ACC_PRIVATE.mask | ACC_STATIC.mask Method(accessFlags, name, descriptor, ArraySeq(code)) } val concatName = newTargetMethodName(cp, methodNameIndex, methodDescriptorIndex, pc, "string_concat") val concatMethod = createConcatMethod(concatName, factoryDescriptor, recipe, staticArgs) updatedClassFile = updatedClassFile._UNSAFE_addMethod(concatMethod) val newInvokestatic = INVOKESTATIC( classFile.thisType, isInterface = classFile.isInterfaceDeclaration, concatName, factoryDescriptor ) if (logStringConcatRewrites) { info("rewriting invokedynamic", s"Java: $invokedynamic => $newInvokestatic") } instructions(pc) = newInvokestatic // since invokestatic is two bytes shorter than invokedynamic, we need to fill the // two-byte gap following the invokestatic with NOPs instructions(pc + 3) = NOP instructions(pc + 4) = NOP updatedClassFile } /** * Resolution of bootstrap methods from java.lang.runtime.ObjectMethods used for Records (Java * 16). * * @see More information about ObjectMethods: * [https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/runtime/ObjectMethods.html] * * @param classFile The classfile to parse. * @param instructions The instructions of the method we are currently parsing. * @param pc The program counter of the current instruction. * @param invokedynamic The INVOKEDYNAMIC instruction we want to replace. * @return A classfile which has the INVOKEDYNAMIC instruction replaced. */ private def objectMethodsResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, classFile: ClassFile, instructions: Array[Instruction], pc: PC, invokedynamic: INVOKEDYNAMIC ): ClassFile = { val INVOKEDYNAMIC(bootstrapMethod, targetMethodName, _) = invokedynamic val newMethodName = newTargetMethodName(cp, methodNameIndex, methodDescriptorIndex, pc, "object_methods") val (updatedClassFile, newMethodDescriptor) = createObjectMethodsTarget( bootstrapMethod.arguments, targetMethodName, newMethodName, classFile ).getOrElse { if (logUnknownInvokeDynamics) { val t = classFile.thisType.toJava info( "load-time transformation", s"$t - unresolvable INVOKEDYNAMIC: $invokedynamic" ) } return classFile; } val newInvokestatic = INVOKESTATIC( classFile.thisType, isInterface = classFile.isInterfaceDeclaration, newMethodName, newMethodDescriptor ) if (logObjectMethodsRewrites) { info("rewriting invokedynamic", s"Java: $invokedynamic => $newInvokestatic") } instructions(pc) = newInvokestatic // since invokestatic is two bytes shorter than invokedynamic, we need to fill the // two-byte gap following the invokestatic with NOPs instructions(pc + 3) = NOP instructions(pc + 4) = NOP updatedClassFile } /** * Resolve invokedynamic instructions introduced by scala.deprecatedName. * * @param classFile The classfile to parse * @param instructions The instructions of the method we are currently parsing * @param pc The program counter of the current instuction * @param invokedynamic The INVOKEDYNAMIC instruction we want to replace * @return A classfile which has the INVOKEDYNAMIC instruction replaced */ private def scalaSymbolResolution( classFile: ClassFile, instructions: Array[Instruction], pc: PC, invokedynamic: INVOKEDYNAMIC, cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index ): ClassFile = { // IMPROVE Rewrite to avoid that we have to use a constant pool entry in the range [0..255] val INVOKEDYNAMIC( bootstrapMethod, _, _ // functionalInterfaceMethodName, factoryDescriptor ) = invokedynamic val bootstrapArguments = bootstrapMethod.arguments val newInvokestatic = INVOKESTATIC( ObjectType.ScalaSymbol, isInterface = false, // the created proxy class is always a concrete class "apply", // the invokedynamic's methodDescriptor (factoryDescriptor) determines // the parameters that are actually pushed and popped from/to the stack MethodDescriptor(ArraySeq(ObjectType.String), ObjectType.ScalaSymbol) ) if (logLambdaExpressionsRewrites) { info("rewriting invokedynamic", s"Scala: $invokedynamic => $newInvokestatic") } val instructionsBuilder = new InstructionsBuilder(7) val (maxStack, newClassFile) = loadBootstrapArgument( bootstrapArguments.head.asInstanceOf[ConstantValue[_]], instructionsBuilder, classFile ) var updatedClassFile = newClassFile instructionsBuilder ++= newInvokestatic instructionsBuilder ++= ARETURN val newInstructions = instructionsBuilder.result() val firstInstruction = newInstructions.head if (newInstructions.length == 7 && firstInstruction.opcode == LDC_W.opcode) { instructions(pc) = firstInstruction match { case LoadString_W(s) => LoadString(s) case LoadDynamic_W(bsm, name, descriptor) => LoadDynamic(bsm, name, descriptor) } instructions(pc + 2) = newInvokestatic } else { val newMethodName = newTargetMethodName(cp, methodNameIndex, methodDescriptorIndex, pc, "scala_symbol") val newMethod = Method( ACC_SYNTHETIC.mask | ACC_PRIVATE.mask | ACC_STATIC.mask, newMethodName, MethodDescriptor.withNoArgs(ObjectType.ScalaSymbol), ArraySeq(Code(maxStack, 0, newInstructions, NoExceptionHandlers, NoAttributes)) ) updatedClassFile = updatedClassFile._UNSAFE_addMethod(newMethod) val newInvoke = INVOKESTATIC( classFile.thisType, isInterface = classFile.isInterfaceDeclaration, newMethodName, MethodDescriptor.withNoArgs(ObjectType.ScalaSymbol) ) instructions(pc) = newInvoke instructions(pc + 3) = NOP instructions(pc + 4) = NOP } updatedClassFile } /** * Remove invokedynamic instructions that use Scala's StructuralCallSite to implement caching * of resolved methods. * * @param classFile The classfile to parse * @param instructions The instructions of the method we are currently parsing * @param pc The program counter of the current instuction * @param invokedynamic The INVOKEDYNAMIC instruction we want to replace * @return A classfile which has the INVOKEDYNAMIC instruction replaced */ private def scalaStructuralCallSiteResolution( classFile: ClassFile, instructions: Array[Instruction], pc: PC, invokedynamic: INVOKEDYNAMIC, cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index ): ClassFile = { val methodType = invokedynamic.bootstrapMethod.arguments.head.asInstanceOf[ConstantValue[_]] val body = new InstructionsBuilder(18) body ++= GETSTATIC(ScalaRuntimeObject, "MODULE$", ScalaRuntimeObject) body ++= ALOAD_0 body ++= instructions(22).asInstanceOf[LoadString] val (methodTypeStack, updatedClassFile) = loadBootstrapArgument(methodType, body, classFile) body ++= INVOKEVIRTUAL( ObjectType.MethodType, "parameterArray", MethodDescriptor.withNoArgs(ArrayType(ObjectType.Class)) ) body ++= instructions(28).asMethodInvocationInstruction // .getMethod body ++= instructions(31).asMethodInvocationInstruction // .ensureAccessible body ++= ARETURN if (logLambdaExpressionsRewrites) { info("rewriting invokedynamic", s"Scala: Removed $invokedynamic") } val maxStack = 3 + methodTypeStack val methodName = cp(methodNameIndex).asString val methodDescriptor = cp(methodDescriptorIndex).asMethodDescriptor val methodToRewrite = classFile.findMethod(methodName, methodDescriptor).get val code = Code(maxStack, 1, body.result(), NoExceptionHandlers, NoAttributes) val rewrittenMethod = Method(methodToRewrite.accessFlags, methodName, methodDescriptor, ArraySeq(code)) updatedClassFile._UNSAFE_replaceMethod(methodToRewrite, rewrittenMethod) } /** * The scala compiler (and possibly other compilers targeting JVM bytecode) add a * `$deserializeLambda$`, which handles validation of lambda methods if the lambda is * Serializable. This method is called when the serialized lambda is deserialized. * For scala 2.12, it includes an INVOKEDYNAMIC instruction. This one has to be replaced with * calls to `LambdaDeserialize::deserializeLambda`, which is what the INVOKEDYNAMIC instruction * refers to, see [https://github.com/scala/scala/blob/v2.12.2/src/library/scala/runtime/LambdaDeserialize.java#L29]. * This is done to reduce the size of `$deserializeLambda$`. * * @note SerializedLambda has a readResolve method that looks for a (possibly private) static * method called $deserializeLambda$(SerializedLambda) in the capturing class, invokes * that with itself as the first argument, and returns the result. Lambda classes * implementing $deserializeLambda$ are responsible for validating that the properties * of the SerializedLambda are consistent with a lambda actually captured by that class. * See: [https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/SerializedLambda.html] * * @see More information about lambda deserialization: * - [https://zeroturnaround.com/rebellabs/java-8-revealed-lambdas-default-methods-and-bulk-data-operations/4/] * - [http://mail.openjdk.java.net/pipermail/mlvm-dev/2016-August/006699.html] * - [https://github.com/scala/scala/blob/v2.12.2/src/library/scala/runtime/LambdaDeserialize.java] * * @param classFile The classfile to parse * @param instructions The instructions of the method we are currently parsing * @param pc The program counter of the current instruction * @param invokedynamic The INVOKEDYNAMIC instruction we want to replace * @return A classfile which has the INVOKEDYNAMIC instruction replaced */ private def scalaLambdaDeserializeResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, classFile: ClassFile, instructions: Array[Instruction], pc: PC, invokedynamic: INVOKEDYNAMIC ): ClassFile = { val bootstrapArguments = invokedynamic.bootstrapMethod.arguments if (bootstrapArguments.nonEmpty) { if (bootstrapArguments exists { case _: InvokeStaticMethodHandle => false // As expected, static method handle case _ => true // Unknown (dynamic) argument }) { if (logUnknownInvokeDynamics) { val t = classFile.thisType.toJava info( "load-time transformation", s"$t - unresolvable INVOKEDYNAMIC (unknown method handle): $invokedynamic" ) } return classFile; } } val methodName = cp(methodNameIndex).asString val methodDescriptor = cp(methodDescriptorIndex).asMethodDescriptor val typeDeclaration = TypeDeclaration( ObjectType(newLambdaTypeName(classFile.thisType, methodName, methodDescriptor, pc)), isInterfaceType = false, Some(LambdaMetafactoryDescriptor.returnType.asObjectType), // we basically create a "CallSiteObject" UIDSet.empty ) val proxy: ClassFile = ClassFileFactory.DeserializeLambdaProxy( typeDeclaration, bootstrapArguments, DefaultDeserializeLambdaStaticMethodName ) val factoryMethod = proxy.findMethod(DefaultDeserializeLambdaStaticMethodName).head val newInvokestatic = INVOKESTATIC( proxy.thisType, isInterface = false, // the created proxy class is always a concrete class factoryMethod.name, MethodDescriptor( ArraySeq(ObjectType.SerializedLambda), ObjectType.Object ) ) if (logLambdaExpressionsRewrites) { info("rewriting invokedynamic", s"Scala: $invokedynamic => $newInvokestatic") } instructions(pc) = newInvokestatic // since invokestatic is two bytes shorter than invokedynamic, we need to fill // the two-byte gap following the invokestatic with NOPs instructions(pc + 3) = NOP instructions(pc + 4) = NOP val reason = Some((classFile, instructions, pc, invokedynamic, newInvokestatic)) storeProxy(classFile, proxy, reason) } /** * Resolution of java 8 lambda and method reference expressions. * * @see More information about lambda deserialization and lambda meta factory: * [https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html] * * @param classFile The classfile to parse. * @param instructions The instructions of the method we are currently parsing. * @param pc The program counter of the current instruction. * @param invokedynamic The INVOKEDYNAMIC instruction we want to replace. * @return A classfile which has the INVOKEDYNAMIC instruction replaced. */ private def java8LambdaResolution( cp: Constant_Pool, methodNameIndex: Constant_Pool_Index, methodDescriptorIndex: Constant_Pool_Index, classFile: ClassFile, instructions: Array[Instruction], pc: PC, invokedynamic: INVOKEDYNAMIC ): ClassFile = { val INVOKEDYNAMIC( bootstrapMethod, functionalInterfaceMethodName, factoryDescriptor ) = invokedynamic val bootstrapArguments = bootstrapMethod.arguments // Extract the arguments of the lambda factory. val ( samMethodType, // describing the implemented method type tempImplMethod, // the MethodHandle providing the implementation instantiatedMethodType, // allowing restrictions on invocation // The available information depends on the metafactory: // (e.g., about bridges or markers) altMetafactoryArgs ) = bootstrapArguments match { case Seq(smt: MethodDescriptor, tim: MethodCallMethodHandle, imt: MethodDescriptor, ama @ _*) => (smt, tim, imt, ama) case _ => if (logUnknownInvokeDynamics) { val t = classFile.thisType.toJava info( "load-time transformation", s"$t - unresolvable INVOKEDYNAMIC (unknown lambda factory argument): "+ invokedynamic ) } return classFile; } var implMethod = tempImplMethod val thisType = classFile.thisType val (markerInterfaces, bridges, serializable) = extractAltMetafactoryArguments(altMetafactoryArgs) val MethodCallMethodHandle( targetMethodOwner: ObjectType, targetMethodName, targetMethodDescriptor ) = implMethod // In case of nested classes, we have to change the invoke instruction from // invokespecial to invokevirtual, because the special handling used for private // methods doesn't apply anymore. implMethod match { case specialImplMethod: InvokeSpecialMethodHandle => implMethod = if (specialImplMethod.isInterface) InvokeInterfaceMethodHandle( implMethod.receiverType, implMethod.name, implMethod.methodDescriptor ) else InvokeVirtualMethodHandle( implMethod.receiverType, implMethod.name, implMethod.methodDescriptor ) case _ => } val superInterfaceTypesBuilder = UIDSet.newBuilder[ObjectType] superInterfaceTypesBuilder += factoryDescriptor.returnType.asObjectType if (serializable) { superInterfaceTypesBuilder += ObjectType.Serializable } markerInterfaces foreach { mi => superInterfaceTypesBuilder += mi.asObjectType } val methodName = cp(methodNameIndex).asString val methodDescriptor = cp(methodDescriptorIndex).asMethodDescriptor val typeDeclaration = TypeDeclaration( ObjectType(newLambdaTypeName(thisType, methodName, methodDescriptor, pc)), isInterfaceType = false, Some(ObjectType.Object), // we basically create a "CallSiteObject" superInterfaceTypesBuilder.result() ) var invocationInstruction = implMethod.opcodeOfUnderlyingInstruction /* Check the type of the invoke instruction using the instruction's opcode. targetMethodOwner identifies the class where the method is actually implemented. This is wrong for INVOKEVIRTUAL and INVOKEINTERFACE. The call to the proxy class is done with the actual class, not the class where the method is implemented. Therefore, the receiverType must be the class from the caller, not where the to- be-called method is implemented. E.g., LinkedHashSet.contains() is implemented in HashSet, but the receiverType and constructor parameter must be LinkedHashSet instead of HashSet. *** INVOKEVIRTUAL *** An INVOKEVIRTUAL is used when the method is defined by a class type (not an interface type). (e.g., LinkedHashSet.addAll()). This instruction requires a receiver object when the method reference uses a non-null object as a receiver. E.g.: LinkedHashSet lhs = new LinkedHashSet<>(); lhs::container() It does not have a receiver field in case of a class based method reference, e.g. LinkedHashSet::container() *** INVOKEINTERFACE *** It is similar to INVOKEVIRTUAL, but the method definition is defined in an interface. Therefore, the same rule like INVOKEVIRTUAL applies. *** INVOKESTATIC *** Because we call a static method, we don't have an instance. Therefore we don't need a receiver field. *** INVOKESPECIAL *** INVOKESPECIAL is used for: - instance initialization methods (i.e., constructors) -> Method is implemented in called class -> no rewrite necessary - private method invocation: The private method must be in the same class as the callee -> no rewrite needed - Invokation of methods using super keyword -> Not needed, because a synthetic method in the callee class is created which handles the INVOKESPECIAL. Therefore the receiverType is also the callee class. E.g. public static class Superclass { protected String someMethod() { return "someMethod"; } } public static class Subclass extends Superclass { public String callSomeMethod() { Supplier s = super::someMethod; return s.get(); } } The class Subclass contains a synthetic method `access`, which has an INVOKESPECIAL instruction calling Superclass.someMethod. The generated Lambda Proxyclass calls Subclass.access, so the receiverType must be Subclass insteaed of Superclass. More information: http://www.javaworld.com/article/2073578/java-s-synthetic-methods.html */ var receiverType = if (invocationInstruction != INVOKEVIRTUAL.opcode && invocationInstruction != INVOKEINTERFACE.opcode) { targetMethodOwner } else if (invokedynamic.methodDescriptor.parameterTypes.nonEmpty && invokedynamic.methodDescriptor.parameterTypes.head.isObjectType) { // If we have an instance of a object and use a method reference, // get the receiver type from the invokedynamic instruction. // It is the first parameter of the functional interface parameter // list. invokedynamic.methodDescriptor.parameterTypes.head.asObjectType } else if (instantiatedMethodType.parameterTypes.nonEmpty && instantiatedMethodType.parameterTypes.head.isObjectType) { // If we get a instance method reference like `LinkedHashSet::addAll`, get // the receiver type from the functional interface. The first parameter is // the instance where the method should be called. instantiatedMethodType.parameterTypes.head.asObjectType } else { targetMethodOwner } /* It is possible for the receiverType to be different from classFile. In this case, check if the receiverType is an interface instead of the classFile. The Proxy Factory must get the correct value to build the correct variant of the INVOKESTATIC instruction. */ var receiverIsInterface = implMethod match { case _: InvokeInterfaceMethodHandle => true case _: InvokeVirtualMethodHandle => false case _: NewInvokeSpecialMethodHandle => false case handle: InvokeSpecialMethodHandle => handle.isInterface case handle: InvokeStaticMethodHandle => // The following test was added to handle a case where the Scala // compiler generated invalid bytecode (the Scala compiler generated // a MethodRef instead of an InterfaceMethodRef which led to the // wrong kind of InvokeStaticMethodHandle). // See https://github.com/scala/bug/issues/10429 for further details. if (implMethod.receiverType eq classFile.thisType) { classFile.isInterfaceDeclaration } else { handle.isInterface } case other => throw new UnknownError("unexpected handle: "+other) } // Creates forwarding method for private method `m` that can be accessed by the proxy class. def createForwardingMethod( m: Method, name: String ): MethodTemplate = { // Access flags for the forwarder are the same as the target method but without private // modifier. For interfaces, ACC_PUBLIC is required and constructors are forwarded by // a static method. val accessFlags = if (receiverIsInterface) (m.accessFlags & ~ACC_PRIVATE.mask) | ACC_PUBLIC.mask else m.accessFlags & ~ACC_PRIVATE.mask // Instructions to push the parameters onto the stack val forwardParameters = ClassFileFactory.parameterForwardingInstructions( m.descriptor, m.descriptor, 1, Seq.empty, classFile.thisType ) val body = new InstructionsBuilder(7 + forwardParameters.length) // if the receiver method is not static, we need to push the receiver object // onto the stack by an ALOAD_0 unless we have a method reference where the receiver // will be explicitly provided body ++= ALOAD_0 body ++= forwardParameters val recType = classFile.thisType // The call instruction itself TODO what happens for nested classes? body ++= INVOKESPECIAL(recType, receiverIsInterface, m.name, m.descriptor) // The return instruction matching the return type body ++= ReturnInstruction(m.descriptor.returnType) val parametersStackSize = m.descriptor.requiredRegisters val maxLocals = 1 + parametersStackSize // parameters + `this` val maxStack = math.max(maxLocals, m.descriptor.returnType.operandSize) val code = Code(maxStack, maxLocals, body.result(), NoExceptionHandlers, NoAttributes) Method(accessFlags, name, m.descriptor, ArraySeq(code)) } val isInterface = classFile.isInterfaceDeclaration def lift(method: Method): MethodTemplate = { val accessFlags = if (isInterface) { // Interface methods must be private or public => public so proxy can // access it (method.accessFlags & ~ACC_PRIVATE.mask) | ACC_PUBLIC.mask } else { method.accessFlags & ~ACC_PRIVATE.mask // Package private, i.e. no modifier } method.copy(accessFlags = accessFlags) } var updatedClassFile = classFile if (targetMethodOwner eq classFile.thisType) { // If the target method is private, we have to generate a forwarding method that is // accessible by the proxy class, i.e. not private, or we have to lift the target method // (only for static methods and constructors, where the lifted method can not interfere with // inherited methods). val targetMethod = updatedClassFile.methods.find { m => m.name == targetMethodName && m.descriptor == targetMethodDescriptor } assert(targetMethod.isDefined) val m = targetMethod.get if (m.isPrivate) { if (m.isStatic || m.isConstructor) { updatedClassFile = updatedClassFile._UNSAFE_replaceMethod( targetMethod.get, lift(targetMethod.get) ) } else { val forwardingName = "$forward$"+m.name // Update the implMethod and other information to match the forwarder implMethod = if (isInterface) { InvokeInterfaceMethodHandle(thisType, forwardingName, m.descriptor) } else { InvokeVirtualMethodHandle(thisType, forwardingName, m.descriptor) } invocationInstruction = implMethod.opcodeOfUnderlyingInstruction receiverType = thisType receiverIsInterface = isInterface val forwarderO = updatedClassFile.findMethod(forwardingName, m.descriptor) if (forwarderO.isEmpty) { // Add forwarder if not already present val forwarder = createForwardingMethod(m, forwardingName) updatedClassFile = updatedClassFile._UNSAFE_addMethod(forwarder) } } } } // If the class is serializable, the $deserializeLambda$ method may need to be lifted if (serializable) { val deserialize = updatedClassFile.methods.find { m => m.hasFlags(AccessFlags.ACC_SYNTHETIC_STATIC_PRIVATE) && m.name == "$deserializeLambda$" } if (deserialize.isDefined) updatedClassFile = updatedClassFile._UNSAFE_replaceMethod( deserialize.get, lift(deserialize.get) ) } val needsBridgeMethod = samMethodType != instantiatedMethodType val bridgeMethodDescriptorBuilder = ArraySeq.newBuilder[MethodDescriptor] if (needsBridgeMethod) { bridgeMethodDescriptorBuilder += samMethodType } // If the bridge has the same method descriptor as the instantiatedMethodType or // samMethodType, they are already present in the proxy class. Do not add them again. // This happens in scala patternmatching for example. bridgeMethodDescriptorBuilder ++= bridges .filterNot(_ == samMethodType) .filterNot(_ == instantiatedMethodType) val bridgeMethodDescriptors = bridgeMethodDescriptorBuilder.result() val proxy: ClassFile = ClassFileFactory.Proxy( thisType, updatedClassFile.isInterfaceDeclaration, typeDeclaration, functionalInterfaceMethodName, instantiatedMethodType, receiverType, receiverIsInterface = receiverIsInterface, implMethod, invocationInstruction, samMethodType, bridgeMethodDescriptors ) val factoryMethod = { if (functionalInterfaceMethodName == DefaultFactoryMethodName) proxy.findMethod(AlternativeFactoryMethodName).head else proxy.findMethod(DefaultFactoryMethodName).head } val newInvokestatic = INVOKESTATIC( proxy.thisType, isInterface = false, // the created proxy class is always a concrete class factoryMethod.name, factoryMethod.descriptor ) if (logLambdaExpressionsRewrites) { info("rewriting invokedynamic", s"Java: $invokedynamic => $newInvokestatic") } instructions(pc) = newInvokestatic // since invokestatic is two bytes shorter than invokedynamic, we need to fill // the two-byte gap following the invokestatic with NOPs instructions(pc + 3) = NOP instructions(pc + 4) = NOP val reason = Some((updatedClassFile, instructions, pc, invokedynamic, newInvokestatic)) storeProxy(updatedClassFile, proxy, reason) } /** * Extract the parameters of `altMetafactory` calls. * * CallSite altMetafactory(MethodHandles.Lookup caller, * String invokedName, * MethodType invokedType, * Object... args) * * Object... args evaluates to the following argument list: * int flags, * int markerInterfaceCount, // IF flags has MARKERS set * Class... markerInterfaces, // IF flags has MARKERS set * int bridgeCount, // IF flags has BRIDGES set * MethodType... bridges // IF flags has BRIDGES set * * flags is a bitwise OR of the desired flags FLAG_MARKERS, FLAG_BRIDGES and FLAG_SERIALIZABLE. * * @see https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html#altMetafactory-java.lang.invoke.MethodHandles.Lookup-java.lang.String-java.lang.invoke.MethodType-java.lang.Object...- * * @param altMetafactoryArgs `Object... args` of altMetafactory parameters * @return A tuple containing an IndexSeq of markerInterfaces, bridges and a boolean indicating * if the class must be serializable. */ def extractAltMetafactoryArguments( altMetafactoryArgs: Seq[BootstrapArgument] ): (IndexedSeq[ReferenceType], IndexedSeq[MethodDescriptor], Boolean) = { var markerInterfaces = IndexedSeq.empty[ReferenceType] var bridges = IndexedSeq.empty[MethodDescriptor] var isSerializable = false if (altMetafactoryArgs.isEmpty) { return (markerInterfaces, bridges, isSerializable); } var argCount = 0 val ConstantInteger(flags) = altMetafactoryArgs(argCount) argCount += 1 // Extract the marker interfaces. They are the first in the argument list if the flag // FLAG_MARKERS is present. if ((flags & LambdaMetafactory.FLAG_MARKERS) > 0) { val ConstantInteger(markerCount) = altMetafactoryArgs(argCount) argCount += 1 markerInterfaces = altMetafactoryArgs.iterator .slice(argCount, argCount + markerCount) .map { case ConstantClass(value) => value } .toIndexedSeq argCount += markerCount } // bridge methods come afterwards if FLAG_BRIDGES is set. if ((flags & LambdaMetafactory.FLAG_BRIDGES) > 0) { val ConstantInteger(bridgesCount) = altMetafactoryArgs(argCount) argCount += 1 bridges = altMetafactoryArgs.iterator .slice(argCount, argCount + bridgesCount) .map { case md: MethodDescriptor => md } .toIndexedSeq argCount += bridgesCount } // Check if the FLAG_SERIALIZABLE is set. isSerializable = (flags & LambdaMetafactory.FLAG_SERIALIZABLE) > 0 (markerInterfaces, bridges, isSerializable) } def storeProxy( classFile: ClassFile, proxy: ClassFile, reason: Option[AnyRef] ): ClassFile = { classFile.synthesizedClassFiles match { case Some(scf @ SynthesizedClassFiles(cfs)) => val newScf = new SynthesizedClassFiles((proxy, reason) :: cfs) val newAttributes = classFile.attributes.filter(_ ne scf) :+ newScf classFile._UNSAFE_replaceAttributes(newAttributes) case None => val attributes = classFile.attributes val newAttributes = attributes :+ new SynthesizedClassFiles(List((proxy, reason))) classFile._UNSAFE_replaceAttributes(newAttributes) } } } object InvokedynamicRewriting { final val DefaultDeserializeLambdaStaticMethodName = "$deserializeLambda" final val LambdaNameRegEx = "[^.;\\[]*:[0-9]+\\$Lambda$" final val TargetMethodNameRegEx = "\\$[A-Za-z_]+\\$[^.;\\[/<>]*:[0-9]+$" final val InvokedynamicKeyPrefix = { ClassFileReaderConfiguration.ConfigKeyPrefix+"Invokedynamic." } final val InvokedynamicRewritingConfigKey = { InvokedynamicKeyPrefix+"rewrite" } final val LambdaExpressionsLogRewritingsConfigKey = { InvokedynamicKeyPrefix+"logLambdaRewrites" } final val StringConcatLogRewritingsConfigKey = { InvokedynamicKeyPrefix+"logStringConcatRewrites" } final val ObjectMethodsLogRewritingsConfigKey = { InvokedynamicKeyPrefix+"logObjectMethodsRewrites" } final val InvokedynamicLogUnknownInvokeDynamicsConfigKey = { InvokedynamicKeyPrefix+"logUnknownInvokeDynamics" } def isJava8LikeLambdaExpression(invokedynamic: INVOKEDYNAMIC): Boolean = { import ObjectType.LambdaMetafactory invokedynamic.bootstrapMethod.handle match { case InvokeStaticMethodHandle(LambdaMetafactory, false, name, descriptor) => if (name == "metafactory") { descriptor == LambdaMetafactoryDescriptor } else { name == "altMetafactory" && descriptor == LambdaAltMetafactoryDescriptor } case _ => false } } def isScalaLambdaDeserializeExpression(invokedynamic: INVOKEDYNAMIC): Boolean = { import MethodDescriptor.ScalaLambdaDeserializeDescriptor import ObjectType.ScalaLambdaDeserialize invokedynamic.bootstrapMethod.handle match { case InvokeStaticMethodHandle( ScalaLambdaDeserialize, false, "bootstrap", ScalaLambdaDeserializeDescriptor ) => true case _ => false } } def isScalaSymbolExpression(invokedynamic: INVOKEDYNAMIC): Boolean = { import MethodDescriptor.ScalaSymbolLiteralDescriptor import ObjectType.ScalaSymbolLiteral invokedynamic.bootstrapMethod.handle match { case InvokeStaticMethodHandle( ScalaSymbolLiteral, false, "bootstrap", ScalaSymbolLiteralDescriptor ) => true case _ => false } } def isScalaStructuralCallSite( invokedynamic: INVOKEDYNAMIC, instructions: Array[Instruction], pc: Int ): Boolean = { import MethodDescriptor.ScalaStructuralCallSiteDescriptor import ObjectType.ScalaStructuralCallSite invokedynamic.bootstrapMethod.handle match { case InvokeStaticMethodHandle( ScalaStructuralCallSite, false, "bootstrap", ScalaStructuralCallSiteDescriptor ) => pc == 0 && instructions.length == 44 && instructions(22).isInstanceOf[LoadString] && instructions(28) == INVOKEVIRTUAL( ObjectType.Class, "getMethod", MethodDescriptor( ArraySeq(ObjectType.String, ArrayType(ObjectType.Class)), ObjectType.Method ) ) && instructions(31) == INVOKEVIRTUAL( ObjectType("scala/runtime/ScalaRunTime$"), "ensureAccessible", MethodDescriptor(ObjectType.Method, ObjectType.Method) ) case _ => false } } def isGroovyInvokedynamic(invokedynamic: INVOKEDYNAMIC): Boolean = { invokedynamic.bootstrapMethod.handle match { case ismh: InvokeStaticMethodHandle if ismh.receiverType.isObjectType => ismh.receiverType.asObjectType.packageName.startsWith("org/codehaus/groovy") case _ => false } } def isDynamoInvokedynamic(invokedynamic: INVOKEDYNAMIC): Boolean = { invokedynamic.bootstrapMethod.handle match { case ismh: InvokeStaticMethodHandle if ismh.receiverType.isObjectType => ismh.receiverType.asObjectType.fqn == "org/dynamo/rt/DynamoBootstrap" case _ => false } } def isJava10StringConcatInvokedynamic(invokedynamic: INVOKEDYNAMIC): Boolean = { invokedynamic.bootstrapMethod.handle match { case InvokeStaticMethodHandle(ObjectType.StringConcatFactory, _, _, _) => true case _ => false } } def isObjectMethodsInvokedynamic(invokedynamic: INVOKEDYNAMIC): Boolean = { invokedynamic.bootstrapMethod.handle match { case InvokeStaticMethodHandle(ObjectType.ObjectMethods, _, _, _) => true case _ => false } } /** * Returns the default config where the settings for rewriting and logging rewrites are * set to the specified values. */ def defaultConfig(rewrite: Boolean, logRewrites: Boolean): Config = { val rewritingConfigKey = InvokedynamicRewritingConfigKey val logLambdaConfigKey = LambdaExpressionsLogRewritingsConfigKey val logConcatConfigKey = StringConcatLogRewritingsConfigKey val logObjectMethodsConfigKey = ObjectMethodsLogRewritingsConfigKey BaseConfig. withValue(rewritingConfigKey, ConfigValueFactory.fromAnyRef(rewrite)). withValue(logLambdaConfigKey, ConfigValueFactory.fromAnyRef(logRewrites)). withValue(logConcatConfigKey, ConfigValueFactory.fromAnyRef(logRewrites)). withValue(logObjectMethodsConfigKey, ConfigValueFactory.fromAnyRef(logRewrites)) } }