diff --git a/.gitignore b/.gitignore index 064a6aa..20cd526 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .idea *.iml +.project +.settings +.classpath + out generated generated-* diff --git a/README.md b/README.md index 3e2f755..947055c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ So far we have "trouble" with... * [Noto Sans](noto-sans) * [weird Tycho/JDT behavior](tycho-jdt) * [XML transformations](xml-transformer) +* [Symbolic References](symbolic-references) ## Observe! diff --git a/pom.xml b/pom.xml index 307cee6..fb528fd 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ noto-sans tycho-jdt xml-transformer + symbolic-references diff --git a/symbolic-references/README.md b/symbolic-references/README.md new file mode 100644 index 0000000..6835d0f --- /dev/null +++ b/symbolic-references/README.md @@ -0,0 +1,32 @@ +# Symbolic References + +This is related to the class loading tests, and really falls into the "Should Break" category, +as previous behaviour was not strictly to-spec. + +## Stricter interpretation of Symbolic References in .class files + +Java 9 appears to be more strict in its interpretation of the term "Symbolic Reference" in +class files. The spec says: + +> For a nonarray class or an interface, the name is the fully qualified name of the class or interface. + +(This is expected in "binary format", with forward-slashes instead of periods, for example +`java/lang/String`.) + +In Java 8, you could get away with using a _Type Descriptor_ when really you should have +used a symbolic reference. A good example is CHECKCAST (which is used in this test). +Under Java 8, the instruction could have referenced a constant pool entry with either +`java/lang/String` (to spec) or `Ljava/lang/String;` (a descriptor, and not strictly +to spec) and it would work fine. + +Under Java 9, such classes will not load. You'll instead receive a `ClassFormatError`: + +``` +[ERROR] loadClassWithDescriptorCheckcast Time elapsed: 0.031 s <<< ERROR! +java.lang.RuntimeException: Unrecoverable Error + at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13) +Caused by: java.lang.reflect.InvocationTargetException + at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13) +Caused by: java.lang.ClassFormatError: Illegal class name "Ljava/lang/String;" in class file TestClassImpl + at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13) +``` diff --git a/symbolic-references/pom.xml b/symbolic-references/pom.xml new file mode 100644 index 0000000..109764e --- /dev/null +++ b/symbolic-references/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + wtf.java9 + symbolic-references + 1.0-SNAPSHOT + + + wtf.java9 + seriously-wtf + 1.0-SNAPSHOT + ../pom.xml + + + + + org.ow2.asm + asm + 6.2 + + + org.ow2.asm + asm-tree + 6.2 + + + diff --git a/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClass.java b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClass.java new file mode 100644 index 0000000..35316a3 --- /dev/null +++ b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClass.java @@ -0,0 +1,5 @@ +package wtf.java9.symbolic_references; + +public interface TestClass { + public String generatedMethod(Object o); +} diff --git a/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClassGenerator.java b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClassGenerator.java new file mode 100644 index 0000000..2414300 --- /dev/null +++ b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClassGenerator.java @@ -0,0 +1,97 @@ +package wtf.java9.symbolic_references; + +import static org.objectweb.asm.Opcodes.*; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.tree.ClassNode; + +public class TestClassGenerator { + private static final Method defineClass; + static { + try { + defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Unrecoverable Error: NoSuchMethodException 'defineClass' on ClassLoader.\n" + + "This is most likely an environment issue.", e); + } + defineClass.setAccessible(true); + } + + public Class generateClass() { + return defineClass(getClass().getClassLoader()); + + } + + byte[] generateBytecode() { + ClassNode cv = new ClassNode(); + + cv.visit(V1_8, + ACC_PUBLIC | ACC_SYNTHETIC | ACC_SUPER, + "TestClassImpl", + null, + "java/lang/Object", + new String[] { "wtf/java9/symbolic_references/TestClass" }); + + generateConstructor(cv); + generateGeneratedMethod(cv); + + cv.visitEnd(); + + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + cv.accept(writer); + return writer.toByteArray(); + } + + void generateConstructor(ClassVisitor cv) { + MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_SYNTHETIC, + "", + "()V", + "", + null); + + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(RETURN); + mv.visitEnd(); + } + + void generateGeneratedMethod(ClassVisitor cv) { + MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_SYNTHETIC, + "generatedMethod", + "(Ljava/lang/Object;)Ljava/lang/String;", + "", + null); + + mv.visitCode(); + mv.visitVarInsn(ALOAD, 1); + + // The following line initiates the failure. Java 9 is + // more strict here and requires an internal name + // rather than a descriptor. So change to "java/lang/String". + mv.visitTypeInsn(CHECKCAST, "Ljava/lang/String;"); + + mv.visitInsn(ARETURN); + mv.visitEnd(); + } + + @SuppressWarnings("unchecked") + Class defineClass(ClassLoader loader) { + byte[] code = generateBytecode(); + try { + return (Class)defineClass.invoke(loader, "TestClassImpl", code, 0, code.length); + } catch (InvocationTargetException e) { + System.err.println("InvocationTargetException: in defineClass: " + e.getMessage()); + throw new RuntimeException("Unrecoverable Error", e); + } catch (IllegalAccessException e) { + System.err.println("IllegalAccessException: in defineClass: " + e.getMessage()); + throw new RuntimeException("Unrecoverable Error", e); + } + } + +} diff --git a/symbolic-references/src/test/java/wtf/java9/symbolic_references/CheckCastTest.java b/symbolic-references/src/test/java/wtf/java9/symbolic_references/CheckCastTest.java new file mode 100644 index 0000000..cfec34a --- /dev/null +++ b/symbolic-references/src/test/java/wtf/java9/symbolic_references/CheckCastTest.java @@ -0,0 +1,19 @@ +package wtf.java9.symbolic_references; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import wtf.java9.symbolic_references.TestClass; +import wtf.java9.symbolic_references.TestClassGenerator; + +public class CheckCastTest { + + /* Works under Java < 9, fails with ClassFormatError under Java 9. */ + @Test + public void loadClassWithDescriptorCheckcast() throws Exception { + Class clz = new TestClassGenerator().generateClass(); + TestClass testClass = clz.newInstance(); + assertThat(testClass.generatedMethod("hello")).isEqualTo("hello"); + } +}