Aurimas Liutikas | 88c7ff1 | 2023-08-10 12:42:26 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. |
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | * |
| 5 | * This code is free software; you can redistribute it and/or modify it |
| 6 | * under the terms of the GNU General Public License version 2 only, as |
| 7 | * published by the Free Software Foundation. Oracle designates this |
| 8 | * particular file as subject to the "Classpath" exception as provided |
| 9 | * by Oracle in the LICENSE file that accompanied this code. |
| 10 | * |
| 11 | * This code is distributed in the hope that it will be useful, but WITHOUT |
| 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | * version 2 for more details (a copy is included in the LICENSE file that |
| 15 | * accompanied this code). |
| 16 | * |
| 17 | * You should have received a copy of the GNU General Public License version |
| 18 | * 2 along with this work; if not, write to the Free Software Foundation, |
| 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | * |
| 21 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| 22 | * or visit www.oracle.com if you need additional information or have any |
| 23 | * questions. |
| 24 | */ |
| 25 | |
| 26 | package java.lang.runtime; |
| 27 | |
| 28 | import java.lang.invoke.ConstantCallSite; |
| 29 | import java.lang.invoke.MethodHandle; |
| 30 | import java.lang.invoke.MethodHandles; |
| 31 | import java.lang.invoke.MethodType; |
| 32 | import java.lang.invoke.TypeDescriptor; |
| 33 | import java.security.AccessController; |
| 34 | import java.security.PrivilegedAction; |
| 35 | import java.util.Arrays; |
| 36 | import java.util.HashMap; |
| 37 | import java.util.List; |
| 38 | import java.util.Objects; |
| 39 | |
| 40 | /** |
| 41 | * Bootstrap methods for state-driven implementations of core methods, |
| 42 | * including {@link Object#equals(Object)}, {@link Object#hashCode()}, and |
| 43 | * {@link Object#toString()}. These methods may be used, for example, by |
| 44 | * Java compiler implementations to implement the bodies of {@link Object} |
| 45 | * methods for record classes. |
| 46 | * |
| 47 | * @since 16 |
| 48 | */ |
| 49 | public class ObjectMethods { |
| 50 | |
| 51 | private ObjectMethods() { } |
| 52 | |
| 53 | private static final MethodType DESCRIPTOR_MT = MethodType.methodType(MethodType.class); |
| 54 | private static final MethodType NAMES_MT = MethodType.methodType(List.class); |
| 55 | private static final MethodHandle FALSE = MethodHandles.constant(boolean.class, false); |
| 56 | private static final MethodHandle TRUE = MethodHandles.constant(boolean.class, true); |
| 57 | private static final MethodHandle ZERO = MethodHandles.constant(int.class, 0); |
| 58 | private static final MethodHandle CLASS_IS_INSTANCE; |
| 59 | private static final MethodHandle OBJECT_EQUALS; |
| 60 | private static final MethodHandle OBJECTS_EQUALS; |
| 61 | private static final MethodHandle OBJECTS_HASHCODE; |
| 62 | private static final MethodHandle OBJECTS_TOSTRING; |
| 63 | private static final MethodHandle OBJECT_EQ; |
| 64 | private static final MethodHandle OBJECT_HASHCODE; |
| 65 | private static final MethodHandle OBJECT_TO_STRING; |
| 66 | private static final MethodHandle STRING_FORMAT; |
| 67 | private static final MethodHandle HASH_COMBINER; |
| 68 | |
| 69 | private static final HashMap<Class<?>, MethodHandle> primitiveEquals = new HashMap<>(); |
| 70 | private static final HashMap<Class<?>, MethodHandle> primitiveHashers = new HashMap<>(); |
| 71 | private static final HashMap<Class<?>, MethodHandle> primitiveToString = new HashMap<>(); |
| 72 | |
| 73 | static { |
| 74 | try { |
| 75 | Class<ObjectMethods> OBJECT_METHODS_CLASS = ObjectMethods.class; |
| 76 | MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); |
| 77 | MethodHandles.Lookup lookup = MethodHandles.lookup(); |
| 78 | |
| 79 | // Android-changed: Use the "normal" class loader |
| 80 | /* |
| 81 | @SuppressWarnings("removal") |
| 82 | ClassLoader loader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { |
| 83 | @Override public ClassLoader run() { return ClassLoader.getPlatformClassLoader(); } |
| 84 | }); |
| 85 | */ |
| 86 | ClassLoader loader = ObjectMethods.class.getClassLoader(); |
| 87 | |
| 88 | CLASS_IS_INSTANCE = publicLookup.findVirtual(Class.class, "isInstance", |
| 89 | MethodType.methodType(boolean.class, Object.class)); |
| 90 | OBJECT_EQUALS = publicLookup.findVirtual(Object.class, "equals", |
| 91 | MethodType.methodType(boolean.class, Object.class)); |
| 92 | OBJECT_HASHCODE = publicLookup.findVirtual(Object.class, "hashCode", |
| 93 | MethodType.fromMethodDescriptorString("()I", loader)); |
| 94 | OBJECT_TO_STRING = publicLookup.findVirtual(Object.class, "toString", |
| 95 | MethodType.methodType(String.class)); |
| 96 | STRING_FORMAT = publicLookup.findStatic(String.class, "format", |
| 97 | MethodType.methodType(String.class, String.class, Object[].class)); |
| 98 | OBJECTS_EQUALS = publicLookup.findStatic(Objects.class, "equals", |
| 99 | MethodType.methodType(boolean.class, Object.class, Object.class)); |
| 100 | OBJECTS_HASHCODE = publicLookup.findStatic(Objects.class, "hashCode", |
| 101 | MethodType.methodType(int.class, Object.class)); |
| 102 | OBJECTS_TOSTRING = publicLookup.findStatic(Objects.class, "toString", |
| 103 | MethodType.methodType(String.class, Object.class)); |
| 104 | |
| 105 | OBJECT_EQ = lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 106 | MethodType.methodType(boolean.class, Object.class, Object.class)); |
| 107 | HASH_COMBINER = lookup.findStatic(OBJECT_METHODS_CLASS, "hashCombiner", |
| 108 | MethodType.fromMethodDescriptorString("(II)I", loader)); |
| 109 | |
| 110 | primitiveEquals.put(byte.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 111 | MethodType.fromMethodDescriptorString("(BB)Z", loader))); |
| 112 | primitiveEquals.put(short.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 113 | MethodType.fromMethodDescriptorString("(SS)Z", loader))); |
| 114 | primitiveEquals.put(char.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 115 | MethodType.fromMethodDescriptorString("(CC)Z", loader))); |
| 116 | primitiveEquals.put(int.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 117 | MethodType.fromMethodDescriptorString("(II)Z", loader))); |
| 118 | primitiveEquals.put(long.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 119 | MethodType.fromMethodDescriptorString("(JJ)Z", loader))); |
| 120 | primitiveEquals.put(float.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 121 | MethodType.fromMethodDescriptorString("(FF)Z", loader))); |
| 122 | primitiveEquals.put(double.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 123 | MethodType.fromMethodDescriptorString("(DD)Z", loader))); |
| 124 | primitiveEquals.put(boolean.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq", |
| 125 | MethodType.fromMethodDescriptorString("(ZZ)Z", loader))); |
| 126 | |
| 127 | primitiveHashers.put(byte.class, lookup.findStatic(Byte.class, "hashCode", |
| 128 | MethodType.fromMethodDescriptorString("(B)I", loader))); |
| 129 | primitiveHashers.put(short.class, lookup.findStatic(Short.class, "hashCode", |
| 130 | MethodType.fromMethodDescriptorString("(S)I", loader))); |
| 131 | primitiveHashers.put(char.class, lookup.findStatic(Character.class, "hashCode", |
| 132 | MethodType.fromMethodDescriptorString("(C)I", loader))); |
| 133 | primitiveHashers.put(int.class, lookup.findStatic(Integer.class, "hashCode", |
| 134 | MethodType.fromMethodDescriptorString("(I)I", loader))); |
| 135 | primitiveHashers.put(long.class, lookup.findStatic(Long.class, "hashCode", |
| 136 | MethodType.fromMethodDescriptorString("(J)I", loader))); |
| 137 | primitiveHashers.put(float.class, lookup.findStatic(Float.class, "hashCode", |
| 138 | MethodType.fromMethodDescriptorString("(F)I", loader))); |
| 139 | primitiveHashers.put(double.class, lookup.findStatic(Double.class, "hashCode", |
| 140 | MethodType.fromMethodDescriptorString("(D)I", loader))); |
| 141 | primitiveHashers.put(boolean.class, lookup.findStatic(Boolean.class, "hashCode", |
| 142 | MethodType.fromMethodDescriptorString("(Z)I", loader))); |
| 143 | |
| 144 | primitiveToString.put(byte.class, lookup.findStatic(Byte.class, "toString", |
| 145 | MethodType.methodType(String.class, byte.class))); |
| 146 | primitiveToString.put(short.class, lookup.findStatic(Short.class, "toString", |
| 147 | MethodType.methodType(String.class, short.class))); |
| 148 | primitiveToString.put(char.class, lookup.findStatic(Character.class, "toString", |
| 149 | MethodType.methodType(String.class, char.class))); |
| 150 | primitiveToString.put(int.class, lookup.findStatic(Integer.class, "toString", |
| 151 | MethodType.methodType(String.class, int.class))); |
| 152 | primitiveToString.put(long.class, lookup.findStatic(Long.class, "toString", |
| 153 | MethodType.methodType(String.class, long.class))); |
| 154 | primitiveToString.put(float.class, lookup.findStatic(Float.class, "toString", |
| 155 | MethodType.methodType(String.class, float.class))); |
| 156 | primitiveToString.put(double.class, lookup.findStatic(Double.class, "toString", |
| 157 | MethodType.methodType(String.class, double.class))); |
| 158 | primitiveToString.put(boolean.class, lookup.findStatic(Boolean.class, "toString", |
| 159 | MethodType.methodType(String.class, boolean.class))); |
| 160 | } |
| 161 | catch (ReflectiveOperationException e) { |
| 162 | throw new RuntimeException(e); |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | private static int hashCombiner(int x, int y) { |
| 167 | return x*31 + y; |
| 168 | } |
| 169 | |
| 170 | private static boolean eq(Object a, Object b) { return a == b; } |
| 171 | private static boolean eq(byte a, byte b) { return a == b; } |
| 172 | private static boolean eq(short a, short b) { return a == b; } |
| 173 | private static boolean eq(char a, char b) { return a == b; } |
| 174 | private static boolean eq(int a, int b) { return a == b; } |
| 175 | private static boolean eq(long a, long b) { return a == b; } |
| 176 | private static boolean eq(float a, float b) { return Float.compare(a, b) == 0; } |
| 177 | private static boolean eq(double a, double b) { return Double.compare(a, b) == 0; } |
| 178 | private static boolean eq(boolean a, boolean b) { return a == b; } |
| 179 | |
| 180 | /** Get the method handle for combining two values of a given type */ |
| 181 | private static MethodHandle equalator(Class<?> clazz) { |
| 182 | return (clazz.isPrimitive() |
| 183 | ? primitiveEquals.get(clazz) |
| 184 | : OBJECTS_EQUALS.asType(MethodType.methodType(boolean.class, clazz, clazz))); |
| 185 | } |
| 186 | |
| 187 | /** Get the hasher for a value of a given type */ |
| 188 | private static MethodHandle hasher(Class<?> clazz) { |
| 189 | return (clazz.isPrimitive() |
| 190 | ? primitiveHashers.get(clazz) |
| 191 | : OBJECTS_HASHCODE.asType(MethodType.methodType(int.class, clazz))); |
| 192 | } |
| 193 | |
| 194 | /** Get the stringifier for a value of a given type */ |
| 195 | private static MethodHandle stringifier(Class<?> clazz) { |
| 196 | return (clazz.isPrimitive() |
| 197 | ? primitiveToString.get(clazz) |
| 198 | : OBJECTS_TOSTRING.asType(MethodType.methodType(String.class, clazz))); |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * Generates a method handle for the {@code equals} method for a given data class |
| 203 | * @param receiverClass the data class |
| 204 | * @param getters the list of getters |
| 205 | * @return the method handle |
| 206 | */ |
| 207 | private static MethodHandle makeEquals(Class<?> receiverClass, |
| 208 | List<MethodHandle> getters) { |
| 209 | MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass); |
| 210 | MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class); |
| 211 | MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z |
| 212 | MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z |
| 213 | MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z |
| 214 | MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z |
| 215 | MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z |
| 216 | |
| 217 | for (MethodHandle getter : getters) { |
| 218 | MethodHandle equalator = equalator(getter.type().returnType()); // (TT)Z |
| 219 | MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z |
| 220 | accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr)); |
| 221 | } |
| 222 | |
| 223 | return MethodHandles.guardWithTest(isSameObject, |
| 224 | instanceTrue, |
| 225 | MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse)); |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Generates a method handle for the {@code hashCode} method for a given data class |
| 230 | * @param receiverClass the data class |
| 231 | * @param getters the list of getters |
| 232 | * @return the method handle |
| 233 | */ |
| 234 | private static MethodHandle makeHashCode(Class<?> receiverClass, |
| 235 | List<MethodHandle> getters) { |
| 236 | MethodHandle accumulator = MethodHandles.dropArguments(ZERO, 0, receiverClass); // (R)I |
| 237 | |
| 238 | // @@@ Use loop combinator instead? |
| 239 | for (MethodHandle getter : getters) { |
| 240 | MethodHandle hasher = hasher(getter.type().returnType()); // (T)I |
| 241 | MethodHandle hashThisField = MethodHandles.filterArguments(hasher, 0, getter); // (R)I |
| 242 | MethodHandle combineHashes = MethodHandles.filterArguments(HASH_COMBINER, 0, accumulator, hashThisField); // (RR)I |
| 243 | accumulator = MethodHandles.permuteArguments(combineHashes, accumulator.type(), 0, 0); // adapt (R)I to (RR)I |
| 244 | } |
| 245 | |
| 246 | return accumulator; |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Generates a method handle for the {@code toString} method for a given data class |
| 251 | * @param receiverClass the data class |
| 252 | * @param getters the list of getters |
| 253 | * @param names the names |
| 254 | * @return the method handle |
| 255 | */ |
| 256 | private static MethodHandle makeToString(Class<?> receiverClass, |
| 257 | List<MethodHandle> getters, |
| 258 | List<String> names) { |
| 259 | // This is a pretty lousy algorithm; we spread the receiver over N places, |
| 260 | // apply the N getters, apply N toString operations, and concat the result with String.format |
| 261 | // Better to use String.format directly, or delegate to StringConcatFactory |
| 262 | // Also probably want some quoting around String components |
| 263 | |
| 264 | assert getters.size() == names.size(); |
| 265 | |
| 266 | int[] invArgs = new int[getters.size()]; |
| 267 | Arrays.fill(invArgs, 0); |
| 268 | MethodHandle[] filters = new MethodHandle[getters.size()]; |
| 269 | StringBuilder sb = new StringBuilder(); |
| 270 | sb.append(receiverClass.getSimpleName()).append("["); |
| 271 | for (int i=0; i<getters.size(); i++) { |
| 272 | MethodHandle getter = getters.get(i); // (R)T |
| 273 | MethodHandle stringify = stringifier(getter.type().returnType()); // (T)String |
| 274 | MethodHandle stringifyThisField = MethodHandles.filterArguments(stringify, 0, getter); // (R)String |
| 275 | filters[i] = stringifyThisField; |
| 276 | sb.append(names.get(i)).append("=%s"); |
| 277 | if (i != getters.size() - 1) |
| 278 | sb.append(", "); |
| 279 | } |
| 280 | sb.append(']'); |
| 281 | String formatString = sb.toString(); |
| 282 | MethodHandle formatter = MethodHandles.insertArguments(STRING_FORMAT, 0, formatString) |
| 283 | .asCollector(String[].class, getters.size()); // (R*)String |
| 284 | if (getters.size() == 0) { |
| 285 | // Add back extra R |
| 286 | formatter = MethodHandles.dropArguments(formatter, 0, receiverClass); |
| 287 | } |
| 288 | else { |
| 289 | MethodHandle filtered = MethodHandles.filterArguments(formatter, 0, filters); |
| 290 | formatter = MethodHandles.permuteArguments(filtered, MethodType.methodType(String.class, receiverClass), invArgs); |
| 291 | } |
| 292 | |
| 293 | return formatter; |
| 294 | } |
| 295 | |
| 296 | /** |
| 297 | * Bootstrap method to generate the {@link Object#equals(Object)}, |
| 298 | * {@link Object#hashCode()}, and {@link Object#toString()} methods, based |
| 299 | * on a description of the component names and accessor methods, for either |
| 300 | * {@code invokedynamic} call sites or dynamic constant pool entries. |
| 301 | * |
| 302 | * For more detail on the semantics of the generated methods see the specification |
| 303 | * of {@link java.lang.Record#equals(Object)}, {@link java.lang.Record#hashCode()} and |
| 304 | * {@link java.lang.Record#toString()}. |
| 305 | * |
| 306 | * |
| 307 | * @param lookup Every bootstrap method is expected to have a {@code lookup} |
| 308 | * which usually represents a lookup context with the |
| 309 | * accessibility privileges of the caller. This is because |
| 310 | * {@code invokedynamic} call sites always provide a {@code lookup} |
| 311 | * to the corresponding bootstrap method, but this method just |
| 312 | * ignores the {@code lookup} parameter |
| 313 | * @param methodName the name of the method to generate, which must be one of |
| 314 | * {@code "equals"}, {@code "hashCode"}, or {@code "toString"} |
| 315 | * @param type a {@link MethodType} corresponding the descriptor type |
| 316 | * for the method, which must correspond to the descriptor |
| 317 | * for the corresponding {@link Object} method, if linking |
| 318 | * an {@code invokedynamic} call site, or the |
| 319 | * constant {@code MethodHandle.class}, if linking a |
| 320 | * dynamic constant |
| 321 | * @param recordClass the record class hosting the record components |
| 322 | * @param names the list of component names, joined into a string |
| 323 | * separated by ";", or the empty string if there are no |
| 324 | * components. Maybe be null, if the {@code methodName} |
| 325 | * is {@code "equals"} or {@code "hashCode"}. |
| 326 | * @param getters method handles for the accessor methods for the components |
| 327 | * @return a call site if invoked by indy, or a method handle |
| 328 | * if invoked by a condy |
| 329 | * @throws IllegalArgumentException if the bootstrap arguments are invalid |
| 330 | * or inconsistent |
| 331 | * @throws Throwable if any exception is thrown during call site construction |
| 332 | */ |
| 333 | public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type, |
| 334 | Class<?> recordClass, |
| 335 | String names, |
| 336 | MethodHandle... getters) throws Throwable { |
| 337 | MethodType methodType; |
| 338 | if (type instanceof MethodType) |
| 339 | methodType = (MethodType) type; |
| 340 | else { |
| 341 | methodType = null; |
| 342 | if (!MethodHandle.class.equals(type)) |
| 343 | throw new IllegalArgumentException(type.toString()); |
| 344 | } |
| 345 | List<MethodHandle> getterList = List.of(getters); |
| 346 | MethodHandle handle = switch (methodName) { |
| 347 | case "equals" -> { |
| 348 | if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class))) |
| 349 | throw new IllegalArgumentException("Bad method type: " + methodType); |
| 350 | yield makeEquals(recordClass, getterList); |
| 351 | } |
| 352 | case "hashCode" -> { |
| 353 | if (methodType != null && !methodType.equals(MethodType.methodType(int.class, recordClass))) |
| 354 | throw new IllegalArgumentException("Bad method type: " + methodType); |
| 355 | yield makeHashCode(recordClass, getterList); |
| 356 | } |
| 357 | case "toString" -> { |
| 358 | if (methodType != null && !methodType.equals(MethodType.methodType(String.class, recordClass))) |
| 359 | throw new IllegalArgumentException("Bad method type: " + methodType); |
| 360 | List<String> nameList = "".equals(names) ? List.of() : List.of(names.split(";")); |
| 361 | if (nameList.size() != getterList.size()) |
| 362 | throw new IllegalArgumentException("Name list and accessor list do not match"); |
| 363 | yield makeToString(recordClass, getterList, nameList); |
| 364 | } |
| 365 | default -> throw new IllegalArgumentException(methodName); |
| 366 | }; |
| 367 | return methodType != null ? new ConstantCallSite(handle) : handle; |
| 368 | } |
| 369 | } |