| /* | |
| * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package sun.reflect.annotation; | |
| import java.io.ObjectInputStream; | |
| import java.lang.annotation.*; | |
| import java.lang.reflect.*; | |
| import java.io.Serializable; | |
| import java.util.*; | |
| import java.util.stream.*; | |
| /** | |
| * InvocationHandler for dynamic proxy implementation of Annotation. | |
| * | |
| * @author Josh Bloch | |
| * @since 1.5 | |
| */ | |
| class AnnotationInvocationHandler implements InvocationHandler, Serializable { | |
| @java.io.Serial | |
| private static final long serialVersionUID = 6182022883658399397L; | |
| private final Class<? extends Annotation> type; | |
| @SuppressWarnings("serial") // Not statically typed as Serializable | |
| private final Map<String, Object> memberValues; | |
| AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { | |
| Class<?>[] superInterfaces = type.getInterfaces(); | |
| if (!type.isAnnotation() || | |
| superInterfaces.length != 1 || | |
| superInterfaces[0] != java.lang.annotation.Annotation.class) | |
| throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type: " + | |
| type.getName()); | |
| this.type = type; | |
| this.memberValues = memberValues; | |
| } | |
| public Object invoke(Object proxy, Method method, Object[] args) { | |
| String member = method.getName(); | |
| int parameterCount = method.getParameterCount(); | |
| // Handle Object and Annotation methods | |
| if (parameterCount == 1 && member == "equals" && | |
| method.getParameterTypes()[0] == Object.class) { | |
| return equalsImpl(proxy, args[0]); | |
| } | |
| if (parameterCount != 0) { | |
| throw new AssertionError("Too many parameters for an annotation method"); | |
| } | |
| if (member == "toString") { | |
| return toStringImpl(); | |
| } else if (member == "hashCode") { | |
| return hashCodeImpl(); | |
| } else if (member == "annotationType") { | |
| return type; | |
| } | |
| // Handle annotation member accessors | |
| Object result = memberValues.get(member); | |
| if (result == null) | |
| throw new IncompleteAnnotationException(type, member); | |
| if (result instanceof ExceptionProxy exceptProxy) | |
| throw exceptProxy.generateException(); | |
| if (result.getClass().isArray() && Array.getLength(result) != 0) | |
| result = cloneArray(result); | |
| return result; | |
| } | |
| /** | |
| * This method, which clones its array argument, would not be necessary | |
| * if Cloneable had a public clone method. | |
| */ | |
| private Object cloneArray(Object array) { | |
| Class<?> type = array.getClass(); | |
| if (type == byte[].class) { | |
| byte[] byteArray = (byte[])array; | |
| return byteArray.clone(); | |
| } | |
| if (type == char[].class) { | |
| char[] charArray = (char[])array; | |
| return charArray.clone(); | |
| } | |
| if (type == double[].class) { | |
| double[] doubleArray = (double[])array; | |
| return doubleArray.clone(); | |
| } | |
| if (type == float[].class) { | |
| float[] floatArray = (float[])array; | |
| return floatArray.clone(); | |
| } | |
| if (type == int[].class) { | |
| int[] intArray = (int[])array; | |
| return intArray.clone(); | |
| } | |
| if (type == long[].class) { | |
| long[] longArray = (long[])array; | |
| return longArray.clone(); | |
| } | |
| if (type == short[].class) { | |
| short[] shortArray = (short[])array; | |
| return shortArray.clone(); | |
| } | |
| if (type == boolean[].class) { | |
| boolean[] booleanArray = (boolean[])array; | |
| return booleanArray.clone(); | |
| } | |
| Object[] objectArray = (Object[])array; | |
| return objectArray.clone(); | |
| } | |
| /** | |
| * Implementation of dynamicProxy.toString() | |
| */ | |
| private String toStringImpl() { | |
| StringBuilder result = new StringBuilder(128); | |
| result.append('@'); | |
| // Guard against null canonical name; shouldn't happen | |
| result.append(Objects.toString(type.getCanonicalName(), | |
| "<no canonical name>")); | |
| result.append('('); | |
| boolean firstMember = true; | |
| Set<Map.Entry<String, Object>> entries = memberValues.entrySet(); | |
| boolean loneValue = entries.size() == 1; | |
| for (Map.Entry<String, Object> e : entries) { | |
| if (firstMember) | |
| firstMember = false; | |
| else | |
| result.append(", "); | |
| String key = e.getKey(); | |
| if (!loneValue || !"value".equals(key)) { | |
| result.append(key); | |
| result.append('='); | |
| } | |
| loneValue = false; | |
| result.append(memberValueToString(e.getValue())); | |
| } | |
| result.append(')'); | |
| return result.toString(); | |
| } | |
| /** | |
| * Translates a member value (in "dynamic proxy return form") into a string. | |
| */ | |
| private static String memberValueToString(Object value) { | |
| Class<?> type = value.getClass(); | |
| if (!type.isArray()) { | |
| // primitive value, string, class, enum const, or annotation | |
| if (type == Class.class) | |
| return toSourceString((Class<?>) value); | |
| else if (type == String.class) | |
| return toSourceString((String) value); | |
| if (type == Character.class) | |
| return toSourceString((char) value); | |
| else if (type == Double.class) | |
| return toSourceString((double) value); | |
| else if (type == Float.class) | |
| return toSourceString((float) value); | |
| else if (type == Long.class) | |
| return toSourceString((long) value); | |
| else if (type == Byte.class) | |
| return toSourceString((byte) value); | |
| else if (value instanceof Enum<?> v) | |
| // Predicate above covers enum constants, including | |
| // those with specialized class bodies. | |
| return toSourceString(v); | |
| else | |
| return value.toString(); | |
| } else { | |
| Stream<String> stringStream; | |
| if (type == byte[].class) | |
| stringStream = convert((byte[]) value); | |
| else if (type == char[].class) | |
| stringStream = convert((char[]) value); | |
| else if (type == double[].class) | |
| stringStream = DoubleStream.of((double[]) value) | |
| .mapToObj(AnnotationInvocationHandler::toSourceString); | |
| else if (type == float[].class) | |
| stringStream = convert((float[]) value); | |
| else if (type == int[].class) | |
| stringStream = IntStream.of((int[]) value).mapToObj(String::valueOf); | |
| else if (type == long[].class) { | |
| stringStream = LongStream.of((long[]) value) | |
| .mapToObj(AnnotationInvocationHandler::toSourceString); | |
| } else if (type == short[].class) | |
| stringStream = convert((short[]) value); | |
| else if (type == boolean[].class) | |
| stringStream = convert((boolean[]) value); | |
| else if (type == Class[].class) | |
| stringStream = | |
| Arrays.stream((Class<?>[]) value). | |
| map(AnnotationInvocationHandler::toSourceString); | |
| else if (type == String[].class) | |
| stringStream = | |
| Arrays.stream((String[])value). | |
| map(AnnotationInvocationHandler::toSourceString); | |
| else if (type.getComponentType().isEnum()) | |
| stringStream = | |
| Arrays.stream((Enum<?>[])value). | |
| map(AnnotationInvocationHandler::toSourceString); | |
| else | |
| stringStream = Arrays.stream((Object[])value).map(Objects::toString); | |
| return stringStreamToString(stringStream); | |
| } | |
| } | |
| /** | |
| * Translates a Class value to a form suitable for use in the | |
| * string representation of an annotation. | |
| */ | |
| private static String toSourceString(Class<?> clazz) { | |
| // Guard against null canonical name; shouldn't happen | |
| return Objects.toString(clazz.getCanonicalName(), | |
| "<no canonical name>") + ".class"; | |
| } | |
| private static String toSourceString(float f) { | |
| if (Float.isFinite(f)) | |
| return Float.toString(f) + "f" ; | |
| else { | |
| if (Float.isInfinite(f)) { | |
| return (f < 0.0f) ? "-1.0f/0.0f": "1.0f/0.0f"; | |
| } else | |
| return "0.0f/0.0f"; | |
| } | |
| } | |
| private static String toSourceString(double d) { | |
| if (Double.isFinite(d)) | |
| return Double.toString(d); | |
| else { | |
| if (Double.isInfinite(d)) { | |
| return (d < 0.0f) ? "-1.0/0.0": "1.0/0.0"; | |
| } else | |
| return "0.0/0.0"; | |
| } | |
| } | |
| private static String toSourceString(char c) { | |
| StringBuilder sb = new StringBuilder(4); | |
| sb.append('\''); | |
| sb.append(quote(c, true)); | |
| return sb.append('\'') .toString(); | |
| } | |
| /** | |
| * Escapes a character if it has an escape sequence or is | |
| * non-printable ASCII. Leaves non-ASCII characters alone. | |
| */ | |
| private static String quote(char ch, boolean charContext) { | |
| /* | |
| * In a char context, single quote (') must be escaped and | |
| * double quote (") need not be escaped. In a non-char | |
| * context, in other words a string context, the reverse is | |
| * true. | |
| */ | |
| switch (ch) { | |
| case '\b': return "\\b"; | |
| case '\f': return "\\f"; | |
| case '\n': return "\\n"; | |
| case '\r': return "\\r"; | |
| case '\t': return "\\t"; | |
| case '\'': return (charContext ? "\\'" : "'"); | |
| case '\"': return (charContext ? "\"" : "\\\""); | |
| case '\\': return "\\\\"; | |
| default: | |
| return (isPrintableAscii(ch)) | |
| ? String.valueOf(ch) | |
| : String.format("\\u%04x", (int) ch); | |
| } | |
| } | |
| /** | |
| * Is a character printable ASCII? | |
| */ | |
| private static boolean isPrintableAscii(char ch) { | |
| return ch >= ' ' && ch <= '~'; | |
| } | |
| private static String toSourceString(byte b) { | |
| return String.format("(byte)0x%02x", b); | |
| } | |
| private static String toSourceString(long ell) { | |
| return String.valueOf(ell) + "L"; | |
| } | |
| private static String toSourceString(Enum<?> enumConstant) { | |
| return enumConstant.name(); | |
| } | |
| /** | |
| * Return a string suitable for use in the string representation | |
| * of an annotation. | |
| */ | |
| private static String toSourceString(String s) { | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append('"'); | |
| for (int i = 0; i < s.length(); i++) { | |
| sb.append(quote(s.charAt(i), false)); | |
| } | |
| sb.append('"'); | |
| return sb.toString(); | |
| } | |
| private static Stream<String> convert(byte[] values) { | |
| List<String> list = new ArrayList<>(values.length); | |
| for (byte b : values) | |
| list.add(toSourceString(b)); | |
| return list.stream(); | |
| } | |
| private static Stream<String> convert(char[] values) { | |
| List<String> list = new ArrayList<>(values.length); | |
| for (char c : values) | |
| list.add(toSourceString(c)); | |
| return list.stream(); | |
| } | |
| private static Stream<String> convert(float[] values) { | |
| List<String> list = new ArrayList<>(values.length); | |
| for (float f : values) { | |
| list.add(toSourceString(f)); | |
| } | |
| return list.stream(); | |
| } | |
| private static Stream<String> convert(short[] values) { | |
| List<String> list = new ArrayList<>(values.length); | |
| for (short s : values) | |
| list.add(Short.toString(s)); | |
| return list.stream(); | |
| } | |
| private static Stream<String> convert(boolean[] values) { | |
| List<String> list = new ArrayList<>(values.length); | |
| for (boolean b : values) | |
| list.add(Boolean.toString(b)); | |
| return list.stream(); | |
| } | |
| private static String stringStreamToString(Stream<String> stream) { | |
| return stream.collect(Collectors.joining(", ", "{", "}")); | |
| } | |
| /** | |
| * Implementation of dynamicProxy.equals(Object o) | |
| */ | |
| private Boolean equalsImpl(Object proxy, Object o) { | |
| if (o == proxy) | |
| return true; | |
| if (!type.isInstance(o)) | |
| return false; | |
| for (Method memberMethod : getMemberMethods()) { | |
| if (memberMethod.isSynthetic()) | |
| continue; | |
| String member = memberMethod.getName(); | |
| Object ourValue = memberValues.get(member); | |
| Object otherValue = null; | |
| AnnotationInvocationHandler otherHandler = asOneOfUs(o); | |
| if (otherHandler != null) { | |
| otherValue = otherHandler.memberValues.get(member); | |
| } else { | |
| try { | |
| otherValue = memberMethod.invoke(o); | |
| } catch (InvocationTargetException e) { | |
| return false; | |
| } catch (IllegalAccessException e) { | |
| throw new AssertionError(e); | |
| } | |
| } | |
| if (!memberValueEquals(ourValue, otherValue)) | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Returns an object's invocation handler if that object is a dynamic | |
| * proxy with a handler of type AnnotationInvocationHandler. | |
| * Returns null otherwise. | |
| */ | |
| private AnnotationInvocationHandler asOneOfUs(Object o) { | |
| if (Proxy.isProxyClass(o.getClass())) { | |
| InvocationHandler handler = Proxy.getInvocationHandler(o); | |
| if (handler instanceof AnnotationInvocationHandler annotationHandler) | |
| return annotationHandler; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Returns true iff the two member values in "dynamic proxy return form" | |
| * are equal using the appropriate equality function depending on the | |
| * member type. The two values will be of the same type unless one of | |
| * the containing annotations is ill-formed. If one of the containing | |
| * annotations is ill-formed, this method will return false unless the | |
| * two members are identical object references. | |
| */ | |
| private static boolean memberValueEquals(Object v1, Object v2) { | |
| Class<?> type = v1.getClass(); | |
| // Check for primitive, string, class, enum const, annotation, | |
| // or ExceptionProxy | |
| if (!type.isArray()) | |
| return v1.equals(v2); | |
| // Check for array of string, class, enum const, annotation, | |
| // or ExceptionProxy | |
| if (v1 instanceof Object[] a1 && v2 instanceof Object[] a2) | |
| return Arrays.equals(a1, a2); | |
| // Check for ill formed annotation(s) | |
| if (v2.getClass() != type) | |
| return false; | |
| // Deal with array of primitives | |
| if (type == byte[].class) | |
| return Arrays.equals((byte[]) v1, (byte[]) v2); | |
| if (type == char[].class) | |
| return Arrays.equals((char[]) v1, (char[]) v2); | |
| if (type == double[].class) | |
| return Arrays.equals((double[]) v1, (double[]) v2); | |
| if (type == float[].class) | |
| return Arrays.equals((float[]) v1, (float[]) v2); | |
| if (type == int[].class) | |
| return Arrays.equals((int[]) v1, (int[]) v2); | |
| if (type == long[].class) | |
| return Arrays.equals((long[]) v1, (long[]) v2); | |
| if (type == short[].class) | |
| return Arrays.equals((short[]) v1, (short[]) v2); | |
| assert type == boolean[].class; | |
| return Arrays.equals((boolean[]) v1, (boolean[]) v2); | |
| } | |
| /** | |
| * Returns the member methods for our annotation type. These are | |
| * obtained lazily and cached, as they're expensive to obtain | |
| * and we only need them if our equals method is invoked (which should | |
| * be rare). | |
| */ | |
| private Method[] getMemberMethods() { | |
| Method[] value = memberMethods; | |
| if (value == null) { | |
| value = computeMemberMethods(); | |
| memberMethods = value; | |
| } | |
| return value; | |
| } | |
| private Method[] computeMemberMethods() { | |
| final Method[] methods = type.getDeclaredMethods(); | |
| validateAnnotationMethods(methods); | |
| AccessibleObject.setAccessible(methods, true); | |
| return methods; | |
| } | |
| private transient volatile Method[] memberMethods; | |
| /** | |
| * Validates that a method is structurally appropriate for an | |
| * annotation type. As of Java SE 8, annotation types cannot | |
| * contain static methods and the declared methods of an | |
| * annotation type must take zero arguments and there are | |
| * restrictions on the return type. | |
| */ | |
| private void validateAnnotationMethods(Method[] memberMethods) { | |
| /* | |
| * Specification citations below are from JLS | |
| * 9.6.1. Annotation Type Elements | |
| */ | |
| boolean valid = true; | |
| Method currentMethod = null; | |
| for(Method method : memberMethods) { | |
| currentMethod = method; | |
| int modifiers = method.getModifiers(); | |
| // Skip over methods that may be a static initializer or | |
| // similar construct. A static initializer may be used for | |
| // purposes such as initializing a lambda stored in an | |
| // interface field. | |
| if (method.isSynthetic() && | |
| (modifiers & (Modifier.STATIC | Modifier.PRIVATE)) != 0 && | |
| method.getParameterCount() == 0) { | |
| continue; | |
| } | |
| /* | |
| * "By virtue of the AnnotationTypeElementDeclaration | |
| * production, a method declaration in an annotation type | |
| * declaration cannot have formal parameters, type | |
| * parameters, or a throws clause. | |
| * | |
| * "By virtue of the AnnotationTypeElementModifier | |
| * production, a method declaration in an annotation type | |
| * declaration cannot be default or static." | |
| */ | |
| if (modifiers != (Modifier.PUBLIC | Modifier.ABSTRACT) || | |
| method.isDefault() || | |
| method.getParameterCount() != 0 || | |
| method.getExceptionTypes().length != 0) { | |
| valid = false; | |
| break; | |
| } | |
| /* | |
| * "It is a compile-time error if the return type of a | |
| * method declared in an annotation type is not one of the | |
| * following: a primitive type, String, Class, any | |
| * parameterized invocation of Class, an enum type | |
| * (section 8.9), an annotation type, or an array type | |
| * (chapter 10) whose element type is one of the preceding | |
| * types." | |
| */ | |
| Class<?> returnType = method.getReturnType(); | |
| if (returnType.isArray()) { | |
| returnType = returnType.getComponentType(); | |
| if (returnType.isArray()) { // Only single dimensional arrays | |
| valid = false; | |
| break; | |
| } | |
| } | |
| if (!((returnType.isPrimitive() && returnType != void.class) || | |
| returnType == java.lang.String.class || | |
| returnType == java.lang.Class.class || | |
| returnType.isEnum() || | |
| returnType.isAnnotation())) { | |
| valid = false; | |
| break; | |
| } | |
| /* | |
| * "It is a compile-time error if any method declared in an | |
| * annotation type has a signature that is | |
| * override-equivalent to that of any public or protected | |
| * method declared in class Object or in the interface | |
| * java.lang.annotation.Annotation." | |
| * | |
| * The methods in Object or Annotation meeting the other | |
| * criteria (no arguments, constrained return type, etc.) | |
| * above are: | |
| * | |
| * String toString() | |
| * int hashCode() | |
| * Class<? extends Annotation> annotationType() | |
| */ | |
| String methodName = method.getName(); | |
| if ((methodName.equals("toString") && returnType == java.lang.String.class) || | |
| (methodName.equals("hashCode") && returnType == int.class) || | |
| (methodName.equals("annotationType") && returnType == java.lang.Class.class)) { | |
| valid = false; | |
| break; | |
| } | |
| } | |
| if (valid) | |
| return; | |
| else | |
| throw new AnnotationFormatError("Malformed method on an annotation type: " + | |
| currentMethod.toString()); | |
| } | |
| /** | |
| * Implementation of dynamicProxy.hashCode() | |
| */ | |
| private int hashCodeImpl() { | |
| int result = 0; | |
| for (Map.Entry<String, Object> e : memberValues.entrySet()) { | |
| result += (127 * e.getKey().hashCode()) ^ | |
| memberValueHashCode(e.getValue()); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Computes hashCode of a member value (in "dynamic proxy return form") | |
| */ | |
| private static int memberValueHashCode(Object value) { | |
| Class<?> type = value.getClass(); | |
| if (!type.isArray()) // primitive, string, class, enum const, | |
| // or annotation | |
| return value.hashCode(); | |
| if (type == byte[].class) | |
| return Arrays.hashCode((byte[]) value); | |
| if (type == char[].class) | |
| return Arrays.hashCode((char[]) value); | |
| if (type == double[].class) | |
| return Arrays.hashCode((double[]) value); | |
| if (type == float[].class) | |
| return Arrays.hashCode((float[]) value); | |
| if (type == int[].class) | |
| return Arrays.hashCode((int[]) value); | |
| if (type == long[].class) | |
| return Arrays.hashCode((long[]) value); | |
| if (type == short[].class) | |
| return Arrays.hashCode((short[]) value); | |
| if (type == boolean[].class) | |
| return Arrays.hashCode((boolean[]) value); | |
| return Arrays.hashCode((Object[]) value); | |
| } | |
| @java.io.Serial | |
| private void readObject(java.io.ObjectInputStream s) | |
| throws java.io.IOException, ClassNotFoundException { | |
| ObjectInputStream.GetField fields = s.readFields(); | |
| @SuppressWarnings("unchecked") | |
| Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null); | |
| @SuppressWarnings("unchecked") | |
| Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null); | |
| // Check to make sure that types have not evolved incompatibly | |
| AnnotationType annotationType = null; | |
| try { | |
| annotationType = AnnotationType.getInstance(t); | |
| } catch(IllegalArgumentException e) { | |
| // Class is no longer an annotation type; time to punch out | |
| throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); | |
| } | |
| Map<String, Class<?>> memberTypes = annotationType.memberTypes(); | |
| // consistent with runtime Map type | |
| Map<String, Object> mv = new LinkedHashMap<>(); | |
| // If there are annotation members without values, that | |
| // situation is handled by the invoke method. | |
| for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) { | |
| String name = memberValue.getKey(); | |
| Object value = null; | |
| Class<?> memberType = memberTypes.get(name); | |
| if (memberType != null) { // i.e. member still exists | |
| value = memberValue.getValue(); | |
| if (!(memberType.isInstance(value) || | |
| value instanceof ExceptionProxy)) { | |
| value = new AnnotationTypeMismatchExceptionProxy( | |
| Objects.toIdentityString(value)) | |
| .setMember(annotationType.members().get(name)); | |
| } | |
| } | |
| mv.put(name, value); | |
| } | |
| UnsafeAccessor.setType(this, t); | |
| UnsafeAccessor.setMemberValues(this, mv); | |
| } | |
| /** | |
| * Gets an unmodifiable view on the member values. | |
| */ | |
| Map<String, Object> memberValues() { | |
| return Collections.unmodifiableMap(memberValues); | |
| } | |
| private static class UnsafeAccessor { | |
| private static final jdk.internal.misc.Unsafe unsafe | |
| = jdk.internal.misc.Unsafe.getUnsafe(); | |
| private static final long typeOffset = unsafe.objectFieldOffset | |
| (AnnotationInvocationHandler.class, "type"); | |
| private static final long memberValuesOffset = unsafe.objectFieldOffset | |
| (AnnotationInvocationHandler.class, "memberValues"); | |
| static void setType(AnnotationInvocationHandler o, | |
| Class<? extends Annotation> type) { | |
| unsafe.putReference(o, typeOffset, type); | |
| } | |
| static void setMemberValues(AnnotationInvocationHandler o, | |
| Map<String, Object> memberValues) { | |
| unsafe.putReference(o, memberValuesOffset, memberValues); | |
| } | |
| } | |
| } |