Skip to content

Commit

Permalink
Read version 51 (JDK 7) class files.
Browse files Browse the repository at this point in the history
This commit makes the ClassFileReader/ICodeReader parse class files
from JDK 7 (class file version 51). It does that by skipping over
the method handle related entries in the constant pool and by doing
some dummy processing on invoke dynamic instructions. The inliner
is updated to not try to inline a method with an invoke dynamic
instruction. A place holder INVOKE_DYNAMIC instruction is added to ICode
but it is designed to create an error if there's ever any attempt to
analyze it. Because the inliner is the only phase that ever tries
to analyze ICode instructions not generated from Scala source and
because Scala source will never emit an INVOKE_DYNAMIC, the place
holder INVOKE_DYNAMIC should never cause any errors.

A test is included that generates a class file with INVOKE_DYNAMIC
and then compiles Scala code that depends on it.
  • Loading branch information
JamesIry committed Mar 14, 2013
1 parent b7b4f87 commit e78896f
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 3 deletions.
4 changes: 4 additions & 0 deletions bincompat-backward.whitelist.conf
Expand Up @@ -158,6 +158,10 @@ filter {
{
matchName="scala.reflect.internal.Names#NameOps.name"
problemName=MissingFieldProblem
},
{
matchName="scala.reflect.internal.ClassfileConstants.xxxunusedxxxx"
problemName=MissingMethodProblem
}
]
}
16 changes: 16 additions & 0 deletions bincompat-forward.whitelist.conf
Expand Up @@ -354,6 +354,22 @@ filter {
{
matchName="scala.reflect.internal.StdNames#TermNames.SelectFromTypeTree"
problemName=MissingMethodProblem
},
{
matchName="scala.reflect.internal.ClassfileConstants.CONSTANT_INVOKEDYNAMIC"
problemName=MissingMethodProblem
},
{
matchName="scala.reflect.internal.ClassfileConstants.invokedynamic"
problemName=MissingMethodProblem
},
{
matchName="scala.reflect.internal.ClassfileConstants.CONSTANT_METHODHANDLE"
problemName=MissingMethodProblem
},
{
matchName="scala.reflect.internal.ClassfileConstants.CONSTANT_METHODTYPE"
problemName=MissingMethodProblem
}
]
}
1 change: 1 addition & 0 deletions src/compiler/scala/tools/nsc/backend/icode/Members.scala
Expand Up @@ -171,6 +171,7 @@ trait Members {
var returnType: TypeKind = _
var recursive: Boolean = false
var bytecodeHasEHs = false // set by ICodeReader only, used by Inliner to prevent inlining (SI-6188)
var bytecodeHasInvokeDynamic = false // set by ICodeReader only, used by Inliner to prevent inlining until we have proper invoke dynamic support

/** local variables and method parameters */
var locals: List[Local] = Nil
Expand Down
19 changes: 19 additions & 0 deletions src/compiler/scala/tools/nsc/backend/icode/Opcodes.scala
Expand Up @@ -409,6 +409,25 @@ trait Opcodes { self: ICodes =>

override def category = mthdsCat
}

/**
* A place holder entry that allows us to parse class files with invoke dynamic
* instructions. Because the compiler doesn't yet really understand the
* behavior of invokeDynamic, this op acts as a poison pill. Any attempt to analyze
* this instruction will cause a failure. The only optimization that
* should ever look at non-Scala generated icode is the inliner, and it
* has been modified to not examine any method with invokeDynamic
* instructions. So if this poison pill ever causes problems then
* there's been a serious misunderstanding
*/
// TODO do the real thing
case class INVOKE_DYNAMIC(poolEntry: Char) extends Instruction {
private def error = sys.error("INVOKE_DYNAMIC is not fully implemented and should not be analyzed")
override def consumed = error
override def produced = error
override def producedTypes = error
override def category = error
}

case class BOX(boxType: TypeKind) extends Instruction {
assert(boxType.isValueType && (boxType ne UNIT)) // documentation
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scala/tools/nsc/backend/opt/Inliners.scala
Expand Up @@ -961,6 +961,7 @@ abstract class Inliners extends SubComponent {
if(isInlineForbidden) { rs ::= "is annotated @noinline" }
if(inc.isSynchronized) { rs ::= "is synchronized method" }
if(inc.m.bytecodeHasEHs) { rs ::= "bytecode contains exception handlers / finally clause" } // SI-6188
if(inc.m.bytecodeHasInvokeDynamic) { rs ::= "bytecode contains invoke dynamic" }
if(rs.isEmpty) null else rs.mkString("", ", and ", "")
}

Expand Down
Expand Up @@ -137,10 +137,13 @@ abstract class ClassfileParser {
(in.nextByte.toInt: @switch) match {
case CONSTANT_UTF8 | CONSTANT_UNICODE =>
in.skip(in.nextChar)
case CONSTANT_CLASS | CONSTANT_STRING =>
case CONSTANT_CLASS | CONSTANT_STRING | CONSTANT_METHODTYPE=>
in.skip(2)
case CONSTANT_METHODHANDLE =>
in.skip(3)
case CONSTANT_FIELDREF | CONSTANT_METHODREF | CONSTANT_INTFMETHODREF
| CONSTANT_NAMEANDTYPE | CONSTANT_INTEGER | CONSTANT_FLOAT =>
| CONSTANT_NAMEANDTYPE | CONSTANT_INTEGER | CONSTANT_FLOAT
| CONSTANT_INVOKEDYNAMIC =>
in.skip(4)
case CONSTANT_LONG | CONSTANT_DOUBLE =>
in.skip(8)
Expand Down
Expand Up @@ -506,6 +506,12 @@ abstract class ICodeReader extends ClassfileParser {
code.emit(UNBOX(toTypeKind(m.info.resultType)))
else
code.emit(CALL_METHOD(m, Static(false)))
case JVM.invokedynamic =>
// TODO, this is just a place holder. A real implementation must parse the class constant entry
containsInvokeDynamic = true
val poolEntry = in.nextChar
in.skip(2)
code.emit(INVOKE_DYNAMIC(poolEntry))

case JVM.new_ =>
code.emit(NEW(REFERENCE(pool.getClassSymbol(in.nextChar))))
Expand Down Expand Up @@ -644,6 +650,7 @@ abstract class ICodeReader extends ClassfileParser {
var containsDUPX = false
var containsNEW = false
var containsEHs = false
var containsInvokeDynamic = false

def emit(i: Instruction) {
instrs += ((pc, i))
Expand All @@ -662,6 +669,7 @@ abstract class ICodeReader extends ClassfileParser {
val code = new Code(method)
method.setCode(code)
method.bytecodeHasEHs = containsEHs
method.bytecodeHasInvokeDynamic = containsInvokeDynamic
var bb = code.startBlock

def makeBasicBlocks: mutable.Map[Int, BasicBlock] =
Expand Down
5 changes: 4 additions & 1 deletion src/reflect/scala/reflect/internal/ClassfileConstants.scala
Expand Up @@ -72,6 +72,9 @@ object ClassfileConstants {
final val CONSTANT_METHODREF = 10
final val CONSTANT_INTFMETHODREF = 11
final val CONSTANT_NAMEANDTYPE = 12
final val CONSTANT_METHODHANDLE = 15
final val CONSTANT_METHODTYPE = 16
final val CONSTANT_INVOKEDYNAMIC = 18

// tags describing the type of a literal in attribute values
final val BYTE_TAG = 'B'
Expand Down Expand Up @@ -306,7 +309,7 @@ object ClassfileConstants {
final val invokespecial = 0xb7
final val invokestatic = 0xb8
final val invokeinterface = 0xb9
final val xxxunusedxxxx = 0xba
final val invokedynamic = 0xba

final val new_ = 0xbb
final val newarray = 0xbc
Expand Down
126 changes: 126 additions & 0 deletions test/files/run/classfile-format-51.scala
@@ -0,0 +1,126 @@
import java.io.{File, FileOutputStream}

import scala.tools.nsc.settings.ScalaVersion
import scala.tools.partest._
import scala.tools.asm
import asm.{AnnotationVisitor, ClassWriter, FieldVisitor, Handle, MethodVisitor, Opcodes}
import Opcodes._

// This test ensures that we can read JDK 7 (classfile format 51) files, including those
// with invokeDynamic instructions and associated constant pool entries
// to do that it first uses ASM to generate a class called DynamicInvoker. Then
// it runs a normal compile on the source in the 'code' field that refers to
// DynamicInvoker. Any failure will be dumped to std out.
//
// By it's nature the test can only work on JDK 7+ because under JDK 6 some of the
// classes referred to by DynamicInvoker won't be available and DynamicInvoker won't
// verify. So the test includes a version check that short-circuites the whole test
// on JDK 6
object Test extends DirectTest {
override def extraSettings: String = "-optimise -usejavacp -d " + testOutput.path + " -cp " + testOutput.path

def generateClass() {
val invokerClassName = "DynamicInvoker"
val bootstrapMethodName = "bootstrap"
val bootStrapMethodType = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"
val targetMethodName = "target"
val targetMethodType = "()Ljava/lang/String;"

val cw = new ClassWriter(0)
cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, invokerClassName, null, "java/lang/Object", null)

val constructor = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null)
constructor.visitCode()
constructor.visitVarInsn(ALOAD, 0)
constructor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V")
constructor.visitInsn(RETURN)
constructor.visitMaxs(1, 1)
constructor.visitEnd()

val target = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, targetMethodName, targetMethodType, null, null)
target.visitCode()
target.visitLdcInsn("hello")
target.visitInsn(ARETURN)
target.visitMaxs(1, 1)
target.visitEnd()

val bootstrap = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, bootstrapMethodName, bootStrapMethodType, null, null)
bootstrap.visitCode()
// val lookup = MethodHandles.lookup();
bootstrap.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup", "()Ljava/lang/invoke/MethodHandles$Lookup;")
bootstrap.visitVarInsn(ASTORE, 3) // lookup

// val clazz = lookup.lookupClass();
bootstrap.visitVarInsn(ALOAD, 3) // lookup
bootstrap.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "lookupClass", "()Ljava/lang/Class;")
bootstrap.visitVarInsn(ASTORE, 4) // clazz

// val methodType = MethodType.fromMethodDescriptorString("()Ljava/lang/String, clazz.getClassLoader()")
bootstrap.visitLdcInsn("()Ljava/lang/String;")
bootstrap.visitVarInsn(ALOAD, 4) // CLAZZ
bootstrap.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getClassLoader", "()Ljava/lang/ClassLoader;")
bootstrap.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodType", "fromMethodDescriptorString", "(Ljava/lang/String;Ljava/lang/ClassLoader;)Ljava/lang/invoke/MethodType;")
bootstrap.visitVarInsn(ASTORE, 5) // methodType

// val methodHandle = lookup.findStatic(thisClass, "target", methodType)
bootstrap.visitVarInsn(ALOAD, 3) // lookup
bootstrap.visitVarInsn(ALOAD, 4) // clazz
bootstrap.visitLdcInsn("target")
bootstrap.visitVarInsn(ALOAD, 5) // methodType
bootstrap.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;")
bootstrap.visitVarInsn(ASTORE, 6) // methodHandle

// new ConstantCallSite(methodHandle)
bootstrap.visitTypeInsn(NEW, "java/lang/invoke/ConstantCallSite")
bootstrap.visitInsn(DUP)
bootstrap.visitVarInsn(ALOAD, 6) // methodHandle
bootstrap.visitMethodInsn(INVOKESPECIAL, "java/lang/invoke/ConstantCallSite", "<init>", "(Ljava/lang/invoke/MethodHandle;)V")
bootstrap.visitInsn(ARETURN)
bootstrap.visitMaxs(4,7)
bootstrap.visitEnd()

val test = cw.visitMethod(ACC_PUBLIC + ACC_FINAL, "test", s"()Ljava/lang/String;", null, null)
test.visitCode()
val bootstrapHandle = new Handle(H_INVOKESTATIC, invokerClassName, bootstrapMethodName, bootStrapMethodType)
test.visitInvokeDynamicInsn("invoke", targetMethodType, bootstrapHandle)
test.visitInsn(ARETURN)
test.visitMaxs(1, 1)
test.visitEnd()

cw.visitEnd()
val bytes = cw.toByteArray()

val fos = new FileOutputStream(new File(s"${testOutput.path}/$invokerClassName.class"))
try
fos write bytes
finally
fos.close()

}

def code =
"""
object Driver {
val invoker = new DynamicInvoker()
println(invoker.test())
}
"""

override def show(): Unit = {
// redirect err to out, for logging
val prevErr = System.err
System.setErr(System.out)
try {
// this test is only valid under JDK 1.7+
// cheat a little by using 'ScalaVersion' because it can parse java versions just as well
val requiredJavaVersion = ScalaVersion("1.7")
val executingJavaVersion = ScalaVersion(System.getProperty("java.specification.version"))
if (executingJavaVersion >= requiredJavaVersion) {
generateClass()
compile()
}
}
finally
System.setErr(prevErr)
}
}

0 comments on commit e78896f

Please sign in to comment.