/* * Copyright (C) 2018 The Flogger Authors. * * 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 com.google.common.flogger.backend; import static org.objectweb.asm.Opcodes.ACC_FINAL; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_STATIC; import static org.objectweb.asm.Opcodes.ACC_SUPER; import static org.objectweb.asm.Opcodes.ACONST_NULL; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ANEWARRAY; import static org.objectweb.asm.Opcodes.ARETURN; import static org.objectweb.asm.Opcodes.ASTORE; import static org.objectweb.asm.Opcodes.CHECKCAST; import static org.objectweb.asm.Opcodes.F_SAME1; import static org.objectweb.asm.Opcodes.ICONST_0; import static org.objectweb.asm.Opcodes.INVOKESPECIAL; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.RETURN; import static org.objectweb.asm.Opcodes.V1_6; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; /** * Class that generates a PlatformProvider class for creating instances of Platform, for use in * Platform discovery. * * <p>This generator is necessary in order to create a class that explicitly references the actual * platform implementation classes without having them visible as a build dependency. If the code * were compiled with javac that wouldn't work, because javac needs to observe the classes on its * classpath. By resorting to manually generating the class file, we can work around the limitation, * and avoid the dependency on platform implementations while still keeping an explicit reference to * the classes. The advantage of this approach is that tools that operate on bytecode (e.g. * proguard) observe the dependency correctly, which is not the case when reflection is used to look * up classes. */ public final class PlatformProviderGenerator { private static final String[] PLATFORM_CLASSES = new String[] { "Lcom/google/common/flogger/backend/system/DefaultPlatform;", }; public static void main(String[] args) throws IOException { // Create the class. ClassWriter classWriter = new ClassWriter(0); classWriter.visit( V1_6, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, "com/google/common/flogger/backend/PlatformProvider", null, "java/lang/Object", null); // Create the no-op constructor. MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null); methodVisitor.visitCode(); methodVisitor.visitVarInsn(ALOAD, 0); methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); methodVisitor.visitInsn(RETURN); methodVisitor.visitMaxs(1, 1); methodVisitor.visitEnd(); // Create the static getter method. methodVisitor = classWriter.visitMethod( ACC_PUBLIC + ACC_STATIC, "getPlatform", "()Lcom/google/common/flogger/backend/Platform;", null, null); // Try the different platforms. for (String platformClass : PLATFORM_CLASSES) { tryBlockForPlatform(methodVisitor, platformClass); } // Return null if no platform is found. methodVisitor.visitInsn(ACONST_NULL); methodVisitor.visitInsn(ARETURN); methodVisitor.visitMaxs(2, 1); methodVisitor.visitEnd(); // Finish creating the class. classWriter.visitEnd(); // Write the class to the output file. Path path = Paths.get(args[0]); Files.createDirectories(path.getParent()); try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(path, StandardOpenOption.CREATE_NEW))) { ZipEntry entry = new ZipEntry("com/google/common/flogger/backend/PlatformProvider.class"); entry.setTime(0); // clear timestamp to ensure JAR is deterministic for cache during builds. jar.putNextEntry(entry); jar.write(classWriter.toByteArray()); jar.closeEntry(); } } private static void tryBlockForPlatform(MethodVisitor methodVisitor, String platformType) { methodVisitor.visitCode(); // Generate the enveloping try/catch block: // // try { // ... // } catch (NoClassDefFoundError | IllegalAccessException | InstantiationException // | InvocationTargetException | NoSuchMethodException e) { // ... // } // // Note that the exception types need to be listed explicitly (rather than using // java.lang.ReflectiveOperationException) because that parent exception type isn't available // on Android until API level 19. Label startLabel = new Label(); Label endLabel = new Label(); Label handlerLabel = new Label(); methodVisitor.visitTryCatchBlock( startLabel, endLabel, handlerLabel, "java/lang/NoClassDefFoundError"); methodVisitor.visitTryCatchBlock( startLabel, endLabel, handlerLabel, "java/lang/IllegalAccessException"); methodVisitor.visitTryCatchBlock( startLabel, endLabel, handlerLabel, "java/lang/InstantiationException"); methodVisitor.visitTryCatchBlock( startLabel, endLabel, handlerLabel, "java/lang/reflect/InvocationTargetException"); methodVisitor.visitTryCatchBlock( startLabel, endLabel, handlerLabel, "java/lang/NoSuchMethodException"); methodVisitor.visitLabel(startLabel); // Generate the actual reflective constructor call inside the try block: // // return (Platform) PlatformClass.class.getDeclaredConstructor().newInstance(); // // Note that the constructor call happens reflectively to make sure that the platform class // isn't loaded until actually executing this instruction. That is important because an // earlier class load could happen outside of the try/catch block where we are explicitly // handling the case of the class not being present. methodVisitor.visitLdcInsn(Type.getType(platformType)); methodVisitor.visitInsn(ICONST_0); methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/Class"); methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/Class", "getDeclaredConstructor", "([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;", false); methodVisitor.visitInsn(ICONST_0); methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/Object"); methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/reflect/Constructor", "newInstance", "([Ljava/lang/Object;)Ljava/lang/Object;", false); methodVisitor.visitTypeInsn(CHECKCAST, "com/google/common/flogger/backend/Platform"); methodVisitor.visitLabel(endLabel); methodVisitor.visitInsn(ARETURN); // Generate the catch block of the overall try/catch. The catch block is actually just empty, // but Java does require the catch handler to have at least a frame in it to declare the // exception variable that is available within the catch block scope: // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.12 methodVisitor.visitLabel(handlerLabel); methodVisitor.visitFrame(F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"}); methodVisitor.visitVarInsn(ASTORE, 0); } }