/*
 * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */


package org.graalvm.compiler.replacements.test.classfile;

import static org.graalvm.compiler.bytecode.Bytecodes.ALOAD;
import static org.graalvm.compiler.bytecode.Bytecodes.ANEWARRAY;
import static org.graalvm.compiler.bytecode.Bytecodes.ASTORE;
import static org.graalvm.compiler.bytecode.Bytecodes.BIPUSH;
import static org.graalvm.compiler.bytecode.Bytecodes.CHECKCAST;
import static org.graalvm.compiler.bytecode.Bytecodes.DLOAD;
import static org.graalvm.compiler.bytecode.Bytecodes.DSTORE;
import static org.graalvm.compiler.bytecode.Bytecodes.FLOAD;
import static org.graalvm.compiler.bytecode.Bytecodes.FSTORE;
import static org.graalvm.compiler.bytecode.Bytecodes.GETFIELD;
import static org.graalvm.compiler.bytecode.Bytecodes.GETSTATIC;
import static org.graalvm.compiler.bytecode.Bytecodes.GOTO;
import static org.graalvm.compiler.bytecode.Bytecodes.GOTO_W;
import static org.graalvm.compiler.bytecode.Bytecodes.IFEQ;
import static org.graalvm.compiler.bytecode.Bytecodes.IFGE;
import static org.graalvm.compiler.bytecode.Bytecodes.IFGT;
import static org.graalvm.compiler.bytecode.Bytecodes.IFLE;
import static org.graalvm.compiler.bytecode.Bytecodes.IFLT;
import static org.graalvm.compiler.bytecode.Bytecodes.IFNE;
import static org.graalvm.compiler.bytecode.Bytecodes.IFNONNULL;
import static org.graalvm.compiler.bytecode.Bytecodes.IFNULL;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ACMPEQ;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ACMPNE;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPEQ;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPGE;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPGT;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPLE;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPLT;
import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPNE;
import static org.graalvm.compiler.bytecode.Bytecodes.ILOAD;
import static org.graalvm.compiler.bytecode.Bytecodes.INSTANCEOF;
import static org.graalvm.compiler.bytecode.Bytecodes.INVOKEDYNAMIC;
import static org.graalvm.compiler.bytecode.Bytecodes.INVOKEINTERFACE;
import static org.graalvm.compiler.bytecode.Bytecodes.INVOKESPECIAL;
import static org.graalvm.compiler.bytecode.Bytecodes.INVOKESTATIC;
import static org.graalvm.compiler.bytecode.Bytecodes.INVOKEVIRTUAL;
import static org.graalvm.compiler.bytecode.Bytecodes.ISTORE;
import static org.graalvm.compiler.bytecode.Bytecodes.JSR;
import static org.graalvm.compiler.bytecode.Bytecodes.JSR_W;
import static org.graalvm.compiler.bytecode.Bytecodes.LDC;
import static org.graalvm.compiler.bytecode.Bytecodes.LDC2_W;
import static org.graalvm.compiler.bytecode.Bytecodes.LDC_W;
import static org.graalvm.compiler.bytecode.Bytecodes.LLOAD;
import static org.graalvm.compiler.bytecode.Bytecodes.LOOKUPSWITCH;
import static org.graalvm.compiler.bytecode.Bytecodes.LSTORE;
import static org.graalvm.compiler.bytecode.Bytecodes.MULTIANEWARRAY;
import static org.graalvm.compiler.bytecode.Bytecodes.NEW;
import static org.graalvm.compiler.bytecode.Bytecodes.NEWARRAY;
import static org.graalvm.compiler.bytecode.Bytecodes.PUTFIELD;
import static org.graalvm.compiler.bytecode.Bytecodes.PUTSTATIC;
import static org.graalvm.compiler.bytecode.Bytecodes.RET;
import static org.graalvm.compiler.bytecode.Bytecodes.SIPUSH;
import static org.graalvm.compiler.bytecode.Bytecodes.TABLESWITCH;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Formatter;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.graalvm.compiler.api.replacements.SnippetReflectionProvider;
import org.graalvm.compiler.api.test.Graal;
import org.graalvm.compiler.bytecode.Bytecode;
import org.graalvm.compiler.bytecode.BytecodeDisassembler;
import org.graalvm.compiler.bytecode.BytecodeLookupSwitch;
import org.graalvm.compiler.bytecode.BytecodeStream;
import org.graalvm.compiler.bytecode.BytecodeSwitch;
import org.graalvm.compiler.bytecode.BytecodeTableSwitch;
import org.graalvm.compiler.bytecode.Bytecodes;
import org.graalvm.compiler.bytecode.ResolvedJavaMethodBytecode;
import org.graalvm.compiler.core.test.GraalCompilerTest;
import org.graalvm.compiler.phases.VerifyPhase;
import org.graalvm.compiler.phases.util.Providers;
import org.graalvm.compiler.replacements.classfile.ClassfileBytecode;
import org.graalvm.compiler.replacements.classfile.ClassfileBytecodeProvider;
import org.graalvm.compiler.runtime.RuntimeProvider;
import org.graalvm.compiler.serviceprovider.JavaVersionUtil;
import org.graalvm.compiler.api.test.ModuleSupport;
import org.graalvm.compiler.test.SubprocessUtil;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;

import jdk.vm.ci.meta.ConstantPool;
import jdk.vm.ci.meta.JavaField;
import jdk.vm.ci.meta.JavaMethodProfile.ProfiledMethod;
import jdk.vm.ci.meta.JavaType;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaMethod;
import jdk.vm.ci.meta.ResolvedJavaType;

Tests that bytecode exposed via ClassfileBytecode objects is the same as the bytecode (modulo minor differences in constant pool resolution) obtained directly from ResolvedJavaMethod objects.
/** * Tests that bytecode exposed via {@link ClassfileBytecode} objects is the same as the bytecode * (modulo minor differences in constant pool resolution) obtained directly from * {@link ResolvedJavaMethod} objects. */
public class ClassfileBytecodeProviderTest extends GraalCompilerTest { @Before public void checkJavaAgent() { assumeManagementLibraryIsLoadable(); Assume.assumeFalse("Java Agent found -> skipping", SubprocessUtil.isJavaAgentAttached()); } private static boolean shouldProcess(String classpathEntry) { if (classpathEntry.endsWith(".jar")) { String name = new File(classpathEntry).getName(); return name.contains("jvmci") || name.contains("graal"); } return false; }
Keep test time down by only sampling a limited number of class files per jar.
/** * Keep test time down by only sampling a limited number of class files per jar. */
private static final int CLASSES_PER_JAR = 250;
Magic token to denote the classes in the Java runtime image (i.e. in the jrt:/ file system).
/** * Magic token to denote the classes in the Java runtime image (i.e. in the {@code jrt:/} file * system). */
public static final String JRT_CLASS_PATH_ENTRY = "<jrt>"; @Test public void test() { RuntimeProvider rt = Graal.getRequiredCapability(RuntimeProvider.class); Providers providers = rt.getHostBackend().getProviders(); MetaAccessProvider metaAccess = providers.getMetaAccess(); Assume.assumeTrue(VerifyPhase.class.desiredAssertionStatus()); String bootclasspath; if (JavaVersionUtil.JAVA_SPEC <= 8) { String propertyName = "sun.boot.class.path"; bootclasspath = System.getProperty(propertyName); Assert.assertNotNull("Cannot find value of " + propertyName, bootclasspath); } else { bootclasspath = JRT_CLASS_PATH_ENTRY; } for (String path : bootclasspath.split(File.pathSeparator)) { if (shouldProcess(path)) { try { if (path.equals(JRT_CLASS_PATH_ENTRY)) { for (String className : ModuleSupport.getJRTGraalClassNames()) { if (isGSON(className)) { /* * GSON classes are compiled with old JDK */ continue; } try { checkClass(metaAccess, getSnippetReflection(), className); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } } else { final ZipFile zipFile = new ZipFile(new File(path)); int index = 0; int step = zipFile.size() > CLASSES_PER_JAR ? zipFile.size() / CLASSES_PER_JAR : 1; for (final Enumeration<? extends ZipEntry> entry = zipFile.entries(); entry.hasMoreElements();) { final ZipEntry zipEntry = entry.nextElement(); if ((index % step) == 0) { String name = zipEntry.getName(); if (name.endsWith(".class") && !name.equals("module-info.class") && !name.startsWith("META-INF/versions/")) { String className = name.substring(0, name.length() - ".class".length()).replace('/', '.'); if (isInNativeImage(className)) { /* * Native image requires non-graalsdk classes to be present * in the classpath. */ continue; } if (isGSON(className)) { /* uses old class format */ continue; } try { checkClass(metaAccess, getSnippetReflection(), className); } catch (UnsupportedClassVersionError e) { // graal-test.jar can contain classes compiled for different // Java versions } catch (NoClassDefFoundError e) { if (!e.getMessage().contains("Could not initialize class")) { throw e; } else { // A second or later attempt to initialize a class // results in this confusing error where the // original cause of initialization failure is lost } } catch (ClassNotFoundException e) { throw new AssertionError(e); } } } index++; } } } catch (IOException ex) { Assert.fail(ex.toString()); } } } } private static boolean isInNativeImage(String className) { return className.startsWith("jdk.internal.vm.compiler.nativeimage"); } private static boolean isGSON(String className) { return className.contains("com.google.gson"); } protected void checkClass(MetaAccessProvider metaAccess, SnippetReflectionProvider snippetReflection, String className) throws ClassNotFoundException { if (className.equals("jdk.vm.ci.services.JVMCIClassLoaderFactory")) { // JVMCIClassLoaderFactory must only be initialized by the VM return; } Class<?> c = Class.forName(className, true, getClass().getClassLoader()); ClassfileBytecodeProvider cbp = new ClassfileBytecodeProvider(metaAccess, snippetReflection); for (Method method : c.getDeclaredMethods()) { checkMethod(cbp, metaAccess, method); } } private static void checkMethod(ClassfileBytecodeProvider cbp, MetaAccessProvider metaAccess, Executable executable) { ResolvedJavaMethod method = metaAccess.lookupJavaMethod(executable); if (method.hasBytecodes()) { Bytecode actual = getBytecode(cbp, method); if (actual != null) { ResolvedJavaMethodBytecode expected = new ResolvedJavaMethodBytecode(method); new BytecodeComparer(expected, actual).compare(); } } } protected static Bytecode getBytecode(ClassfileBytecodeProvider cbp, ResolvedJavaMethod method) { try { return cbp.getBytecode(method); } catch (UnsupportedClassVersionError e) { // This can happen when a library containing old class files // is bundled into a Graal jar (GR-12672). return null; } catch (Throwable e) { throw new AssertionError(String.format("Error getting bytecode for %s", method.format("%H.%n(%p)")), e); } } static class BytecodeComparer { private Bytecode expected; private Bytecode actual; private ConstantPool eCp; private ConstantPool aCp; BytecodeStream eStream; BytecodeStream aStream; int bci = -1; BytecodeComparer(Bytecode expected, Bytecode actual) { this.expected = expected; this.actual = actual; this.eCp = expected.getConstantPool(); this.aCp = actual.getConstantPool(); Assert.assertEquals(expected.getMethod().toString(), expected.getCodeSize(), actual.getCodeSize()); this.eStream = new BytecodeStream(expected.getCode()); this.aStream = new BytecodeStream(actual.getCode()); } public void compare() { try { compare0(); } catch (Throwable e) { BytecodeDisassembler dis = new BytecodeDisassembler(true, false); Formatter msg = new Formatter(); msg.format("Error comparing bytecode for %s", expected.getMethod().format("%H.%n(%p)")); if (bci >= 0) { msg.format("%nexpected: %s", dis.disassemble(expected, bci, eStream.nextBCI() - 1)); msg.format("%nactual: %s", dis.disassemble(actual, bci, aStream.nextBCI() - 1)); } throw new AssertionError(msg.toString(), e); } } public void compare0() { int opcode = eStream.currentBC(); ResolvedJavaMethod method = expected.getMethod(); while (opcode != Bytecodes.END) { bci = eStream.currentBCI(); int actualOpcode = aStream.currentBC(); if (opcode != actualOpcode) { Assert.assertEquals(opcode, actualOpcode); } if (eStream.nextBCI() > bci + 1) { switch (opcode) { case BIPUSH: Assert.assertEquals(eStream.readByte(), aStream.readByte()); break; case SIPUSH: Assert.assertEquals(eStream.readShort(), aStream.readShort()); break; case NEW: case CHECKCAST: case INSTANCEOF: case ANEWARRAY: { ResolvedJavaType e = lookupType(eCp, eStream.readCPI(), opcode); ResolvedJavaType a = lookupType(aCp, aStream.readCPI(), opcode); assertEqualTypes(e, a); break; } case GETSTATIC: case PUTSTATIC: case GETFIELD: case PUTFIELD: { ResolvedJavaField e = lookupField(eCp, eStream.readCPI(), method, opcode); ResolvedJavaField a = lookupField(aCp, aStream.readCPI(), method, opcode); assertEqualFields(e, a); break; } case INVOKEVIRTUAL: case INVOKESPECIAL: case INVOKESTATIC: { ResolvedJavaMethod e = lookupMethod(eCp, eStream.readCPI(), opcode); ResolvedJavaMethod a = lookupMethodOrNull(aCp, aStream.readCPI(), opcode); assertEqualMethods(e, a); break; } case INVOKEINTERFACE: { ResolvedJavaMethod e = lookupMethod(eCp, eStream.readCPI(), opcode); ResolvedJavaMethod a = lookupMethod(aCp, aStream.readCPI(), opcode); assertEqualMethods(e, a); break; } case INVOKEDYNAMIC: { // INVOKEDYNAMIC is not supported by ClassfileBytecodeProvider return; } case LDC: case LDC_W: case LDC2_W: { Object e = lookupConstant(eCp, eStream.readCPI(), opcode); Object a = lookupConstant(aCp, aStream.readCPI(), opcode); assertEqualsConstants(e, a); break; } case RET: case ILOAD: case LLOAD: case FLOAD: case DLOAD: case ALOAD: case ISTORE: case LSTORE: case FSTORE: case DSTORE: case ASTORE: { Assert.assertEquals(eStream.readLocalIndex(), aStream.readLocalIndex()); break; } case IFEQ: case IFNE: case IFLT: case IFGE: case IFGT: case IFLE: case IF_ICMPEQ: case IF_ICMPNE: case IF_ICMPLT: case IF_ICMPGE: case IF_ICMPGT: case IF_ICMPLE: case IF_ACMPEQ: case IF_ACMPNE: case GOTO: case JSR: case IFNULL: case IFNONNULL: case GOTO_W: case JSR_W: { Assert.assertEquals(eStream.readBranchDest(), aStream.readBranchDest()); break; } case LOOKUPSWITCH: case TABLESWITCH: { BytecodeSwitch e = opcode == LOOKUPSWITCH ? new BytecodeLookupSwitch(eStream, bci) : new BytecodeTableSwitch(eStream, bci); BytecodeSwitch a = opcode == LOOKUPSWITCH ? new BytecodeLookupSwitch(aStream, bci) : new BytecodeTableSwitch(aStream, bci); Assert.assertEquals(e.numberOfCases(), a.numberOfCases()); for (int i = 0; i < e.numberOfCases(); i++) { Assert.assertEquals(e.keyAt(i), a.keyAt(i)); Assert.assertEquals(e.targetAt(i), a.targetAt(i)); } Assert.assertEquals(e.defaultTarget(), a.defaultTarget()); Assert.assertEquals(e.defaultOffset(), a.defaultOffset()); break; } case NEWARRAY: { Assert.assertEquals(eStream.readLocalIndex(), aStream.readLocalIndex()); break; } case MULTIANEWARRAY: { ResolvedJavaType e = lookupType(eCp, eStream.readCPI(), opcode); ResolvedJavaType a = lookupType(aCp, aStream.readCPI(), opcode); Assert.assertEquals(e, a); break; } } } eStream.next(); aStream.next(); opcode = eStream.currentBC(); } } static Object lookupConstant(ConstantPool cp, int cpi, int opcode) { cp.loadReferencedType(cpi, opcode); return cp.lookupConstant(cpi); } static ResolvedJavaField lookupField(ConstantPool cp, int cpi, ResolvedJavaMethod method, int opcode) { cp.loadReferencedType(cpi, opcode); return (ResolvedJavaField) cp.lookupField(cpi, method, opcode); } static ResolvedJavaMethod lookupMethod(ConstantPool cp, int cpi, int opcode) { cp.loadReferencedType(cpi, opcode); return (ResolvedJavaMethod) cp.lookupMethod(cpi, opcode); } static ResolvedJavaMethod lookupMethodOrNull(ConstantPool cp, int cpi, int opcode) { try { return lookupMethod(cp, cpi, opcode); } catch (NoSuchMethodError e) { // A method hidden to reflection return null; } } static ResolvedJavaType lookupType(ConstantPool cp, int cpi, int opcode) { cp.loadReferencedType(cpi, opcode); return (ResolvedJavaType) cp.lookupType(cpi, opcode); } static void assertEqualsConstants(Object e, Object a) { if (!e.equals(a)) { Assert.assertEquals(String.valueOf(e), String.valueOf(a)); } } static void assertEqualFields(JavaField e, JavaField a) { if (!e.equals(a)) { Assert.assertEquals(e.format("%H.%n %T"), a.format("%H.%n %T")); } } static void assertEqualTypes(JavaType e, JavaType a) { if (!e.equals(a)) { Assert.assertEquals(e.toJavaName(), a.toJavaName()); } } static void assertEqualMethods(ResolvedJavaMethod e, ResolvedJavaMethod a) { if (a != null) { if (!e.equals(a)) { if (!e.equals(a)) { if (!e.getDeclaringClass().equals(a.getDeclaringClass())) { if (!typesAreRelated(e, a)) { throw new AssertionError(String.format("%s and %s are unrelated", a.getDeclaringClass().toJavaName(), e.getDeclaringClass().toJavaName())); } } Assert.assertEquals(e.getName(), a.getName()); Assert.assertEquals(e.getSignature(), a.getSignature()); } else { Assert.assertEquals(e, a); } } } }
The VM can resolve references to methods not available via reflection. For example, the javap output for ProfiledMethod.toString() includes:
    16: invokeinterface #40, 1 // InterfaceMethod jdk/vm/ci/meta/ResolvedJavaMethod.getName:()Ljava/lang/String;
When resolving via HotSpotConstantPool, we get:
    16: invokeinterface#4, 1   // jdk.vm.ci.meta.ResolvedJavaMethod.getName:()java.lang.String
However resolving via ClassfileConstantPool, we get:
    16: invokeinterface#40, 1  // jdk.vm.ci.meta.JavaMethod.getName:()java.lang.String
since the latter relies on ResolvedJavaType.getDeclaredMethods() which only returns methods originating from class files. We accept such differences for the purpose of this test if the declaring class of two otherwise similar methods are related (i.e. one is a subclass of the other).
/** * The VM can resolve references to methods not available via reflection. For example, the * javap output for {@link ProfiledMethod#toString()} includes: * * <pre> * 16: invokeinterface #40, 1 // InterfaceMethod jdk/vm/ci/meta/ResolvedJavaMethod.getName:()Ljava/lang/String; * </pre> * * When resolving via {@code HotSpotConstantPool}, we get: * * <pre> * 16: invokeinterface#4, 1 // jdk.vm.ci.meta.ResolvedJavaMethod.getName:()java.lang.String * </pre> * * However resolving via {@code ClassfileConstantPool}, we get: * * <pre> * 16: invokeinterface#40, 1 // jdk.vm.ci.meta.JavaMethod.getName:()java.lang.String * </pre> * * since the latter relies on {@link ResolvedJavaType#getDeclaredMethods()} which only * returns methods originating from class files. * * We accept such differences for the purpose of this test if the declaring class of two * otherwise similar methods are related (i.e. one is a subclass of the other). */
protected static boolean typesAreRelated(ResolvedJavaMethod e, ResolvedJavaMethod a) { return a.getDeclaringClass().isAssignableFrom(e.getDeclaringClass()) || e.getDeclaringClass().isAssignableFrom(a.getDeclaringClass()); } } }