| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package art.constmethodhandle; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.lang.invoke.CallSite; |
| import java.lang.invoke.MethodHandle; |
| import java.lang.invoke.MethodHandles; |
| import java.lang.invoke.MethodType; |
| import java.nio.file.*; |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.ClassVisitor; |
| import org.objectweb.asm.ClassWriter; |
| import org.objectweb.asm.Handle; |
| import org.objectweb.asm.MethodVisitor; |
| import org.objectweb.asm.Opcodes; |
| import org.objectweb.asm.Type; |
| |
| // This test will modify in place the compiled java files to fill in the transformed version and |
| // fill in the TestInvoker.runTest function with a load-constant of a method-handle. It will use d8 |
| // (passed in as an argument) to create the dex we will transform TestInvoke into. |
| public class TestGenerator { |
| |
| public static void main(String[] args) throws IOException { |
| if (args.length != 2) { |
| throw new Error("Unable to convert class to dex without d8 binary!"); |
| } |
| Path base = Paths.get(args[0]); |
| String d8Bin = args[1]; |
| |
| Path initTestInvoke = base.resolve(TestGenerator.class.getPackage().getName().replace('.', '/')) |
| .resolve(TestInvoke.class.getSimpleName() + ".class"); |
| byte[] initClass = new FileInputStream(initTestInvoke.toFile()).readAllBytes(); |
| |
| // Make the initial version of TestInvoker |
| generateInvoker(initClass, "sayHi", new FileOutputStream(initTestInvoke.toFile())); |
| |
| // Make the final 'class' version of testInvoker |
| ByteArrayOutputStream finalClass = new ByteArrayOutputStream(); |
| generateInvoker(initClass, "sayBye", finalClass); |
| |
| Path initTest1948 = base.resolve("art").resolve(art.Test1948.class.getSimpleName() + ".class"); |
| byte[] finalClassBytes = finalClass.toByteArray(); |
| byte[] finalDexBytes = getFinalDexBytes(d8Bin, finalClassBytes); |
| generateTestCode( |
| new FileInputStream(initTest1948.toFile()).readAllBytes(), |
| finalClassBytes, |
| finalDexBytes, |
| new FileOutputStream(initTest1948.toFile())); |
| } |
| |
| // Modify the Test1948 class bytecode so it has the transformed version of TestInvoker as a string |
| // constant. |
| private static void generateTestCode( |
| byte[] initClass, byte[] transClass, byte[] transDex, OutputStream out) throws IOException { |
| ClassReader cr = new ClassReader(initClass); |
| ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); |
| cr.accept( |
| new ClassVisitor(Opcodes.ASM9, cw) { |
| @Override |
| public void visitEnd() { |
| generateStringAccessorMethod( |
| cw, "getDexBase64", Base64.getEncoder().encodeToString(transDex)); |
| generateStringAccessorMethod( |
| cw, "getClassBase64", Base64.getEncoder().encodeToString(transClass)); |
| super.visitEnd(); |
| } |
| }, 0); |
| out.write(cw.toByteArray()); |
| } |
| |
| // Insert a string accessor method so we can get the transformed versions of TestInvoker. |
| private static void generateStringAccessorMethod(ClassVisitor cv, String name, String ret) { |
| MethodVisitor mv = cv.visitMethod( |
| Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC, |
| name, "()Ljava/lang/String;", null, null); |
| mv.visitLdcInsn(ret); |
| mv.visitInsn(Opcodes.ARETURN); |
| mv.visitMaxs(-1, -1); |
| } |
| |
| // Use d8bin to convert the classBytes into a dex file bytes. We need to do this here because we |
| // need the dex-file bytes to be used by the test class to redefine TestInvoker. We use d8 because |
| // it doesn't require setting up a directory structures or matching file names like dx does. |
| // TODO We should maybe just call d8 functions directly? |
| private static byte[] getFinalDexBytes(String d8Bin, byte[] classBytes) throws IOException { |
| Path tempDir = Files.createTempDirectory("FinalTestInvoker_Gen"); |
| File tempInput = Files.createTempFile(tempDir, "temp_input_class", ".class").toFile(); |
| |
| OutputStream tempClassStream = new FileOutputStream(tempInput); |
| tempClassStream.write(classBytes); |
| tempClassStream.close(); |
| tempClassStream = null; |
| |
| Process d8Proc = new ProcessBuilder(d8Bin, |
| // Put classes.dex in the temp-dir we made. |
| "--output", tempDir.toAbsolutePath().toString(), |
| "--min-api", "28", // Allow the new invoke ops. |
| "--no-desugaring", // Don't try to be clever please. |
| tempInput.toPath().toAbsolutePath().toString()) |
| .inheritIO() // Just print to stdio. |
| .start(); |
| int res; |
| try { |
| res = d8Proc.waitFor(); |
| } catch (Exception e) { |
| System.out.println("Failed to dex: ".concat(e.toString())); |
| e.printStackTrace(); |
| res = -123; |
| } |
| tempInput.delete(); |
| try { |
| if (res == 0) { |
| byte[] out = new FileInputStream(tempDir.resolve("classes.dex").toFile()).readAllBytes(); |
| tempDir.resolve("classes.dex").toFile().delete(); |
| return out; |
| } |
| } finally { |
| tempDir.toFile().delete(); |
| } |
| throw new Error("Failed to get dex file! " + res); |
| } |
| |
| private static void generateInvoker( |
| byte[] inputClass, |
| String toCall, |
| OutputStream output) throws IOException { |
| ClassReader cr = new ClassReader(inputClass); |
| ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); |
| cr.accept( |
| new ClassVisitor(Opcodes.ASM9, cw) { |
| @Override |
| public void visitEnd() { |
| generateRunTest(cw, toCall); |
| super.visitEnd(); |
| } |
| }, 0); |
| output.write(cw.toByteArray()); |
| } |
| |
| // Creates the following method: |
| // public runTest(Runnable preCall) { |
| // preCall.run(); |
| // MethodHandle mh = <CONSTANT MH>; |
| // mh.invokeExact(); |
| // } |
| private static void generateRunTest(ClassVisitor cv, String toCall) { |
| MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, |
| "runTest", "(Ljava/lang/Runnable;)V", null, null); |
| MethodType mt = MethodType.methodType(Void.TYPE); |
| Handle mh = new Handle( |
| Opcodes.H_INVOKESTATIC, |
| Type.getInternalName(Responses.class), |
| toCall, |
| mt.toMethodDescriptorString(), |
| false); |
| String internalName = Type.getInternalName(Runnable.class); |
| mv.visitVarInsn(Opcodes.ALOAD, 1); |
| mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, internalName, "run", "()V", true); |
| mv.visitLdcInsn(mh); |
| mv.visitMethodInsn( |
| Opcodes.INVOKEVIRTUAL, |
| Type.getInternalName(MethodHandle.class), |
| "invokeExact", |
| "()V", |
| false); |
| mv.visitInsn(Opcodes.RETURN); |
| mv.visitMaxs(-1, -1); |
| } |
| } |