1 // Copyright 2017 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package com.google.devtools.build.android.desugar; 15 16 import static com.google.common.base.Preconditions.checkNotNull; 17 import static com.google.common.base.Preconditions.checkState; 18 import static org.objectweb.asm.Opcodes.ACC_STATIC; 19 import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; 20 import static org.objectweb.asm.Opcodes.ASM6; 21 import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; 22 import static org.objectweb.asm.Opcodes.INVOKESTATIC; 23 import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; 24 25 import com.google.common.base.Function; 26 import com.google.common.base.Preconditions; 27 import com.google.common.collect.FluentIterable; 28 import com.google.common.collect.ImmutableMap; 29 import com.google.common.collect.ImmutableMultimap; 30 import com.google.common.collect.ImmutableSet; 31 import com.google.devtools.build.android.desugar.BytecodeTypeInference.InferredType; 32 import com.google.devtools.build.android.desugar.io.BitFlags; 33 import java.util.Collections; 34 import java.util.LinkedHashSet; 35 import java.util.Optional; 36 import java.util.Set; 37 import java.util.concurrent.atomic.AtomicInteger; 38 import javax.annotation.Nullable; 39 import org.objectweb.asm.ClassVisitor; 40 import org.objectweb.asm.Label; 41 import org.objectweb.asm.MethodVisitor; 42 import org.objectweb.asm.commons.ClassRemapper; 43 import org.objectweb.asm.commons.Remapper; 44 import org.objectweb.asm.tree.MethodNode; 45 46 /** 47 * Desugar try-with-resources. This class visitor intercepts calls to the following methods, and 48 * redirect them to ThrowableExtension. 49 * <li>{@code Throwable.addSuppressed(Throwable)} 50 * <li>{@code Throwable.getSuppressed()} 51 * <li>{@code Throwable.printStackTrace()} 52 * <li>{@code Throwable.printStackTrace(PrintStream)} 53 * <li>{@code Throwable.printStackTrace(PringWriter)} 54 */ 55 public class TryWithResourcesRewriter extends ClassVisitor { 56 57 private static final String RUNTIME_PACKAGE_INTERNAL_NAME = 58 "com/google/devtools/build/android/desugar/runtime"; 59 60 static final String THROWABLE_EXTENSION_INTERNAL_NAME = 61 RUNTIME_PACKAGE_INTERNAL_NAME + '/' + "ThrowableExtension"; 62 63 /** The extension classes for java.lang.Throwable. */ 64 static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES = 65 ImmutableSet.of( 66 THROWABLE_EXTENSION_INTERNAL_NAME, 67 THROWABLE_EXTENSION_INTERNAL_NAME + "$AbstractDesugaringStrategy", 68 THROWABLE_EXTENSION_INTERNAL_NAME + "$ConcurrentWeakIdentityHashMap", 69 THROWABLE_EXTENSION_INTERNAL_NAME + "$ConcurrentWeakIdentityHashMap$WeakKey", 70 THROWABLE_EXTENSION_INTERNAL_NAME + "$MimicDesugaringStrategy", 71 THROWABLE_EXTENSION_INTERNAL_NAME + "$NullDesugaringStrategy", 72 THROWABLE_EXTENSION_INTERNAL_NAME + "$ReuseDesugaringStrategy"); 73 74 /** The extension classes for java.lang.Throwable. All the names end with ".class" */ 75 static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT = 76 FluentIterable.from(THROWABLE_EXT_CLASS_INTERNAL_NAMES) 77 .transform( 78 new Function<String, String>() { 79 @Override 80 public String apply(String s) { 81 return s + ".class"; 82 } 83 }) 84 .toSet(); 85 86 static final ImmutableMultimap<String, String> TARGET_METHODS = 87 ImmutableMultimap.<String, String>builder() 88 .put("addSuppressed", "(Ljava/lang/Throwable;)V") 89 .put("getSuppressed", "()[Ljava/lang/Throwable;") 90 .put("printStackTrace", "()V") 91 .put("printStackTrace", "(Ljava/io/PrintStream;)V") 92 .put("printStackTrace", "(Ljava/io/PrintWriter;)V") 93 .build(); 94 95 static final ImmutableMap<String, String> METHOD_DESC_MAP = 96 ImmutableMap.<String, String>builder() 97 .put("(Ljava/lang/Throwable;)V", "(Ljava/lang/Throwable;Ljava/lang/Throwable;)V") 98 .put("()[Ljava/lang/Throwable;", "(Ljava/lang/Throwable;)[Ljava/lang/Throwable;") 99 .put("()V", "(Ljava/lang/Throwable;)V") 100 .put("(Ljava/io/PrintStream;)V", "(Ljava/lang/Throwable;Ljava/io/PrintStream;)V") 101 .put("(Ljava/io/PrintWriter;)V", "(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V") 102 .build(); 103 104 static final String CLOSE_RESOURCE_METHOD_NAME = "$closeResource"; 105 static final String CLOSE_RESOURCE_METHOD_DESC = 106 "(Ljava/lang/Throwable;Ljava/lang/AutoCloseable;)V"; 107 108 private final ClassLoader classLoader; 109 private final Set<String> visitedExceptionTypes; 110 private final AtomicInteger numOfTryWithResourcesInvoked; 111 /** Stores the internal class names of resources that need to be closed. */ 112 private final LinkedHashSet<String> resourceTypeInternalNames = new LinkedHashSet<>(); 113 114 private final boolean hasCloseResourceMethod; 115 116 private String internalName; 117 /** 118 * Indicate whether the current class being desugared should be ignored. If the current class is 119 * one of the runtime extension classes, then it should be ignored. 120 */ 121 private boolean shouldCurrentClassBeIgnored; 122 /** 123 * A method node for $closeResource(Throwable, AutoCloseable). At then end, we specialize this 124 * method node. 125 */ 126 @Nullable private MethodNode closeResourceMethod; 127 TryWithResourcesRewriter( ClassVisitor classVisitor, ClassLoader classLoader, Set<String> visitedExceptionTypes, AtomicInteger numOfTryWithResourcesInvoked, boolean hasCloseResourceMethod)128 public TryWithResourcesRewriter( 129 ClassVisitor classVisitor, 130 ClassLoader classLoader, 131 Set<String> visitedExceptionTypes, 132 AtomicInteger numOfTryWithResourcesInvoked, 133 boolean hasCloseResourceMethod) { 134 super(ASM6, classVisitor); 135 this.classLoader = classLoader; 136 this.visitedExceptionTypes = visitedExceptionTypes; 137 this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked; 138 this.hasCloseResourceMethod = hasCloseResourceMethod; 139 } 140 141 @Override visit( int version, int access, String name, String signature, String superName, String[] interfaces)142 public void visit( 143 int version, 144 int access, 145 String name, 146 String signature, 147 String superName, 148 String[] interfaces) { 149 super.visit(version, access, name, signature, superName, interfaces); 150 internalName = name; 151 shouldCurrentClassBeIgnored = THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name); 152 Preconditions.checkState( 153 !shouldCurrentClassBeIgnored || !hasCloseResourceMethod, 154 "The current class which will be ignored " 155 + "contains $closeResource(Throwable, AutoCloseable)."); 156 } 157 158 @Override visitEnd()159 public void visitEnd() { 160 if (!resourceTypeInternalNames.isEmpty()) { 161 checkNotNull(closeResourceMethod); 162 for (String resourceInternalName : resourceTypeInternalNames) { 163 boolean isInterface = isInterface(resourceInternalName.replace('/', '.')); 164 // We use "this" to desugar the body of the close resource method. 165 closeResourceMethod.accept( 166 new CloseResourceMethodSpecializer(cv, resourceInternalName, isInterface)); 167 } 168 } else { 169 // It is possible that all calls to $closeResources(...) are in dead code regions, and the 170 // calls are eliminated, which leaving the method $closeResources() unused. (b/78030676). 171 // In this case, we just discard the method body. 172 checkState( 173 !hasCloseResourceMethod || closeResourceMethod != null, 174 "There should be $closeResources(...) in the class file."); 175 } 176 super.visitEnd(); 177 } 178 179 @Override visitMethod( int access, String name, String desc, String signature, String[] exceptions)180 public MethodVisitor visitMethod( 181 int access, String name, String desc, String signature, String[] exceptions) { 182 if (exceptions != null && exceptions.length > 0) { 183 // collect exception types. 184 Collections.addAll(visitedExceptionTypes, exceptions); 185 } 186 if (isSyntheticCloseResourceMethod(access, name, desc)) { 187 checkState(closeResourceMethod == null, "The TWR rewriter has been used."); 188 closeResourceMethod = new MethodNode(ASM6, access, name, desc, signature, exceptions); 189 // Run the TWR desugar pass over the $closeResource(Throwable, AutoCloseable) first, for 190 // example, to rewrite calls to AutoCloseable.close().. 191 TryWithResourceVisitor twrVisitor = 192 new TryWithResourceVisitor( 193 internalName, name + desc, closeResourceMethod, classLoader, null); 194 return twrVisitor; 195 } 196 197 MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions); 198 if (visitor == null || shouldCurrentClassBeIgnored) { 199 return visitor; 200 } 201 202 BytecodeTypeInference inference = null; 203 if (hasCloseResourceMethod) { 204 /* 205 * BytecodeTypeInference will run after the TryWithResourceVisitor, because when we are 206 * processing a bytecode instruction, we need to know the types in the operand stack, which 207 * are inferred after the previous instruction. 208 */ 209 inference = new BytecodeTypeInference(access, internalName, name, desc); 210 inference.setDelegateMethodVisitor(visitor); 211 visitor = inference; 212 } 213 214 TryWithResourceVisitor twrVisitor = 215 new TryWithResourceVisitor(internalName, name + desc, visitor, classLoader, inference); 216 return twrVisitor; 217 } 218 isSyntheticCloseResourceMethod(int access, String name, String desc)219 public static boolean isSyntheticCloseResourceMethod(int access, String name, String desc) { 220 return BitFlags.isSet(access, ACC_SYNTHETIC | ACC_STATIC) 221 && CLOSE_RESOURCE_METHOD_NAME.equals(name) 222 && CLOSE_RESOURCE_METHOD_DESC.equals(desc); 223 } 224 isInterface(String className)225 private boolean isInterface(String className) { 226 try { 227 Class<?> klass = classLoader.loadClass(className); 228 return klass.isInterface(); 229 } catch (ClassNotFoundException e) { 230 throw new AssertionError("Failed to load class when desugaring class " + internalName); 231 } 232 } 233 isCallToSyntheticCloseResource( String currentClassInternalName, int opcode, String owner, String name, String desc)234 public static boolean isCallToSyntheticCloseResource( 235 String currentClassInternalName, int opcode, String owner, String name, String desc) { 236 if (opcode != INVOKESTATIC) { 237 return false; 238 } 239 if (!currentClassInternalName.equals(owner)) { 240 return false; 241 } 242 if (!CLOSE_RESOURCE_METHOD_NAME.equals(name)) { 243 return false; 244 } 245 if (!CLOSE_RESOURCE_METHOD_DESC.equals(desc)) { 246 return false; 247 } 248 return true; 249 } 250 251 private class TryWithResourceVisitor extends MethodVisitor { 252 253 private final ClassLoader classLoader; 254 /** For debugging purpose. Enrich exception information. */ 255 private final String internalName; 256 257 private final String methodSignature; 258 @Nullable private final BytecodeTypeInference typeInference; 259 TryWithResourceVisitor( String internalName, String methodSignature, MethodVisitor methodVisitor, ClassLoader classLoader, @Nullable BytecodeTypeInference typeInference)260 public TryWithResourceVisitor( 261 String internalName, 262 String methodSignature, 263 MethodVisitor methodVisitor, 264 ClassLoader classLoader, 265 @Nullable BytecodeTypeInference typeInference) { 266 super(ASM6, methodVisitor); 267 this.classLoader = classLoader; 268 this.internalName = internalName; 269 this.methodSignature = methodSignature; 270 this.typeInference = typeInference; 271 } 272 273 @Override visitTryCatchBlock(Label start, Label end, Label handler, String type)274 public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { 275 if (type != null) { 276 visitedExceptionTypes.add(type); // type in a try-catch block must extend Throwable. 277 } 278 super.visitTryCatchBlock(start, end, handler, type); 279 } 280 281 @Override visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)282 public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { 283 if (isCallToSyntheticCloseResource(internalName, opcode, owner, name, desc)) { 284 checkNotNull( 285 typeInference, 286 "This method %s.%s has a call to $closeResource(Throwable, AutoCloseable) method, " 287 + "but the type inference is null.", 288 internalName, 289 methodSignature); 290 { 291 // Check the exception type. 292 InferredType exceptionClass = typeInference.getTypeOfOperandFromTop(1); 293 if (!exceptionClass.isNull()) { 294 Optional<String> exceptionClassInternalName = exceptionClass.getInternalName(); 295 checkState( 296 exceptionClassInternalName.isPresent(), 297 "The exception %s is not a reference type in %s.%s", 298 exceptionClass, 299 internalName, 300 methodSignature); 301 checkState( 302 isAssignableFrom( 303 "java.lang.Throwable", exceptionClassInternalName.get().replace('/', '.')), 304 "The exception type %s in %s.%s should be a subclass of java.lang.Throwable.", 305 exceptionClassInternalName, 306 internalName, 307 methodSignature); 308 } 309 } 310 311 InferredType resourceType = typeInference.getTypeOfOperandFromTop(0); 312 Optional<String> resourceClassInternalName = resourceType.getInternalName(); 313 { 314 // Check the resource type. 315 checkState( 316 resourceClassInternalName.isPresent(), 317 "The resource class %s is not a reference type in %s.%s", 318 resourceType, 319 internalName, 320 methodSignature); 321 String resourceClassName = resourceClassInternalName.get().replace('/', '.'); 322 checkState( 323 hasCloseMethod(resourceClassName), 324 "The resource class %s should have a close() method.", 325 resourceClassName); 326 } 327 resourceTypeInternalNames.add(resourceClassInternalName.get()); 328 super.visitMethodInsn( 329 opcode, 330 owner, 331 "$closeResource", 332 "(Ljava/lang/Throwable;L" + resourceClassInternalName.get() + ";)V", 333 itf); 334 return; 335 } 336 337 if (!isMethodCallTargeted(opcode, owner, name, desc)) { 338 super.visitMethodInsn(opcode, owner, name, desc, itf); 339 return; 340 } 341 numOfTryWithResourcesInvoked.incrementAndGet(); 342 visitedExceptionTypes.add(checkNotNull(owner)); // owner extends Throwable. 343 super.visitMethodInsn( 344 INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false); 345 } 346 isMethodCallTargeted(int opcode, String owner, String name, String desc)347 private boolean isMethodCallTargeted(int opcode, String owner, String name, String desc) { 348 if (opcode != INVOKEVIRTUAL) { 349 return false; 350 } 351 if (!TARGET_METHODS.containsEntry(name, desc)) { 352 return false; 353 } 354 if (visitedExceptionTypes.contains(owner)) { 355 return true; // The owner is an exception that has been visited before. 356 } 357 return isAssignableFrom("java.lang.Throwable", owner.replace('/', '.')); 358 } 359 hasCloseMethod(String resourceClassName)360 private boolean hasCloseMethod(String resourceClassName) { 361 try { 362 Class<?> klass = classLoader.loadClass(resourceClassName); 363 klass.getMethod("close"); 364 return true; 365 } catch (ClassNotFoundException e) { 366 throw new AssertionError( 367 "Failed to load class " 368 + resourceClassName 369 + " when desugaring method " 370 + internalName 371 + "." 372 + methodSignature, 373 e); 374 } catch (NoSuchMethodException e) { 375 // There is no close() method in the class, so return false. 376 return false; 377 } 378 } 379 isAssignableFrom(String baseClassName, String subClassName)380 private boolean isAssignableFrom(String baseClassName, String subClassName) { 381 try { 382 Class<?> baseClass = classLoader.loadClass(baseClassName); 383 Class<?> subClass = classLoader.loadClass(subClassName); 384 return baseClass.isAssignableFrom(subClass); 385 } catch (ClassNotFoundException e) { 386 throw new AssertionError( 387 "Failed to load class when desugaring method " 388 + internalName 389 + "." 390 + methodSignature 391 + " when checking the assignable relation for class " 392 + baseClassName 393 + " and " 394 + subClassName, 395 e); 396 } 397 } 398 } 399 400 /** 401 * A class to specialize the method $closeResource(Throwable, AutoCloseable), which does 402 * 403 * <ul> 404 * <li>Rename AutoCloseable to the given concrete resource type. 405 * <li>Adjust the invoke instruction that calls AutoCloseable.close() 406 * </ul> 407 */ 408 private static class CloseResourceMethodSpecializer extends ClassRemapper { 409 410 private final boolean isResourceAnInterface; 411 private final String targetResourceInternalName; 412 CloseResourceMethodSpecializer( ClassVisitor cv, String targetResourceInternalName, boolean isResourceAnInterface)413 public CloseResourceMethodSpecializer( 414 ClassVisitor cv, String targetResourceInternalName, boolean isResourceAnInterface) { 415 super( 416 cv, 417 new Remapper() { 418 @Override 419 public String map(String typeName) { 420 if (typeName.equals("java/lang/AutoCloseable")) { 421 return targetResourceInternalName; 422 } else { 423 return typeName; 424 } 425 } 426 }); 427 this.targetResourceInternalName = targetResourceInternalName; 428 this.isResourceAnInterface = isResourceAnInterface; 429 } 430 431 @Override visitMethod( int access, String name, String desc, String signature, String[] exceptions)432 public MethodVisitor visitMethod( 433 int access, String name, String desc, String signature, String[] exceptions) { 434 MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); 435 return new MethodVisitor(ASM6, mv) { 436 @Override 437 public void visitMethodInsn( 438 int opcode, String owner, String name, String desc, boolean itf) { 439 if (opcode == INVOKEINTERFACE 440 && owner.endsWith("java/lang/AutoCloseable") 441 && name.equals("close") 442 && desc.equals("()V") 443 && itf) { 444 opcode = isResourceAnInterface ? INVOKEINTERFACE : INVOKEVIRTUAL; 445 owner = targetResourceInternalName; 446 itf = isResourceAnInterface; 447 } 448 super.visitMethodInsn(opcode, owner, name, desc, itf); 449 } 450 }; 451 } 452 } 453 } 454