| // 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.jasmin; | |
| import static com.android.tools.r8.utils.DescriptorUtils.getPathFromDescriptor; | |
| import com.android.tools.r8.ByteDataView; | |
| import com.android.tools.r8.ClassFileConsumer; | |
| import com.android.tools.r8.DiagnosticsHandler; | |
| import com.android.tools.r8.dex.ApplicationReader; | |
| import com.android.tools.r8.graph.DexApplication; | |
| import com.android.tools.r8.naming.MemberNaming.FieldSignature; | |
| import com.android.tools.r8.naming.MemberNaming.MethodSignature; | |
| import com.android.tools.r8.origin.Origin; | |
| import com.android.tools.r8.origin.PathOrigin; | |
| import com.android.tools.r8.utils.AndroidApp; | |
| import com.android.tools.r8.utils.DescriptorUtils; | |
| import com.android.tools.r8.utils.InternalOptions; | |
| import com.android.tools.r8.utils.StringUtils; | |
| import com.android.tools.r8.utils.StringUtils.BraceType; | |
| import com.android.tools.r8.utils.Timing; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.Iterables; | |
| import jasmin.ClassFile; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.StringReader; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.List; | |
| public class JasminBuilder { | |
| public enum ClassFileVersion { | |
| JDK_1_1 { | |
| @Override | |
| public int getMajorVersion() { | |
| return 45; | |
| } | |
| @Override | |
| public int getMinorVersion() { | |
| return 3; | |
| } | |
| }, | |
| JDK_1_2 { | |
| @Override | |
| public int getMajorVersion() { | |
| return 46; | |
| } | |
| }, | |
| JDK_1_3 { | |
| @Override | |
| public int getMajorVersion() { | |
| return 47; | |
| } | |
| }, | |
| JDK_1_4 { | |
| @Override | |
| public int getMajorVersion() { | |
| return 48; | |
| } | |
| }, | |
| /** JSE 5 is not fully supported by Jasmin. Interfaces will not work. */ | |
| JSE_5 { | |
| @Override | |
| public int getMajorVersion() { | |
| return 49; | |
| } | |
| }; | |
| public abstract int getMajorVersion(); | |
| public int getMinorVersion() { | |
| return 0; | |
| } | |
| } | |
| public class ClassBuilder { | |
| public final String name; | |
| public final String superName; | |
| public final ImmutableList<String> interfaces; | |
| private final List<String> methods = new ArrayList<>(); | |
| private final List<String> fields = new ArrayList<>(); | |
| private boolean makeInit = false; | |
| private boolean hasInit = false; | |
| private final List<String> clinit = new ArrayList<>(); | |
| private boolean isAbstract = false; | |
| private boolean isInterface = false; | |
| private String access = "public"; | |
| private ClassBuilder(String name) { | |
| this(name, "java/lang/Object"); | |
| } | |
| private ClassBuilder(String name, String superName) { | |
| this.name = name; | |
| this.superName = superName; | |
| this.interfaces = ImmutableList.of(); | |
| } | |
| private ClassBuilder(String name, String superName, String... interfaces) { | |
| this.name = name; | |
| this.superName = superName; | |
| this.interfaces = ImmutableList.copyOf(interfaces); | |
| } | |
| public String getSourceFile() { | |
| return name + ".j"; | |
| } | |
| public String getDescriptor() { | |
| return "L" + name + ";"; | |
| } | |
| public MethodSignature addAbstractMethod( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType) { | |
| return addMethod("public abstract", name, argumentTypes, returnType); | |
| } | |
| public MethodSignature addFinalMethod( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| makeInit = true; | |
| return addMethod("public final", name, argumentTypes, returnType, lines); | |
| } | |
| public MethodSignature addVirtualMethod(String name, String returnType, String... lines) { | |
| return addVirtualMethod(name, ImmutableList.of(), returnType, lines); | |
| } | |
| public MethodSignature addVirtualMethod( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| makeInit = true; | |
| return addMethod("public", name, argumentTypes, returnType, lines); | |
| } | |
| /** | |
| * Note that the JVM rejects native methods with code. This method is used to test that D8 | |
| * removes code from native methods. | |
| */ | |
| public MethodSignature addNativeMethodWithCode( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| makeInit = true; | |
| return addMethod("public static native", name, argumentTypes, returnType, lines); | |
| } | |
| public MethodSignature addBridgeMethod( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| makeInit = true; | |
| return addMethod("public bridge", name, argumentTypes, returnType, lines); | |
| } | |
| public MethodSignature addPrivateVirtualMethod( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| makeInit = true; | |
| return addMethod("private", name, argumentTypes, returnType, lines); | |
| } | |
| public MethodSignature addStaticMethod( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| return addMethod("public static", name, argumentTypes, returnType, lines); | |
| } | |
| public MethodBuilder staticMethodBuilder( | |
| String name, List<String> argumentTypes, String returnType) { | |
| return new MethodBuilder(name, argumentTypes, returnType, this).setPublic().setStatic(); | |
| } | |
| public MethodSignature addPackagePrivateStaticMethod( | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| return addMethod("static", name, argumentTypes, returnType, lines); | |
| } | |
| public MethodSignature addMainMethod(Iterable<String> lines) { | |
| return addMainMethod(Iterables.toArray(lines, String.class)); | |
| } | |
| public MethodSignature addMainMethod(String... lines) { | |
| return addStaticMethod("main", ImmutableList.of("[Ljava/lang/String;"), "V", lines); | |
| } | |
| public MethodSignature addMethod( | |
| String access, | |
| String name, | |
| List<String> argumentTypes, | |
| String returnType, | |
| String... lines) { | |
| StringBuilder builder = new StringBuilder(); | |
| builder.append(".method ").append(access).append(" ").append(name) | |
| .append(StringUtils.join(argumentTypes, "", BraceType.PARENS)) | |
| .append(returnType).append("\n"); | |
| for (String line : lines) { | |
| builder.append(line).append("\n"); | |
| } | |
| builder.append(".end method\n"); | |
| methods.add(builder.toString()); | |
| String returnJavaType = DescriptorUtils.descriptorToJavaType(returnType); | |
| String[] argumentJavaTypes = new String[argumentTypes.size()]; | |
| for (int i = 0; i < argumentTypes.size(); i++) { | |
| argumentJavaTypes[i] = DescriptorUtils.descriptorToJavaType(argumentTypes.get(i)); | |
| } | |
| return new MethodSignature(name, returnJavaType, argumentJavaTypes); | |
| } | |
| public void addClassInitializer(String... lines) { | |
| clinit.addAll(Arrays.asList(lines)); | |
| } | |
| public FieldSignature addField(String flags, String name, String type, String value) { | |
| fields.add( | |
| ".field " + flags + " " + name + " " + type + (value != null ? (" = " + value) : "")); | |
| return new FieldSignature(name, type); | |
| } | |
| public FieldSignature addStaticField(String name, String type) { | |
| return addStaticField(name, type, null); | |
| } | |
| public FieldSignature addStaticField(String name, String type, String value) { | |
| return addField("public static", name, type, value); | |
| } | |
| public FieldSignature addStaticFinalField(String name, String type, String value) { | |
| return addField("public static final", name, type, value); | |
| } | |
| @Override | |
| public String toString() { | |
| StringBuilder builder = new StringBuilder(); | |
| builder.append(".bytecode ").append(majorVersion).append('.').append(minorVersion) | |
| .append('\n'); | |
| builder.append(".source ").append(getSourceFile()).append('\n'); | |
| builder.append(".class"); | |
| if (isAbstract) { | |
| builder.append(" abstract"); | |
| } else if (isInterface) { | |
| builder.append(" interface abstract"); | |
| } | |
| builder.append(" ").append(access).append(" ").append(name).append('\n'); | |
| builder.append(".super ").append(superName).append('\n'); | |
| for (String iface : interfaces) { | |
| builder.append(".implements ").append(iface).append('\n'); | |
| } | |
| if (makeInit && !hasInit) { | |
| builder | |
| .append(".method public <init>()V\n") | |
| .append(".limit locals 1\n") | |
| .append(".limit stack 1\n") | |
| .append(" aload 0\n") | |
| .append(" invokespecial ").append(superName).append("/<init>()V\n") | |
| .append(" return\n") | |
| .append(".end method\n"); | |
| } | |
| for (String field : fields) { | |
| builder.append(field).append("\n"); | |
| } | |
| for (String method : methods) { | |
| builder.append(method).append("\n"); | |
| } | |
| if (!clinit.isEmpty()) { | |
| builder.append(".method public static <clinit>()V\n"); | |
| clinit.forEach(line -> builder.append(line).append('\n')); | |
| builder.append(".end method\n"); | |
| } | |
| return builder.toString(); | |
| } | |
| public void setIsAbstract() { | |
| isAbstract = true; | |
| } | |
| public void setIsInterface() { | |
| isInterface = true; | |
| } | |
| public void setAccess(String access) { | |
| this.access = access; | |
| } | |
| public MethodSignature addDefaultConstructor() { | |
| assert !hasInit; | |
| hasInit = true; | |
| return addMethod("public", "<init>", Collections.emptyList(), "V", | |
| ".limit stack 1", | |
| ".limit locals 1", | |
| " aload_0", | |
| " invokenonvirtual " + superName + "/<init>()V", | |
| " return"); | |
| } | |
| } | |
| public class MethodBuilder { | |
| private final String name; | |
| private final List<String> argumentTypes; | |
| private final String returnType; | |
| private final ClassBuilder parent; | |
| private List<String> instructions = new ArrayList<>(); | |
| private int localsLimit = -1; | |
| private int stackLimit = -1; | |
| private boolean isPublic = false; | |
| private boolean isStatic = false; | |
| public MethodBuilder( | |
| String name, List<String> argumentTypes, String returnType, ClassBuilder parent) { | |
| this.name = name; | |
| this.argumentTypes = argumentTypes; | |
| this.returnType = returnType; | |
| this.parent = parent; | |
| } | |
| public MethodBuilder setPublic() { | |
| isPublic = true; | |
| return this; | |
| } | |
| public MethodBuilder setStatic() { | |
| isStatic = true; | |
| return this; | |
| } | |
| public MethodBuilder setCode(String... lines) { | |
| assert Arrays.stream(lines).noneMatch(line -> line.contains(".limit")); | |
| instructions.addAll(Arrays.asList(lines)); | |
| return this; | |
| } | |
| public MethodBuilder setLocalsLimit(int localsLimit) { | |
| assert this.localsLimit < 0; | |
| this.localsLimit = localsLimit; | |
| return this; | |
| } | |
| public MethodBuilder setStackLimit(int stackLimit) { | |
| assert this.stackLimit < 0; | |
| this.stackLimit = stackLimit; | |
| return this; | |
| } | |
| public void build() { | |
| if (localsLimit < 0 || stackLimit < 0) { | |
| inferConservativeLimits(); | |
| } | |
| StringBuilder builder = new StringBuilder(); | |
| builder.append(".method "); | |
| if (isPublic) { | |
| builder.append("public "); | |
| } | |
| if (isStatic) { | |
| builder.append("static "); | |
| } | |
| builder | |
| .append(name) | |
| .append(StringUtils.join(argumentTypes, "", BraceType.PARENS)) | |
| .append(returnType) | |
| .append(System.lineSeparator()); | |
| builder.append(".limit locals ").append(localsLimit).append(System.lineSeparator()); | |
| builder.append(".limit stack ").append(stackLimit).append(System.lineSeparator()); | |
| for (String line : instructions) { | |
| builder.append(line).append(System.lineSeparator()); | |
| } | |
| builder.append(".end method").append(System.lineSeparator()); | |
| parent.methods.add(builder.toString()); | |
| } | |
| private void inferConservativeLimits() { | |
| int conservativeLimit = instructions.size() + argumentTypes.size(); | |
| if (localsLimit < 0) { | |
| setLocalsLimit(conservativeLimit); | |
| } | |
| if (stackLimit < 0) { | |
| setStackLimit(conservativeLimit); | |
| } | |
| } | |
| } | |
| private final List<ClassBuilder> classes = new ArrayList<>(); | |
| private final int minorVersion; | |
| private final int majorVersion; | |
| public JasminBuilder() { | |
| this(ClassFileVersion.JDK_1_4); | |
| } | |
| public JasminBuilder(ClassFileVersion version) { | |
| majorVersion = version.getMajorVersion(); | |
| minorVersion = version.getMinorVersion(); | |
| } | |
| public ClassBuilder addClass(String name) { | |
| ClassBuilder builder = new ClassBuilder(name); | |
| classes.add(builder); | |
| return builder; | |
| } | |
| public ClassBuilder addClass(String name, String superName) { | |
| ClassBuilder builder = new ClassBuilder(name, superName); | |
| classes.add(builder); | |
| return builder; | |
| } | |
| public ClassBuilder addClass(String name, String superName, String... interfaces) { | |
| ClassBuilder builder = new ClassBuilder(name, superName, interfaces); | |
| classes.add(builder); | |
| return builder; | |
| } | |
| public ClassBuilder addInterface(String name, String... interfaces) { | |
| // Interfaces are broken in Jasmin (the ACC_SUPER access flag is set) and the JSE_5 and later | |
| // will not load corresponding classes. | |
| assert majorVersion <= ClassFileVersion.JDK_1_4.getMajorVersion(); | |
| ClassBuilder builder = new ClassBuilder(name, "java/lang/Object", interfaces); | |
| builder.setIsInterface(); | |
| classes.add(builder); | |
| return builder; | |
| } | |
| public ImmutableList<ClassBuilder> getClasses() { | |
| return ImmutableList.copyOf(classes); | |
| } | |
| private static byte[] compile(ClassBuilder builder) throws Exception { | |
| ClassFile file = new ClassFile(); | |
| file.readJasmin(new StringReader(builder.toString()), builder.name, false); | |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |
| file.write(out); | |
| return out.toByteArray(); | |
| } | |
| public ImmutableList.Builder<byte[]> buildClasses(ImmutableList.Builder<byte[]> builder) | |
| throws Exception { | |
| for (ClassBuilder clazz : classes) { | |
| builder.add(compile(clazz)); | |
| } | |
| return builder; | |
| } | |
| public List<byte[]> buildClasses() throws Exception { | |
| return buildClasses(ImmutableList.builder()).build(); | |
| } | |
| public AndroidApp build() throws Exception { | |
| Origin root = new PathOrigin(Paths.get("JasminBuilder")); | |
| AndroidApp.Builder builder = AndroidApp.builder(); | |
| for (ClassBuilder clazz : classes) { | |
| Origin origin = new Origin(root) { | |
| @Override | |
| public String part() { | |
| return clazz.getSourceFile(); | |
| } | |
| }; | |
| builder.addClassProgramData( | |
| compile(clazz), origin, Collections.singleton(clazz.getDescriptor())); | |
| } | |
| return builder.build(); | |
| } | |
| public List<Path> writeClassFiles(Path output) throws Exception { | |
| List<Path> outputs = new ArrayList<>(classes.size()); | |
| for (ClassBuilder clazz : classes) { | |
| Path path = output.resolve(getPathFromDescriptor(clazz.getDescriptor())); | |
| Files.createDirectories(path.getParent()); | |
| Files.write(path, compile(clazz)); | |
| outputs.add(path); | |
| } | |
| return outputs; | |
| } | |
| public void writeClassFiles(ClassFileConsumer consumer, DiagnosticsHandler handler) | |
| throws Exception { | |
| for (ClassBuilder clazz : classes) { | |
| consumer.accept(ByteDataView.of(compile(clazz)), clazz.getDescriptor(), handler); | |
| } | |
| } | |
| public void writeJar(Path output) throws Exception { | |
| writeJar(output, null); | |
| } | |
| public void writeJar(Path output, DiagnosticsHandler handler) throws Exception { | |
| ClassFileConsumer consumer = new ClassFileConsumer.ArchiveConsumer(output); | |
| writeClassFiles(consumer, handler); | |
| consumer.finished(handler); | |
| } | |
| public DexApplication read() throws Exception { | |
| return read(new InternalOptions()); | |
| } | |
| public DexApplication read(InternalOptions options) throws Exception { | |
| Timing timing = new Timing("JasminTest"); | |
| return new ApplicationReader(build(), options, timing).read(); | |
| } | |
| } |