1 // Copyright 2018 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.checkArgument; 17 import static com.google.common.base.Preconditions.checkNotNull; 18 import static com.google.common.base.Preconditions.checkState; 19 import static java.util.stream.Stream.concat; 20 21 import com.google.auto.value.AutoValue; 22 import com.google.common.base.Splitter; 23 import com.google.common.collect.ImmutableList; 24 import com.google.common.collect.ImmutableMap; 25 import com.google.common.collect.ImmutableSet; 26 import com.google.common.collect.LinkedHashMultimap; 27 import com.google.common.collect.Multimap; 28 import com.google.devtools.build.android.desugar.io.BitFlags; 29 import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; 30 import com.google.errorprone.annotations.Immutable; 31 import java.lang.reflect.Method; 32 import java.util.Collection; 33 import java.util.HashMap; 34 import java.util.Iterator; 35 import java.util.LinkedHashMap; 36 import java.util.LinkedHashSet; 37 import java.util.List; 38 import java.util.Objects; 39 import java.util.Set; 40 import javax.annotation.Nullable; 41 import org.objectweb.asm.ClassVisitor; 42 import org.objectweb.asm.Label; 43 import org.objectweb.asm.MethodVisitor; 44 import org.objectweb.asm.Opcodes; 45 import org.objectweb.asm.Type; 46 47 /** 48 * Helper that keeps track of which core library classes and methods we want to rewrite. 49 */ 50 class CoreLibrarySupport { 51 52 private static final Object[] EMPTY_FRAME = new Object[0]; 53 private static final String[] EMPTY_LIST = new String[0]; 54 55 private final CoreLibraryRewriter rewriter; 56 private final ClassLoader targetLoader; 57 /** Internal name prefixes that we want to move to a custom package. */ 58 private final ImmutableSet<String> renamedPrefixes; 59 private final ImmutableSet<String> excludeFromEmulation; 60 /** Internal names of interfaces whose default and static interface methods we'll emulate. */ 61 private final ImmutableSet<Class<?>> emulatedInterfaces; 62 /** Map from {@code owner#name} core library members to their new owners. */ 63 private final ImmutableMap<String, String> memberMoves; 64 65 /** For the collection of definitions of emulated default methods (deterministic iteration). */ 66 private final Multimap<String, EmulatedMethod> emulatedDefaultMethods = 67 LinkedHashMultimap.create(); 68 69 public CoreLibrarySupport( 70 CoreLibraryRewriter rewriter, 71 ClassLoader targetLoader, 72 List<String> renamedPrefixes, 73 List<String> emulatedInterfaces, 74 List<String> memberMoves, 75 List<String> excludeFromEmulation) { 76 this.rewriter = rewriter; 77 this.targetLoader = targetLoader; 78 checkArgument( 79 renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes); 80 this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes); 81 this.excludeFromEmulation = ImmutableSet.copyOf(excludeFromEmulation); 82 83 ImmutableSet.Builder<Class<?>> classBuilder = ImmutableSet.builder(); 84 for (String itf : emulatedInterfaces) { 85 checkArgument(itf.startsWith("java/util/"), itf); 86 Class<?> clazz = loadFromInternal(rewriter.getPrefix() + itf); 87 checkArgument(clazz.isInterface(), itf); 88 classBuilder.add(clazz); 89 } 90 this.emulatedInterfaces = classBuilder.build(); 91 92 // We can call isRenamed and rename below b/c we initialized the necessary fields above 93 // Use LinkedHashMap to tolerate identical duplicates 94 LinkedHashMap<String, String> movesBuilder = new LinkedHashMap<>(); 95 Splitter splitter = Splitter.on("->").trimResults().omitEmptyStrings(); 96 for (String move : memberMoves) { 97 List<String> pair = splitter.splitToList(move); 98 checkArgument(pair.size() == 2, "Doesn't split as expected: %s", move); 99 checkArgument(pair.get(0).startsWith("java/"), "Unexpected member: %s", move); 100 int sep = pair.get(0).indexOf('#'); 101 checkArgument(sep > 0 && sep == pair.get(0).lastIndexOf('#'), "invalid member: %s", move); 102 checkArgument(!isRenamedCoreLibrary(pair.get(0).substring(0, sep)), 103 "Original renamed, no need to move it: %s", move); 104 checkArgument(isRenamedCoreLibrary(pair.get(1)), "Target not renamed: %s", move); 105 checkArgument(!this.excludeFromEmulation.contains(pair.get(0)), 106 "Retargeted invocation %s shouldn't overlap with excluded", move); 107 108 String value = renameCoreLibrary(pair.get(1)); 109 String existing = movesBuilder.put(pair.get(0), value); 110 checkArgument(existing == null || existing.equals(value), 111 "Two move destinations %s and %s configured for %s", existing, value, pair.get(0)); 112 } 113 this.memberMoves = ImmutableMap.copyOf(movesBuilder); 114 } 115 116 public boolean isRenamedCoreLibrary(String internalName) { 117 String unprefixedName = rewriter.unprefix(internalName); 118 if (!unprefixedName.startsWith("java/") || renamedPrefixes.isEmpty()) { 119 return false; // shortcut 120 } 121 // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as 122 // configured prefixes 123 return looksGenerated(unprefixedName) 124 || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix)); 125 } 126 127 public String renameCoreLibrary(String internalName) { 128 internalName = rewriter.unprefix(internalName); 129 return (internalName.startsWith("java/")) 130 ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5) 131 : internalName; 132 } 133 134 @Nullable 135 public String getMoveTarget(String owner, String name) { 136 return memberMoves.get(rewriter.unprefix(owner) + '#' + name); 137 } 138 139 /** 140 * Returns {@code true} for java.* classes or interfaces that are subtypes of emulated interfaces. 141 * Note that implies that this method always returns {@code false} for user-written classes. 142 */ 143 public boolean isEmulatedCoreClassOrInterface(String internalName) { 144 return getEmulatedCoreClassOrInterface(internalName) != null; 145 } 146 147 /** Includes the given method definition in any applicable core interface emulation logic. */ 148 public void registerIfEmulatedCoreInterface( 149 int access, 150 String owner, 151 String name, 152 String desc, 153 String[] exceptions) { 154 Class<?> emulated = getEmulatedCoreClassOrInterface(owner); 155 if (emulated == null) { 156 return; 157 } 158 checkArgument(emulated.isInterface(), "Shouldn't be called for a class: %s.%s", owner, name); 159 checkArgument( 160 BitFlags.noneSet( 161 access, 162 Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE), 163 "Should only be called for default methods: %s.%s", owner, name); 164 emulatedDefaultMethods.put( 165 name + ":" + desc, EmulatedMethod.create(access, emulated, name, desc, exceptions)); 166 } 167 168 /** 169 * If the given invocation needs to go through a companion class of an emulated or renamed 170 * core interface, this methods returns that interface. This is a helper method for 171 * {@link CoreLibraryInvocationRewriter}. 172 * 173 * <p>This method can only return non-{@code null} if {@code owner} is a core library type. 174 * It usually returns an emulated interface, unless the given invocation is a super-call to a 175 * core class's implementation of an emulated method that's being moved (other implementations 176 * of emulated methods in core classes are ignored). In that case the class is returned and the 177 * caller can use {@link #getMoveTarget} to find out where to redirect the invokespecial to. 178 */ 179 // TODO(kmb): Rethink this API and consider combining it with getMoveTarget(). 180 @Nullable 181 public Class<?> getCoreInterfaceRewritingTarget( 182 int opcode, String owner, String name, String desc, boolean itf) { 183 if (looksGenerated(owner)) { 184 // Regular desugaring handles generated classes, no emulation is needed 185 return null; 186 } 187 if (!itf && opcode == Opcodes.INVOKESTATIC) { 188 // Ignore static invocations on classes--they never need rewriting (unless moved but that's 189 // handled separately). 190 return null; 191 } 192 if ("<init>".equals(name)) { 193 return null; // Constructors aren't rewritten 194 } 195 196 Class<?> clazz; 197 if (isRenamedCoreLibrary(owner)) { 198 // For renamed invocation targets we just need to do what InterfaceDesugaring does, that is, 199 // only worry about invokestatic and invokespecial interface invocations; nothing to do for 200 // classes and invokeinterface. InterfaceDesugaring ignores bootclasspath interfaces, 201 // so we have to do its work here for renamed interfaces. 202 if (itf 203 && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) { 204 clazz = loadFromInternal(owner); 205 } else { 206 return null; 207 } 208 } else { 209 // If not renamed, see if the owner needs emulation. 210 clazz = getEmulatedCoreClassOrInterface(owner); 211 if (clazz == null) { 212 return null; 213 } 214 } 215 checkArgument(itf == clazz.isInterface(), "%s expected to be interface: %s", owner, itf); 216 217 if (opcode == Opcodes.INVOKESTATIC) { 218 // Static interface invocation always goes to the given owner 219 checkState(itf); // we should've bailed out above. 220 return clazz; 221 } 222 223 // See if the invoked method is a default method, which will need rewriting. For invokespecial 224 // we can only get here if its a default method, and invokestatic we handled above. 225 Method callee = findInterfaceMethod(clazz, name, desc); 226 if (callee != null && callee.isDefault()) { 227 if (isExcluded(callee)) { 228 return null; 229 } 230 231 if (!itf && opcode == Opcodes.INVOKESPECIAL) { 232 // See if the invoked implementation is moved; note we ignore all other overrides in classes 233 Class<?> impl = clazz; // we know clazz is not an interface because !itf 234 while (impl != null) { 235 String implName = impl.getName().replace('.', '/'); 236 if (getMoveTarget(implName, name) != null) { 237 return impl; 238 } 239 impl = impl.getSuperclass(); 240 } 241 } 242 243 Class<?> result = callee.getDeclaringClass(); 244 if (isRenamedCoreLibrary(result.getName().replace('.', '/')) 245 || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) { 246 return result; 247 } 248 // We get here if the declaring class is a supertype of an emulated interface. In that case 249 // use the emulated interface instead (since we don't desugar the supertype). Fail in case 250 // there are multiple possibilities. 251 Iterator<Class<?>> roots = 252 emulatedInterfaces 253 .stream() 254 .filter( 255 emulated -> emulated.isAssignableFrom(clazz) && result.isAssignableFrom(emulated)) 256 .iterator(); 257 checkState(roots.hasNext()); // must exist 258 Class<?> substitute = roots.next(); 259 checkState(!roots.hasNext(), "Ambiguous emulation substitute: %s", callee); 260 return substitute; 261 } else { 262 checkArgument(!itf || opcode != Opcodes.INVOKESPECIAL, 263 "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc); 264 } 265 return null; 266 } 267 268 /** 269 * Returns the given class if it's a core library class or interface with emulated default 270 * methods. This is equivalent to calling {@link #isEmulatedCoreClassOrInterface} and then 271 * just loading the class (using the target class loader). 272 */ 273 public Class<?> getEmulatedCoreClassOrInterface(String internalName) { 274 if (looksGenerated(internalName)) { 275 // Regular desugaring handles generated classes, no emulation is needed 276 return null; 277 } 278 { 279 String unprefixedOwner = rewriter.unprefix(internalName); 280 if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) { 281 return null; 282 } 283 } 284 285 Class<?> clazz = loadFromInternal(internalName); 286 if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) { 287 return clazz; 288 } 289 return null; 290 } 291 292 public void makeDispatchHelpers(GeneratedClassStore store) { 293 HashMap<Class<?>, ClassVisitor> dispatchHelpers = new HashMap<>(); 294 for (Collection<EmulatedMethod> group : emulatedDefaultMethods.asMap().values()) { 295 checkState(!group.isEmpty()); 296 Class<?> root = group 297 .stream() 298 .map(EmulatedMethod::owner) 299 .max(DefaultMethodClassFixer.SubtypeComparator.INSTANCE) 300 .get(); 301 checkState(group.stream().map(m -> m.owner()).allMatch(o -> root.isAssignableFrom(o)), 302 "Not a single unique method: %s", group); 303 String methodName = group.stream().findAny().get().name(); 304 305 ImmutableList<Class<?>> customOverrides = findCustomOverrides(root, methodName); 306 307 for (EmulatedMethod methodDefinition : group) { 308 Class<?> owner = methodDefinition.owner(); 309 ClassVisitor dispatchHelper = dispatchHelpers.computeIfAbsent(owner, clazz -> { 310 String className = clazz.getName().replace('.', '/') + "$$Dispatch"; 311 ClassVisitor result = store.add(className); 312 result.visit( 313 Opcodes.V1_7, 314 // Must be public so dispatch methods can be called from anywhere 315 Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC, 316 className, 317 /*signature=*/ null, 318 "java/lang/Object", 319 EMPTY_LIST); 320 return result; 321 }); 322 323 // Types to check for before calling methodDefinition's companion, sub- before super-types 324 ImmutableList<Class<?>> typechecks = 325 concat(group.stream().map(EmulatedMethod::owner), customOverrides.stream()) 326 .filter(o -> o != owner && owner.isAssignableFrom(o)) 327 .distinct() // should already be but just in case 328 .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE) 329 .collect(ImmutableList.toImmutableList()); 330 makeDispatchHelperMethod(dispatchHelper, methodDefinition, typechecks); 331 } 332 } 333 } 334 335 private ImmutableList<Class<?>> findCustomOverrides(Class<?> root, String methodName) { 336 ImmutableList.Builder<Class<?>> customOverrides = ImmutableList.builder(); 337 for (ImmutableMap.Entry<String, String> move : memberMoves.entrySet()) { 338 // move.getKey is a string <owner>#<name> which we validated in the constructor. 339 // We need to take the string apart here to compare owner and name separately. 340 if (!methodName.equals(move.getKey().substring(move.getKey().indexOf('#') + 1))) { 341 continue; 342 } 343 Class<?> target = 344 loadFromInternal( 345 rewriter.getPrefix() + move.getKey().substring(0, move.getKey().indexOf('#'))); 346 if (!root.isAssignableFrom(target)) { 347 continue; 348 } 349 checkState(!target.isInterface(), "can't move emulated interface method: %s", move); 350 customOverrides.add(target); 351 } 352 return customOverrides.build(); 353 } 354 355 private void makeDispatchHelperMethod( 356 ClassVisitor helper, EmulatedMethod method, ImmutableList<Class<?>> typechecks) { 357 checkArgument(method.owner().isInterface()); 358 String owner = method.owner().getName().replace('.', '/'); 359 Type methodType = Type.getMethodType(method.descriptor()); 360 String companionDesc = 361 InterfaceDesugaring.companionDefaultMethodDescriptor(owner, method.descriptor()); 362 MethodVisitor dispatchMethod = 363 helper.visitMethod( 364 method.access() | Opcodes.ACC_STATIC, 365 method.name(), 366 companionDesc, 367 /*signature=*/ null, // signature is invalid due to extra "receiver" argument 368 method.exceptions().toArray(EMPTY_LIST)); 369 370 371 dispatchMethod.visitCode(); 372 { 373 // See if the receiver might come with its own implementation of the method, and call it. 374 // We do this by testing for the interface type created by EmulatedInterfaceRewriter 375 Label fallthrough = new Label(); 376 String emulationInterface = renameCoreLibrary(owner); 377 dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" 378 dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface); 379 dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough); 380 dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" 381 dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface); 382 383 visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */); 384 dispatchMethod.visitMethodInsn( 385 Opcodes.INVOKEINTERFACE, 386 emulationInterface, 387 method.name(), 388 method.descriptor(), 389 /*itf=*/ true); 390 dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN)); 391 392 dispatchMethod.visitLabel(fallthrough); 393 // Trivial frame for the branch target: same empty stack as before 394 dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME); 395 } 396 397 // Next, check for subtypes with specialized implementations and call them 398 for (Class<?> tested : typechecks) { 399 Label fallthrough = new Label(); 400 String testedName = tested.getName().replace('.', '/'); 401 // In case of a class this must be a member move; for interfaces use the companion. 402 String target = 403 tested.isInterface() 404 ? InterfaceDesugaring.getCompanionClassName(testedName) 405 : checkNotNull(memberMoves.get(rewriter.unprefix(testedName) + '#' + method.name())); 406 dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" 407 dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, testedName); 408 dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough); 409 dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" 410 dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, testedName); // make verifier happy 411 412 visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */); 413 dispatchMethod.visitMethodInsn( 414 Opcodes.INVOKESTATIC, 415 target, 416 method.name(), 417 InterfaceDesugaring.companionDefaultMethodDescriptor(testedName, method.descriptor()), 418 /*itf=*/ false); 419 dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN)); 420 421 dispatchMethod.visitLabel(fallthrough); 422 // Trivial frame for the branch target: same empty stack as before 423 dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME); 424 } 425 426 // Call static type's default implementation in companion class 427 dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" 428 visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */); 429 dispatchMethod.visitMethodInsn( 430 Opcodes.INVOKESTATIC, 431 InterfaceDesugaring.getCompanionClassName(owner), 432 method.name(), 433 companionDesc, 434 /*itf=*/ false); 435 dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN)); 436 437 dispatchMethod.visitMaxs(0, 0); 438 dispatchMethod.visitEnd(); 439 } 440 441 private boolean isExcluded(Method method) { 442 String unprefixedOwner = 443 rewriter.unprefix(method.getDeclaringClass().getName().replace('.', '/')); 444 return excludeFromEmulation.contains(unprefixedOwner + "#" + method.getName()); 445 } 446 447 private Class<?> loadFromInternal(String internalName) { 448 try { 449 return targetLoader.loadClass(internalName.replace('/', '.')); 450 } catch (ClassNotFoundException e) { 451 throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e); 452 } 453 } 454 455 private static Method findInterfaceMethod(Class<?> clazz, String name, String desc) { 456 return collectImplementedInterfaces(clazz, new LinkedHashSet<>()) 457 .stream() 458 // search more subtypes before supertypes 459 .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE) 460 .map(itf -> findMethod(itf, name, desc)) 461 .filter(Objects::nonNull) 462 .findFirst() 463 .orElse((Method) null); 464 } 465 466 private static Method findMethod(Class<?> clazz, String name, String desc) { 467 for (Method m : clazz.getMethods()) { 468 if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) { 469 return m; 470 } 471 } 472 return null; 473 } 474 475 private static Set<Class<?>> collectImplementedInterfaces(Class<?> clazz, Set<Class<?>> dest) { 476 if (clazz.isInterface()) { 477 if (!dest.add(clazz)) { 478 return dest; 479 } 480 } else if (clazz.getSuperclass() != null) { 481 collectImplementedInterfaces(clazz.getSuperclass(), dest); 482 } 483 484 for (Class<?> itf : clazz.getInterfaces()) { 485 collectImplementedInterfaces(itf, dest); 486 } 487 return dest; 488 } 489 490 /** 491 * Emits instructions to load a method's parameters as arguments of a method call assumed to have 492 * compatible descriptor, starting at the given local variable slot. 493 */ 494 private static void visitLoadArgs(MethodVisitor dispatchMethod, Type neededType, int slot) { 495 for (Type arg : neededType.getArgumentTypes()) { 496 dispatchMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot); 497 slot += arg.getSize(); 498 } 499 } 500 501 /** Checks whether the given class is (likely) generated by desugar itself. */ 502 private static boolean looksGenerated(String owner) { 503 return owner.contains("$$Lambda$") || owner.endsWith("$$CC") || owner.endsWith("$$Dispatch"); 504 } 505 506 @AutoValue 507 @Immutable 508 abstract static class EmulatedMethod { 509 public static EmulatedMethod create( 510 int access, Class<?> owner, String name, String desc, @Nullable String[] exceptions) { 511 return new AutoValue_CoreLibrarySupport_EmulatedMethod(access, owner, name, desc, 512 exceptions != null ? ImmutableList.copyOf(exceptions) : ImmutableList.of()); 513 } 514 515 abstract int access(); 516 abstract Class<?> owner(); 517 abstract String name(); 518 abstract String descriptor(); 519 abstract ImmutableList<String> exceptions(); 520 } 521 } 522