203 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Java
		
	
	
	
			
		
		
	
	
			203 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Java
		
	
	
	
| /*
 | |
|  * 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.BootstrapMethod;
 | |
| import annotations.CalledByIndy;
 | |
| import annotations.Constant;
 | |
| 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.
 | |
|  *
 | |
|  * <p>This class replaces static method invocations of annotated methods with invoke-dynamic
 | |
|  * instructions. Suppose a method is annotated as: <code>
 | |
|  *
 | |
|  * @CalledByIndy(
 | |
|  *      bootstrapMethod =
 | |
|  *          @BootstrapMethod(
 | |
|  *               enclosingType = TestLinkerMethodMinimalArguments.class,
 | |
|  *               parameterTypes = {MethodHandles.Lookup.class, String.class, MethodType.class},
 | |
|  *               name = "linkerMethod"
 | |
|  *     ),
 | |
|  *     fieldOdMethodName = "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;
 | |
|  * }
 | |
|  *
 | |
|  * </code> 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.
 | |
|  *
 | |
|  * <p>In the example above, this results in add() being replaced by invocations of magicAdd().
 | |
|  */
 | |
| public 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.fieldOrMethodName(), desc, callsite);
 | |
|                             return;
 | |
|                         }
 | |
|                     }
 | |
|                     mv.visitMethodInsn(opcode, owner, name, desc, itf);
 | |
|                 }
 | |
| 
 | |
|                 private void insertIndy(String name, String desc, CalledByIndy callsite) {
 | |
|                     Handle bsm = buildBootstrapMethodHandle(callsite.bootstrapMethod()[0]);
 | |
|                     Object[] bsmArgs =
 | |
|                             buildBootstrapArguments(callsite.constantArgumentsForBootstrapMethod());
 | |
|                     mv.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
 | |
|                 }
 | |
| 
 | |
|                 private Handle buildBootstrapMethodHandle(BootstrapMethod bootstrapMethod) {
 | |
|                     String className = Type.getInternalName(bootstrapMethod.enclosingType());
 | |
|                     String methodName = bootstrapMethod.name();
 | |
|                     String methodType =
 | |
|                             MethodType.methodType(
 | |
|                                             bootstrapMethod.returnType(),
 | |
|                                             bootstrapMethod.parameterTypes())
 | |
|                                     .toMethodDescriptorString();
 | |
|                     return new Handle(
 | |
|                             Opcodes.H_INVOKESTATIC,
 | |
|                             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(Constant[] bootstrapMethodArguments) {
 | |
|                     Object[] args = new Object[bootstrapMethodArguments.length];
 | |
|                     for (int i = 0; i < bootstrapMethodArguments.length; ++i) {
 | |
|                         args[i] = decodeConstant(i, bootstrapMethodArguments[i]);
 | |
|                     }
 | |
|                     return args;
 | |
|                 }
 | |
|             };
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private static void transform(Path inputClassPath, Path outputClassPath) throws Throwable {
 | |
|         URL url = inputClassPath.getParent().toUri().toURL();
 | |
|         URLClassLoader classLoader =
 | |
|                 new URLClassLoader(new URL[] {url}, 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.fieldOrMethodName() == null) {
 | |
|                 throw new Error("CallByIndy annotation does not specify a field or method 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.ASM7, 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]));
 | |
|     }
 | |
| }
 |