Use ASM for invoke-custom tests

Removes the dependency of binary blobs generated by the jack compiler.

Fixes the exception chain raised when a bootstrap method returns null.

Bug: 73807070
Test: art/test/run-test --host 952-invoke-custom
Change-Id: Iac615cdeec342b1b67f50bb1258768e199adff10
diff --git a/test/952-invoke-custom/src/transformer/IndyTransformer.java b/test/952-invoke-custom/src/transformer/IndyTransformer.java
new file mode 100644
index 0000000..286c098
--- /dev/null
+++ b/test/952-invoke-custom/src/transformer/IndyTransformer.java
@@ -0,0 +1,263 @@
+/*
+ * 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 transformer;
+
+import annotations.CalledByIndy;
+import annotations.Constant;
+import annotations.LinkerFieldHandle;
+import annotations.LinkerMethodHandle;
+import annotations.MethodHandleKind;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+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;
+
+/**
+ * Class for inserting invoke-dynamic instructions in annotated Java class files.
+ *
+ * This class replaces static method invocations of annotated methods
+ * with invoke-dynamic instructions. Suppose a method is annotated as:
+ *
+ * @CalledByIndy(
+ *     invokeMethodHandle =
+ *         @LinkerMethodHandle(
+ *              kind = MethodHandleKind.INVOKE_STATIC,
+ *                  enclosingType = TestLinkerMethodMinimalArguments.class,
+ *                  argumentTypes = {MethodHandles.Lookup.class, String.class, MethodType.class},
+ *                  name = "linkerMethod"
+ *              ),
+ *     name = "magicAdd",
+ *     returnType = int.class,
+ *     argumentTypes = {int.class, int.class}
+ * )
+ * private int add(int x, int y) {
+ *    throw new UnsupportedOperationException(e);
+ * }
+ *
+ * private int magicAdd(int x, int y) {
+ *    return x + y;
+ * }
+ *
+ * Then invokestatic bytecodes targeting the add() method will be
+ * replaced invokedynamic instructions targetting the CallSite that is
+ * construction by the bootstrap method described by the @CalledByIndy
+ * annotation.
+ *
+ * In the example above, this results in add() being replaced by
+ * invocations of magicAdd().
+ */
+class IndyTransformer {
+
+    static class BootstrapBuilder extends ClassVisitor {
+
+        private final Map<String, CalledByIndy> callsiteMap;
+        private final Map<String, Handle> bsmMap = new HashMap<>();
+
+        public BootstrapBuilder(int api, Map<String, CalledByIndy> callsiteMap) {
+            this(api, null, callsiteMap);
+        }
+
+        public BootstrapBuilder(int api, ClassVisitor cv, Map<String, CalledByIndy> callsiteMap) {
+            super(api, cv);
+            this.callsiteMap = callsiteMap;
+        }
+
+        @Override
+        public MethodVisitor visitMethod(
+                int access, String name, String desc, String signature, String[] exceptions) {
+            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
+            return new MethodVisitor(this.api, mv) {
+                @Override
+                public void visitMethodInsn(
+                        int opcode, String owner, String name, String desc, boolean itf) {
+                    if (opcode == org.objectweb.asm.Opcodes.INVOKESTATIC) {
+                        CalledByIndy callsite = callsiteMap.get(name);
+                        if (callsite != null) {
+                            insertIndy(callsite.name(), desc, callsite);
+                            return;
+                        }
+                    }
+                    mv.visitMethodInsn(opcode, owner, name, desc, itf);
+                }
+
+                private void insertIndy(String name, String desc, CalledByIndy callsite) {
+                    Handle bsm = buildBootstrapMethodHandle(callsite);
+                    Object[] bsmArgs = buildBootstrapArguments(callsite);
+                    mv.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
+                }
+
+                private Handle buildBootstrapMethodHandle(CalledByIndy callsite) {
+                    MethodHandleKind kind;
+                    if (callsite.fieldMethodHandle().length != 0) {
+                        return buildBootstrapMethodHandleForField(callsite.fieldMethodHandle()[0]);
+                    } else if (callsite.invokeMethodHandle().length != 0) {
+                        return buildBootstrapMethodHandleForMethod(
+                                callsite.invokeMethodHandle()[0]);
+                    } else {
+                        throw new Error("Missing linker method handle in CalledByIndy annotation");
+                    }
+                }
+
+                private Handle buildBootstrapMethodHandleForField(LinkerFieldHandle fieldHandle) {
+                    int handleKind;
+                    switch (fieldHandle.kind()) {
+                        case GET_FIELD:
+                            handleKind = Opcodes.H_GETFIELD;
+                            break;
+                        case GET_STATIC:
+                            handleKind = Opcodes.H_GETSTATIC;
+                            break;
+                        case PUT_FIELD:
+                            handleKind = Opcodes.H_PUTFIELD;
+                            break;
+                        case PUT_STATIC:
+                            handleKind = Opcodes.H_PUTSTATIC;
+                            break;
+                        default:
+                            throw new Error("Unknown field invocation kind: " + fieldHandle.kind());
+                    }
+                    Class<?> resolverClass = fieldHandle.enclosingType();
+                    String resolverMethod = fieldHandle.name();
+                    Class<?> resolverReturnType = fieldHandle.type();
+
+                    // TODO: arguments types to invoke resolver with (default + extra args).
+                    throw new Error("WIP");
+                }
+
+                private Handle buildBootstrapMethodHandleForMethod(
+                        LinkerMethodHandle methodHandle) {
+                    int handleKind;
+                    switch (methodHandle.kind()) {
+                        case INVOKE_CONSTRUCTOR:
+                            handleKind = Opcodes.H_NEWINVOKESPECIAL;
+                            break;
+                        case INVOKE_INTERFACE:
+                            handleKind = Opcodes.H_INVOKEINTERFACE;
+                            break;
+                        case INVOKE_SPECIAL:
+                            handleKind = Opcodes.H_INVOKESPECIAL;
+                            break;
+                        case INVOKE_STATIC:
+                            handleKind = Opcodes.H_INVOKESTATIC;
+                            break;
+                        case INVOKE_VIRTUAL:
+                            handleKind = Opcodes.H_INVOKEVIRTUAL;
+                            break;
+                        default:
+                            throw new Error(
+                                    "Unknown method invocation kind: " + methodHandle.kind());
+                    }
+                    String className = Type.getInternalName(methodHandle.enclosingType());
+                    String methodName = methodHandle.name();
+                    String methodType =
+                            MethodType.methodType(
+                                            methodHandle.returnType(), methodHandle.argumentTypes())
+                                    .toMethodDescriptorString();
+                    return new Handle(
+                            handleKind, className, methodName, methodType, false /* itf */);
+                }
+
+                private Object decodeConstant(int index, Constant constant) {
+                    if (constant.booleanValue().length == 1) {
+                        return constant.booleanValue()[0];
+                    } else if (constant.byteValue().length == 1) {
+                        return constant.byteValue()[0];
+                    } else if (constant.charValue().length == 1) {
+                        return constant.charValue()[0];
+                    } else if (constant.shortValue().length == 1) {
+                        return constant.shortValue()[0];
+                    } else if (constant.intValue().length == 1) {
+                        return constant.intValue()[0];
+                    } else if (constant.longValue().length == 1) {
+                        return constant.longValue()[0];
+                    } else if (constant.floatValue().length == 1) {
+                        return constant.floatValue()[0];
+                    } else if (constant.doubleValue().length == 1) {
+                        return constant.doubleValue()[0];
+                    } else if (constant.stringValue().length == 1) {
+                        return constant.stringValue()[0];
+                    } else if (constant.classValue().length == 1) {
+                        return Type.getType(constant.classValue()[0]);
+                    } else {
+                        throw new Error("Bad constant at index " + index);
+                    }
+                }
+
+                private Object[] buildBootstrapArguments(CalledByIndy callsite) {
+                    Constant[] rawArgs = callsite.methodHandleExtraArgs();
+                    Object[] args = new Object[rawArgs.length];
+                    for (int i = 0; i < rawArgs.length; ++i) {
+                        args[i] = decodeConstant(i, rawArgs[i]);
+                    }
+                    return args;
+                }
+            };
+        }
+    }
+
+    private static void transform(Path inputClassPath, Path outputClassPath) throws Throwable {
+        URLClassLoader classLoader =
+                new URLClassLoader(
+                        new URL[] {inputClassPath.toUri().toURL()},
+                        ClassLoader.getSystemClassLoader());
+        String inputClassName = inputClassPath.getFileName().toString().replace(".class", "");
+        Class<?> inputClass = classLoader.loadClass(inputClassName);
+        Map<String, CalledByIndy> callsiteMap = new HashMap<>();
+
+        for (Method m : inputClass.getDeclaredMethods()) {
+            CalledByIndy calledByIndy = m.getAnnotation(CalledByIndy.class);
+            if (calledByIndy == null) {
+                continue;
+            }
+            if (calledByIndy.name() == null) {
+                throw new Error("CallByIndy annotation does not specify name");
+            }
+            final int PRIVATE_STATIC = Modifier.STATIC | Modifier.PRIVATE;
+            if ((m.getModifiers() & PRIVATE_STATIC) != PRIVATE_STATIC) {
+                throw new Error(
+                        "Method whose invocations should be replaced should be private and static");
+            }
+            callsiteMap.put(m.getName(), calledByIndy);
+        }
+        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+        try (InputStream is = Files.newInputStream(inputClassPath)) {
+            ClassReader cr = new ClassReader(is);
+            cr.accept(new BootstrapBuilder(Opcodes.ASM6, cw, callsiteMap), 0);
+        }
+        try (OutputStream os = Files.newOutputStream(outputClassPath)) {
+            os.write(cw.toByteArray());
+        }
+    }
+
+    public static void main(String[] args) throws Throwable {
+        transform(Paths.get(args[0]), Paths.get(args[1]));
+    }
+}