/* * 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.ConstantMethodHandle; import annotations.ConstantMethodType; import java.io.InputStream; import java.io.OutputStream; import java.lang.invoke.MethodHandle; 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 transforming invoke static bytecodes into constant method handle loads and and constant * method type loads. * *

When a parameterless private static method returning a MethodHandle is defined and annotated * with {@code ConstantMethodHandle}, this transformer will replace static invocations of the method * with a load constant bytecode with a method handle in the constant pool. * *

Suppose a method is annotated as: * @ConstantMethodHandle( * kind = ConstantMethodHandle.STATIC_GET, * owner = "java/lang/Math", * fieldOrMethodName = "E", * descriptor = "D" * ) * private static MethodHandle getMathE() { * unreachable(); * return null; * } * Then invocations of {@code getMathE} will be replaced by a load from the constant pool * with the constant method handle described in the {@code ConstantMethodHandle} annotation. * *

Similarly, a parameterless private static method returning a {@code MethodType} and annotated * with {@code ConstantMethodType}, will have invocations replaced by a load constant bytecode with * a method type in the constant pool. */ class ConstantTransformer { static class ConstantBuilder extends ClassVisitor { private final Map constantMethodHandles; private final Map constantMethodTypes; ConstantBuilder( int api, ClassVisitor cv, Map constantMethodHandles, Map constantMethodTypes) { super(api, cv); this.constantMethodHandles = constantMethodHandles; this.constantMethodTypes = constantMethodTypes; } @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) { ConstantMethodHandle constantMethodHandle = constantMethodHandles.get(name); if (constantMethodHandle != null) { insertConstantMethodHandle(constantMethodHandle); return; } ConstantMethodType constantMethodType = constantMethodTypes.get(name); if (constantMethodType != null) { insertConstantMethodType(constantMethodType); return; } } mv.visitMethodInsn(opcode, owner, name, desc, itf); } private Type buildMethodType(Class returnType, Class[] parameterTypes) { Type rType = Type.getType(returnType); Type[] pTypes = new Type[parameterTypes.length]; for (int i = 0; i < pTypes.length; ++i) { pTypes[i] = Type.getType(parameterTypes[i]); } return Type.getMethodType(rType, pTypes); } private int getHandleTag(int kind) { switch (kind) { case ConstantMethodHandle.STATIC_PUT: return Opcodes.H_PUTSTATIC; case ConstantMethodHandle.STATIC_GET: return Opcodes.H_GETSTATIC; case ConstantMethodHandle.INSTANCE_PUT: return Opcodes.H_PUTFIELD; case ConstantMethodHandle.INSTANCE_GET: return Opcodes.H_GETFIELD; case ConstantMethodHandle.INVOKE_STATIC: return Opcodes.H_INVOKESTATIC; case ConstantMethodHandle.INVOKE_VIRTUAL: return Opcodes.H_INVOKEVIRTUAL; case ConstantMethodHandle.INVOKE_SPECIAL: return Opcodes.H_INVOKESPECIAL; case ConstantMethodHandle.NEW_INVOKE_SPECIAL: return Opcodes.H_NEWINVOKESPECIAL; case ConstantMethodHandle.INVOKE_INTERFACE: return Opcodes.H_INVOKEINTERFACE; } throw new Error("Unhandled kind " + kind); } private void insertConstantMethodHandle(ConstantMethodHandle constantMethodHandle) { Handle handle = new Handle( getHandleTag(constantMethodHandle.kind()), constantMethodHandle.owner(), constantMethodHandle.fieldOrMethodName(), constantMethodHandle.descriptor(), constantMethodHandle.ownerIsInterface()); mv.visitLdcInsn(handle); } private void insertConstantMethodType(ConstantMethodType constantMethodType) { Type methodType = buildMethodType( constantMethodType.returnType(), constantMethodType.parameterTypes()); mv.visitLdcInsn(methodType); } }; } } private static void throwAnnotationError( Method method, Class annotationClass, String reason) { StringBuilder sb = new StringBuilder(); sb.append("Error in annotation ") .append(annotationClass) .append(" on method ") .append(method) .append(": ") .append(reason); throw new Error(sb.toString()); } private static void checkMethodToBeReplaced( Method method, Class annotationClass, Class returnType) { final int PRIVATE_STATIC = Modifier.STATIC | Modifier.PRIVATE; if ((method.getModifiers() & PRIVATE_STATIC) != PRIVATE_STATIC) { throwAnnotationError(method, annotationClass, " method is not private and static"); } if (method.getTypeParameters().length != 0) { throwAnnotationError(method, annotationClass, " method expects parameters"); } if (!method.getReturnType().equals(returnType)) { throwAnnotationError(method, annotationClass, " wrong return type"); } } private static void transform(Path inputClassPath, Path outputClassPath) throws Throwable { Path classLoadPath = inputClassPath.toAbsolutePath().getParent(); URLClassLoader classLoader = new URLClassLoader(new URL[] {classLoadPath.toUri().toURL()}, ClassLoader.getSystemClassLoader()); String inputClassName = inputClassPath.getFileName().toString().replace(".class", ""); Class inputClass = classLoader.loadClass(inputClassName); final Map constantMethodHandles = new HashMap<>(); final Map constantMethodTypes = new HashMap<>(); for (Method m : inputClass.getDeclaredMethods()) { ConstantMethodHandle constantMethodHandle = m.getAnnotation(ConstantMethodHandle.class); if (constantMethodHandle != null) { checkMethodToBeReplaced(m, ConstantMethodHandle.class, MethodHandle.class); constantMethodHandles.put(m.getName(), constantMethodHandle); continue; } ConstantMethodType constantMethodType = m.getAnnotation(ConstantMethodType.class); if (constantMethodType != null) { checkMethodToBeReplaced(m, ConstantMethodType.class, MethodType.class); constantMethodTypes.put(m.getName(), constantMethodType); continue; } } ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); try (InputStream is = Files.newInputStream(inputClassPath)) { ClassReader cr = new ClassReader(is); ConstantBuilder cb = new ConstantBuilder( Opcodes.ASM6, cw, constantMethodHandles, constantMethodTypes); cr.accept(cb, 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])); } }