diff options
author | Ian Zerny <zerny@google.com> | 2017-10-26 09:29:25 +0200 |
---|---|---|
committer | Ian Zerny <zerny@google.com> | 2017-10-26 09:29:25 +0200 |
commit | dbb891e86e8cf34b71c7096e1710009292d8e72e (patch) | |
tree | 040d07473872dc9313bb81d76c45ce4cd767f9da | |
parent | 2e49cc0222901d4220fbd46b1785187b7e98d0e8 (diff) | |
download | r8-dbb891e86e8cf34b71c7096e1710009292d8e72e.tar.gz |
Partial identity transformation from Java bytecode to Java bytecode.
This change minimally enables writing out Java bytecode from Java bytecode
inputs for a simple hello world program. Follow-up work will extend the support
to the full program structure and producing code via our IR.
Bug: 65390962
Change-Id: I978c7400f9a21323dc0c5e95fc55272300839878
19 files changed, 316 insertions, 28 deletions
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java index 5f438492d..6c4b9bcd0 100644 --- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java +++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java @@ -81,6 +81,8 @@ abstract class BaseCompilerCommand extends BaseCommand { if (outputPath == null) { return new IgnoreContentsOutputSink(); } else { + // TODO(zerny): Calling getInternalOptions here is incorrect since any modifications by an + // options consumer will not be visible to the sink. return FileSystemOutputSink.create(outputPath, getInternalOptions()); } } diff --git a/src/main/java/com/android/tools/r8/OutputSink.java b/src/main/java/com/android/tools/r8/OutputSink.java index 8ffb792a1..a73391ae2 100644 --- a/src/main/java/com/android/tools/r8/OutputSink.java +++ b/src/main/java/com/android/tools/r8/OutputSink.java @@ -32,7 +32,8 @@ public interface OutputSink { * gives the current file count. * <p> * Files are not necessarily generated in order and files might be written concurrently. However, - * for each fileId only one file is ever written. + * for each fileId only one file is ever written. If this method is called, the other writeDexFile + * and writeClassFile methods will not be called. */ void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId) throws IOException; @@ -46,13 +47,28 @@ public interface OutputSink { * primaryClassName only one file is ever written. * <p> * This method is only invoked by D8 and only if compiling each class into its own dex file, e.g., - * for incremental compilation. If this method is called, the other writeDexFile method will - * not be called. + * for incremental compilation. If this method is called, the other writeDexFile and + * writeClassFile methods will not be called. */ void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) throws IOException; /** + * Write a Java classfile that contains the class primaryClassName and its companion classes. + * <p> + * This is equivalent to writing out the file com/foo/bar/Test.class given a primaryClassName of + * com.foo.bar.Test. + * <p> + * There is no guaranteed order and files might be written concurrently. However, for each + * primaryClassName only one file is ever written. + * <p> + * This method is only invoked by R8 and only if compiling to Java bytecode. If this method is + * called, the other writeDexFile and writeClassFile methods will not be called. + */ + void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) + throws IOException; + + /** * Provides the raw bytes that would be generated for the <code>-printusage</code> flag. * <p> * This method is only invoked by R8 and only if R8 is instructed to generate printusage diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java index 5255fc5d6..d9066bdf1 100644 --- a/src/main/java/com/android/tools/r8/R8.java +++ b/src/main/java/com/android/tools/r8/R8.java @@ -19,6 +19,7 @@ import com.android.tools.r8.graph.GraphLense; import com.android.tools.r8.ir.conversion.IRConverter; import com.android.tools.r8.ir.optimize.EnumOrdinalMapCollector; import com.android.tools.r8.ir.optimize.SwitchMapCollector; +import com.android.tools.r8.jar.CfApplicationWriter; import com.android.tools.r8.naming.Minifier; import com.android.tools.r8.naming.NamingLens; import com.android.tools.r8.naming.SourceFileRewriter; @@ -92,11 +93,14 @@ public class R8 { throws ExecutionException, DexOverflowException { try { Marker marker = getMarker(options); - new ApplicationWriter( - application, options, marker, deadCode, namingLens, proguardSeedsData) - .write(outputSink, executorService); + if (options.outputClassFiles) { + new CfApplicationWriter(application, options).write(outputSink, executorService); + } else { + new ApplicationWriter(application, options, marker, deadCode, namingLens, proguardSeedsData) + .write(outputSink, executorService); + } } catch (IOException e) { - throw new RuntimeException("Cannot write dex application", e); + throw new RuntimeException("Cannot write application", e); } } diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java index b64053ae0..ad1af107f 100644 --- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java +++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java @@ -25,6 +25,7 @@ public class DexProgramClass extends DexClass implements Supplier<DexProgramClas private final ProgramResource.Kind originKind; private DexEncodedArray staticValues = SENTINEL_NOT_YET_COMPUTED; private final Collection<DexProgramClass> synthesizedFrom; + private int classFileVersion = -1; public DexProgramClass( DexType type, @@ -296,4 +297,13 @@ public class DexProgramClass extends DexClass implements Supplier<DexProgramClas public DexProgramClass get() { return this; } + + public void setClassFileVersion(int classFileVersion) { + this.classFileVersion = classFileVersion; + } + + public int getClassFileVersion() { + assert classFileVersion != -1; + return classFileVersion; + } } diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java index 001480fcb..9742df7d3 100644 --- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java +++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java @@ -101,6 +101,7 @@ public class JarClassFileReader { private final ReparseContext context = new ReparseContext(); // DexClass data. + private int version; private DexType type; private DexAccessFlags accessFlags; private DexType superType; @@ -174,6 +175,7 @@ public class JarClassFileReader { @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.version = version; accessFlags = createAccessFlags(access); // Unset the (in dex) non-existent ACC_SUPER flag on the class. assert Constants.ACC_SYNCHRONIZED == Opcodes.ACC_SUPER; @@ -263,6 +265,9 @@ public class JarClassFileReader { if (classKind == ClassKind.PROGRAM) { context.owner = clazz.asProgramClass(); } + if (clazz.isProgramClass()) { + clazz.asProgramClass().setClassFileVersion(version); + } classConsumer.accept(clazz); } diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java index 5988ae5ce..e8420284f 100644 --- a/src/main/java/com/android/tools/r8/graph/JarCode.java +++ b/src/main/java/com/android/tools/r8/graph/JarCode.java @@ -27,6 +27,11 @@ import org.objectweb.asm.util.TraceMethodVisitor; public class JarCode extends Code { + // TODO(zerny): Write via the IR. + public void writeTo(MethodVisitor visitor) { + node.accept(visitor); + } + public static class ReparseContext { // This will hold the content of the whole class. Once all the methods of the class are swapped diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java index 063bee1dd..dda8031a6 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java @@ -581,7 +581,23 @@ public class IRConverter { } printMethod(code, "Optimized IR (SSA)"); + finalizeIR(method, code, feedback); + } + + private void finalizeIR(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) { + if (options.outputClassFiles) { + finalizeToCf(method, code, feedback); + } else { + finalizeToDex(method, code, feedback); + } + } + + private void finalizeToCf(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) { + assert method.getCode().isJarCode(); + // TODO(zerny): Actually convert IR back to Java bytecode. + } + private void finalizeToDex(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) { // Perform register allocation. RegisterAllocator registerAllocator = performRegisterAllocation(code, method); method.setCode(code, registerAllocator, options); diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java new file mode 100644 index 000000000..4662cd0a8 --- /dev/null +++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java @@ -0,0 +1,94 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +package com.android.tools.r8.jar; + +import static org.objectweb.asm.Opcodes.ACC_SUPER; + +import com.android.tools.r8.OutputSink; +import com.android.tools.r8.errors.Unimplemented; +import com.android.tools.r8.graph.DexAccessFlags; +import com.android.tools.r8.graph.DexApplication; +import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.graph.DexProgramClass; +import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.utils.InternalOptions; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import jdk.internal.org.objectweb.asm.Type; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; + +public class CfApplicationWriter { + private final DexApplication application; + private final InternalOptions options; + + public CfApplicationWriter(DexApplication application, InternalOptions options) { + this.application = application; + this.options = options; + } + + public void write(OutputSink outputSink, ExecutorService executor) throws IOException { + application.timing.begin("CfApplicationWriter.write"); + try { + writeApplication(outputSink, executor); + } finally { + application.timing.end(); + } + } + + private void writeApplication(OutputSink outputSink, ExecutorService executor) + throws IOException { + for (DexProgramClass clazz : application.classes()) { + if (clazz.getSynthesizedFrom().isEmpty()) { + writeClass(clazz, outputSink); + } else { + throw new Unimplemented("No support for synthetics in the Java bytecode backend."); + } + } + } + + private void writeClass(DexProgramClass clazz, OutputSink outputSink) throws IOException { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + writer.visitSource(clazz.sourceFile.toString(), null); + int version = clazz.getClassFileVersion(); + int access = classAndInterfaceAccessFlags(clazz.accessFlags); + String desc = clazz.type.toDescriptorString(); + String name = internalName(clazz.type); + String signature = null; // TODO(zerny): Support generic signatures. + String superName = + clazz.type == options.itemFactory.objectType ? null : internalName(clazz.superType); + String[] interfaces = new String[clazz.interfaces.values.length]; + for (int i = 0; i < clazz.interfaces.values.length; i++) { + interfaces[i] = internalName(clazz.interfaces.values[i]); + } + writer.visit(version, access, name, signature, superName, interfaces); + // TODO(zerny): Methods and fields. + for (DexEncodedMethod method : clazz.directMethods()) { + writeMethod(method, writer); + } + outputSink.writeClassFile(writer.toByteArray(), Collections.singleton(desc), desc); + } + + private void writeMethod(DexEncodedMethod method, ClassWriter writer) { + int access = method.accessFlags.get(); + String name = method.method.name.toString(); + String desc = method.descriptor(); + String signature = null; // TODO(zerny): Support generic signatures. + String[] exceptions = null; + MethodVisitor visitor = writer.visitMethod(access, name, desc, signature, exceptions); + method.getCode().asJarCode().writeTo(visitor); + } + + private static int classAndInterfaceAccessFlags(DexAccessFlags accessFlags) { + // TODO(zerny): Refactor access flags to account for the union of both DEX and Java flags. + int access = accessFlags.get(); + access |= ACC_SUPER; + return access; + } + + private static String internalName(DexType type) { + return Type.getType(type.toDescriptorString()).getInternalName(); + } +} diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java index b082d9068..24f5c08b2 100644 --- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java +++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java @@ -600,6 +600,10 @@ public class AndroidApp { return addProgramResources(Kind.CLASS, Resource.fromBytes(origin, data)); } + public Builder addClassProgramData(Origin origin, byte[] data, Set<String> classDescriptors) { + return addProgramResources(Kind.CLASS, Resource.fromBytes(origin, data, classDescriptors)); + } + /** * Set dead-code data. */ diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java b/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java index 914428404..a4f223663 100644 --- a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java +++ b/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java @@ -4,7 +4,10 @@ package com.android.tools.r8.utils; import com.android.tools.r8.OutputSink; +import com.android.tools.r8.Resource.Origin; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.TreeMap; @@ -13,6 +16,7 @@ public class AndroidAppOutputSink extends ForwardingOutputSink { private final AndroidApp.Builder builder = AndroidApp.builder(); private final TreeMap<String, DescriptorsWithContents> dexFilesWithPrimary = new TreeMap<>(); private final TreeMap<Integer, DescriptorsWithContents> dexFilesWithId = new TreeMap<>(); + private final List<DescriptorsWithContents> classFiles = new ArrayList<>(); private boolean closed = false; public AndroidAppOutputSink(OutputSink forwardTo) { @@ -26,6 +30,7 @@ public class AndroidAppOutputSink extends ForwardingOutputSink { @Override public synchronized void writeDexFile(byte[] contents, Set<String> classDescriptors, int fileId) throws IOException { + assert dexFilesWithPrimary.isEmpty() && classFiles.isEmpty(); // Sort the files by id so that their order is deterministic. Some tests depend on this. dexFilesWithId.put(fileId, new DescriptorsWithContents(classDescriptors, contents)); super.writeDexFile(contents, classDescriptors, fileId); @@ -35,6 +40,7 @@ public class AndroidAppOutputSink extends ForwardingOutputSink { public synchronized void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) throws IOException { + assert dexFilesWithId.isEmpty() && classFiles.isEmpty(); // Sort the files by their name for good measure. dexFilesWithPrimary .put(primaryClassName, new DescriptorsWithContents(classDescriptors, contents)); @@ -42,6 +48,14 @@ public class AndroidAppOutputSink extends ForwardingOutputSink { } @Override + public synchronized void writeClassFile( + byte[] contents, Set<String> classDescriptors, String primaryClassName) throws IOException { + assert dexFilesWithPrimary.isEmpty() && dexFilesWithId.isEmpty(); + classFiles.add(new DescriptorsWithContents(classDescriptors, contents)); + super.writeClassFile(contents, classDescriptors, primaryClassName); + } + + @Override public void writePrintUsedInformation(byte[] contents) throws IOException { builder.setDeadCode(contents); super.writePrintUsedInformation(contents); @@ -68,9 +82,18 @@ public class AndroidAppOutputSink extends ForwardingOutputSink { @Override public void close() throws IOException { assert !closed; - assert dexFilesWithId.isEmpty() || dexFilesWithPrimary.isEmpty(); - dexFilesWithPrimary.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v)); - dexFilesWithId.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors)); + if (!dexFilesWithPrimary.isEmpty()) { + assert dexFilesWithId.isEmpty() && classFiles.isEmpty(); + dexFilesWithPrimary.forEach( + (v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v)); + } else if (!dexFilesWithId.isEmpty()) { + assert dexFilesWithPrimary.isEmpty() && classFiles.isEmpty(); + dexFilesWithId.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors)); + } else if (!classFiles.isEmpty()) { + assert dexFilesWithPrimary.isEmpty() && dexFilesWithId.isEmpty(); + classFiles.forEach( + d -> builder.addClassProgramData(Origin.unknown(), d.contents, d.descriptors)); + } closed = true; super.close(); } diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java b/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java index 56e0ea149..6c1090a8c 100644 --- a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java +++ b/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java @@ -3,6 +3,9 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.utils; +import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION; +import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -43,7 +46,18 @@ public class DirectoryOutputSink extends FileSystemOutputSink { @Override public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) throws IOException { - Path target = outputDirectory.resolve(getOutputFileName(primaryClassName)); + writeFileFromDescriptor(contents, primaryClassName, DEX_EXTENSION); + } + + @Override + public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) + throws IOException { + writeFileFromDescriptor(contents, primaryClassName, CLASS_EXTENSION); + } + + private void writeFileFromDescriptor(byte[] contents, String descriptor, String extension) + throws IOException { + Path target = outputDirectory.resolve(getOutputFileName(descriptor, extension)); Files.createDirectories(target.getParent()); writeToFile(target, null, contents); } diff --git a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java index c0aef0a6b..1ca0cf979 100644 --- a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java +++ b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java @@ -28,13 +28,13 @@ public abstract class FileSystemOutputSink implements OutputSink { } String getOutputFileName(int index) { + assert !options.outputClassFiles; return index == 0 ? "classes.dex" : ("classes" + (index + 1) + FileUtils.DEX_EXTENSION); } - String getOutputFileName(String classDescriptor) throws IOException { + String getOutputFileName(String classDescriptor, String extension) throws IOException { assert classDescriptor != null && DescriptorUtils.isClassDescriptor(classDescriptor); - return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) - + FileUtils.DEX_EXTENSION; + return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + extension; } diff --git a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java b/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java index 3ac0e2b54..f1763291a 100644 --- a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java +++ b/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java @@ -33,6 +33,12 @@ public abstract class ForwardingOutputSink implements OutputSink { } @Override + public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) + throws IOException { + forwardTo.writeClassFile(contents, classDescriptors, primaryClassName); + } + + @Override public void writePrintUsedInformation(byte[] contents) throws IOException { forwardTo.writePrintUsedInformation(contents); } diff --git a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java b/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java index 8dbf3bcbd..384c7d5bc 100644 --- a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java +++ b/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java @@ -20,6 +20,12 @@ public class IgnoreContentsOutputSink implements OutputSink { } @Override + public void writeClassFile( + byte[] contents, Set<String> classDescriptors, String primaryClassName) { + // Intentionally left empty. + } + + @Override public void writePrintUsedInformation(byte[] contents) { // Intentionally left empty. } diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java index 75f89beac..a04e28fe5 100644 --- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java +++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java @@ -51,6 +51,8 @@ public class InternalOptions { public boolean printTimes = false; + public boolean outputClassFiles = false; + // Optimization-related flags. These should conform to -dontoptimize. public boolean skipDebugLineNumberOpt = false; public boolean skipClassMerging = true; diff --git a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java b/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java index d290b564f..c1ad6c933 100644 --- a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java +++ b/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java @@ -3,6 +3,9 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.utils; +import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION; +import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -31,7 +34,13 @@ public class ZipFileOutputSink extends FileSystemOutputSink { @Override public void writeDexFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) throws IOException { - writeToZipFile(getOutputFileName(primaryClassName), contents); + writeToZipFile(getOutputFileName(primaryClassName, DEX_EXTENSION), contents); + } + + @Override + public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName) + throws IOException { + writeToZipFile(getOutputFileName(primaryClassName, CLASS_EXTENSION), contents); } @Override diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java index f1ebcbb3f..96d9e8b5b 100644 --- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java +++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java @@ -7,6 +7,7 @@ import static com.android.tools.r8.TestCondition.R8_COMPILER; import static com.android.tools.r8.TestCondition.match; import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest; @@ -42,6 +43,11 @@ public class R8RunExamplesTest { DX, JAVAC, JAVAC_ALL, JAVAC_NONE } + enum Output { + DEX, + CF + } + private static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_BUILD_DIR; // For local testing on a specific Art version(s) change this set. e.g. to @@ -65,7 +71,7 @@ public class R8RunExamplesTest { TestCondition.runtimes(Version.V6_0_1, Version.V5_1_1, Version.V4_4_4))) .build(); - @Parameters(name = "{0}_{1}_{2}_{3}") + @Parameters(name = "{0}_{1}_{2}_{3}_{5}") public static Collection<String[]> data() { String[] tests = { "arithmetic.Arithmetic", @@ -115,6 +121,10 @@ public class R8RunExamplesTest { "switchmaps.Switches", }; + String[] javaBytecodeTests = { + "hello.Hello", + }; + List<String[]> fullTestList = new ArrayList<>(tests.length * 2); for (String test : tests) { fullTestList.add(makeTest(Input.JAVAC, CompilerUnderTest.D8, CompilationMode.DEBUG, test)); @@ -130,13 +140,26 @@ public class R8RunExamplesTest { test)); fullTestList.add(makeTest(Input.DX, CompilerUnderTest.R8, CompilationMode.RELEASE, test)); } + // TODO(zerny): Once all tests pass create the java tests in the main test loop. + for (String test : javaBytecodeTests) { + fullTestList.add( + makeTest(Input.JAVAC_ALL, CompilerUnderTest.R8, CompilationMode.DEBUG, test, Output.CF)); + fullTestList.add( + makeTest( + Input.JAVAC_ALL, CompilerUnderTest.R8, CompilationMode.RELEASE, test, Output.CF)); + } return fullTestList; } private static String[] makeTest( Input input, CompilerUnderTest compiler, CompilationMode mode, String clazz) { + return makeTest(input, compiler, mode, clazz, Output.DEX); + } + + private static String[] makeTest( + Input input, CompilerUnderTest compiler, CompilationMode mode, String clazz, Output output) { String pkg = clazz.substring(0, clazz.lastIndexOf('.')); - return new String[]{pkg, input.name(), compiler.name(), mode.name(), clazz}; + return new String[] {pkg, input.name(), compiler.name(), mode.name(), clazz, output.name()}; } @Rule @@ -147,18 +170,21 @@ public class R8RunExamplesTest { private final CompilationMode mode; private final String pkg; private final String mainClass; + private final Output output; public R8RunExamplesTest( String pkg, String input, String compiler, String mode, - String mainClass) { + String mainClass, + String output) { this.pkg = pkg; this.input = Input.valueOf(input); this.compiler = CompilerUnderTest.valueOf(compiler); this.mode = CompilationMode.valueOf(mode); this.mainClass = mainClass; + this.output = Output.valueOf(output); } private Path getOutputFile() { @@ -192,15 +218,19 @@ public class R8RunExamplesTest { return input == Input.DX ? DexTool.DX : DexTool.NONE; } + private Path getOutputPath() { + return temp.getRoot().toPath().resolve("out.jar"); + } + @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void compile() throws IOException, ProguardRuleParserException, ExecutionException, CompilationException { - Path out = temp.getRoot().toPath(); switch (compiler) { case D8: { + assertTrue(output == Output.DEX); ToolHelper.runD8(D8Command.builder() .addProgramFiles(getInputFile()) .setOutputPath(getOutputFile()) @@ -213,7 +243,8 @@ public class R8RunExamplesTest { .addProgramFiles(getInputFile()) .setOutputPath(getOutputFile()) .setMode(mode) - .build()); + .build(), + options -> options.outputClassFiles = (output == Output.CF)); break; } default: @@ -236,6 +267,20 @@ public class R8RunExamplesTest { fail("JVM failed for: " + mainClass); } + if (output == Output.CF) { + ToolHelper.ProcessResult result = + ToolHelper.runJava(ImmutableList.of(generated.toString()), mainClass); + if (result.exitCode != 0) { + System.err.println(result.stderr); + fail("JVM failed on compiled output for: " + mainClass); + } + assertEquals( + "JavaC/JVM and " + compiler.name() + "/JVM output differ", + javaResult.stdout, + result.stdout); + return; + } + DexVm vm = ToolHelper.getDexVm(); TestCondition condition = failingRun.get(mainClass); if (condition != null && condition.test(getTool(), compiler, vm.getVersion(), mode)) { diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java index 663901f3b..9baed4897 100644 --- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java +++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java @@ -21,6 +21,7 @@ import java.io.ByteArrayOutputStream; import java.io.StringReader; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class JasminBuilder { @@ -45,6 +46,10 @@ public class JasminBuilder { return name + ".j"; } + public String getDescriptor() { + return "L" + name + ";"; + } + public MethodSignature addVirtualMethod( String name, List<String> argumentTypes, @@ -185,7 +190,8 @@ public class JasminBuilder { return clazz.getSourceFile(); } }; - builder.addClassProgramData(origin, compile(clazz)); + builder.addClassProgramData( + origin, compile(clazz), Collections.singleton(clazz.getDescriptor())); } return builder.build(); } diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java index 6962f976f..67decf2a7 100644 --- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java +++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertTrue; import com.android.tools.r8.CompilationException; import com.android.tools.r8.R8; +import com.android.tools.r8.Resource; import com.android.tools.r8.ToolHelper; import com.android.tools.r8.ToolHelper.ProcessResult; import com.android.tools.r8.dex.ApplicationReader; @@ -27,10 +28,13 @@ import com.android.tools.r8.utils.OutputMode; import com.android.tools.r8.utils.StringUtils; import com.android.tools.r8.utils.Timing; import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; import jasmin.ClassFile; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; @@ -48,20 +52,37 @@ public class JasminTestBase { @Rule public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest(); + public static String getPathFromDescriptor(String classDescriptor) { + assert classDescriptor.startsWith("L"); + assert classDescriptor.endsWith(";"); + return classDescriptor.substring(1, classDescriptor.length() - 1) + ".class"; + } + protected ProcessResult runOnJavaRaw(JasminBuilder builder, String main) throws Exception { - File out = temp.newFolder("classes"); - for (ClassBuilder clazz : builder.getClasses()) { - ClassFile file = new ClassFile(); - file.readJasmin(new StringReader(clazz.toString()), clazz.name, false); - Path path = out.toPath().resolve(clazz.name + ".class"); + return runOnJavaRaw(builder.build(), main); + } + + protected ProcessResult runOnJavaRaw(AndroidApp app, String main) throws Exception { + File out = temp.newFolder(); + for (Resource clazz : app.getClassProgramResources()) { + assert clazz.getClassDescriptors().size() == 1; + String desc = clazz.getClassDescriptors().iterator().next(); + Path path = out.toPath().resolve(getPathFromDescriptor(desc)); Files.createDirectories(path.getParent()); - file.write(new FileOutputStream(path.toFile())); + try (InputStream input = clazz.getStream(); + OutputStream output = Files.newOutputStream(path)) { + ByteStreams.copy(input, output); + } } return ToolHelper.runJava(ImmutableList.of(out.getPath()), main); } protected String runOnJava(JasminBuilder builder, String main) throws Exception { - ProcessResult result = runOnJavaRaw(builder, main); + return runOnJava(builder.build(), main); + } + + protected String runOnJava(AndroidApp app, String main) throws Exception { + ProcessResult result = runOnJavaRaw(app, main); if (result.exitCode != 0) { System.out.println("Std out:"); System.out.println(result.stdout); |