From 0187beb9e29adaab5acd96e4d54b230023af5011 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Tue, 2 Sep 2025 01:23:57 +0200 Subject: [PATCH 1/3] some optimization work --- .../impl/transform/unsafe/FieldAccessor.java | 297 ++++++++++++++++ .../impl/transform/unsafe/FieldOffsetOps.java | 7 +- .../impl/transform/unsafe/FieldOps.java | 324 ------------------ .../unsafe/UnsafeReplacementDefiner.java | 2 +- .../unsafe/UnsafeReplacementDelegate.java | 32 +- .../impl/transform/unsafe/ValueTypeKind.java | 7 +- .../unsafe/UnsafeReplacementDelegateTest.java | 10 +- 7 files changed, 328 insertions(+), 351 deletions(-) create mode 100644 wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java delete mode 100644 wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOps.java diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java new file mode 100644 index 0000000000..814f4cb11f --- /dev/null +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java @@ -0,0 +1,297 @@ +/* + * Copyright 2019-2024 CloudNetService team & contributors + * + * 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 eu.cloudnetservice.wrapper.impl.transform.unsafe; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.function.Function; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +/** + * @since 4.0 + */ +final class FieldAccessor { + + private final Class fieldType; + private final ValueTypeKind kind; + private final WeakReference fieldRef; + + private final VarHandle varHandle; + private final MethodHandle putMethodHandle; // null if VarHandle can support set (field is not trusted final) + + /** + * + * @param field + */ + private FieldAccessor(@NonNull Field field) throws ReflectiveOperationException { + this.fieldType = field.getType(); + this.kind = ValueTypeKind.of(this.fieldType); + this.fieldRef = new WeakReference<>(field); + + // resolve field var handle, add leading dummy Object argument for static handles (easier calling later) + var lookup = OpConstants.TRUSTED_LOOKUP.get(); + var fieldVh = lookup.unreflectVarHandle(field); + var isStatic = Modifier.isStatic(field.getModifiers()); + this.varHandle = switch (isStatic) { + case false -> fieldVh; + case true -> MethodHandles.dropCoordinates(fieldVh, 0, Object.class); + }; + + // resolve field put method handle, only needed when the var handle cannot be used to set the field value + // if present a static field put mh will have a leading dummy object argument for calling convenience + var vhCanSet = this.varHandle.isAccessModeSupported(VarHandle.AccessMode.SET); + this.putMethodHandle = switch (vhCanSet) { + case true -> null; // can do everything with VH + case false -> { + if (isStatic) { + var mh = lookup.findStaticSetter(field.getDeclaringClass(), field.getName(), field.getType()); + yield MethodHandles.dropArguments(mh, 0, Object.class); + } else { + yield lookup.findSetter(field.getDeclaringClass(), field.getName(), field.getType()); + } + } + }; + } + + /** + * + * @param field + * @return + */ + public static @NonNull FieldAccessor make(@NonNull Field field) { + try { + return new FieldAccessor(field); + } catch (ReflectiveOperationException exception) { + throw new IllegalStateException(exception); + } + } + + /** + * Checks and converts the given field value if necessary. Throws an exception when an attempt is made to write an + * incompatible type into a reference field. + * + * @param value the value to convert. + * @param requiredType the field type. + * @param kind the field type kind. + * @return the value converted into a form that can be written into the target field. + * @throws NullPointerException if the given required type or required type kind is null. + * @throws UnsupportedOperationException when an incompatible value for the target reference field type is provided. + */ + private static @Nullable Object convertFieldValue( + @Nullable Object value, + @NonNull Class requiredType, + @NonNull ValueTypeKind kind + ) { + if (kind == ValueTypeKind.REF) { + if (value != null && !requiredType.isAssignableFrom(value.getClass())) { + // value is unsupported by the field, this type of method call was allowed with unsafe + // but is no longer allowed as we cannot support this with the safe replacements of the jvm + throw new UnsupportedOperationException( + "Tried to put value of type " + value.getClass() + " into field of type " + requiredType); + } + + return value; + } + + // convert primitive types between each other as close as possible + return switch (kind) { + case BYTE -> convertNumber(value, (byte) 0, Number::byteValue); + case SHORT -> convertNumber(value, (short) 0, Number::shortValue); + case INT -> convertNumber(value, 0, Number::intValue); + case LONG -> convertNumber(value, 0L, Number::longValue); + case FLOAT -> convertNumber(value, 0F, Number::floatValue); + case DOUBLE -> convertNumber(value, 0D, Number::doubleValue); + case BOOL -> switch (value) { + case Boolean b -> b; + case Number n -> n.byteValue() == 1; + case Character c -> c != '\0'; + case null, default -> false; + }; + case CHAR -> switch (value) { + case Character c -> c; + case Number n -> (char) n.byteValue(); + case Boolean b -> b ? '\1' : '\0'; + case null, default -> '\0'; + }; + default -> throw new AssertionError(); + }; + } + + /** + * Converts the given value into a number of the expected type, conditionally returning the given default value if the + * given value cannot be converted somehow. + * + * @param value the value to convert into the expected number. + * @param defaultValue the default value of the number type. + * @param numToType a conversion function for a general number to the expected number type. + * @return the given value, converted to a number of the requested type. + * @throws NullPointerException if the given default value or number converter is null. + */ + private static @NonNull Object convertNumber( + @Nullable Object value, + @NonNull Object defaultValue, + @NonNull Function numToType + ) { + return switch (value) { + case Number n -> numToType.apply(n); + case Boolean b -> numToType.apply(b ? 1 : 0); + case Character c -> numToType.apply((byte) c.charValue()); + case null, default -> defaultValue; + }; + } + + /** + * Gets the value of the wrapped field in the given instance. + * + * @param instance the instance to get the value from, possibly null. + * @param op the operation to use for getting the value. + * @return the value of the given field in the given instance, possibly null. + * @throws NullPointerException if the given get operation type is null. + */ + public @Nullable Object get(@Nullable Object instance, @NonNull OpConstants.GetOp op) { + return switch (op) { + case DEFAULT -> this.varHandle.get(instance); + case VOLATILE -> this.varHandle.getVolatile(instance); + }; + } + + /** + * Puts the given value into the wrapped field in the given instance. Does nothing in case the given value might not + * be supported by the given field. This method does not guarantee the requested memory semantics if the field is + * trusted final. + * + * @param instance the instance to set the value in, possibly null. + * @param value the value to set into the given field, possibly null. + * @param op the operation to use for setting the value. + * @throws NullPointerException if the given set operation type is null. + * @throws Throwable if an unexpected exception occurs while writing the field value. + */ + public void put( + @Nullable Object instance, + @Nullable Object value, + @NonNull OpConstants.SetOp op + ) throws Throwable { + var convertedVal = convertFieldValue(value, this.fieldType, this.kind); + if (this.putMethodHandle == null) { + switch (op) { + case DEFAULT -> this.varHandle.set(instance, convertedVal); + case VOLATILE -> this.varHandle.setVolatile(instance, convertedVal); + case RELEASE -> this.varHandle.setRelease(instance, convertedVal); + } + } else { + this.putMethodHandle.invoke(instance, convertedVal); + } + } + + /** + * Gets the value of the wrapped field in the given instance and sets it to the given value. Returns null if the field + * value was null or the given value is not supported by the given field. This method does not guarantee the requested + * memory semantics if the field is trusted final. + * + * @param instance the instance to get the value from, possibly null. + * @param value the value to set into the given field, possibly null. + * @return the old value of the given field in the given instance, possibly null. + * @throws Throwable if an unexpected exception occurs while reading or writing the field value. + */ + public @Nullable Object getAndPut( + @Nullable Object instance, + @Nullable Object value + ) throws Throwable { + var convertedVal = convertFieldValue(value, this.fieldType, this.kind); + if (this.putMethodHandle == null) { + return this.varHandle.getAndSet(instance, convertedVal); + } else { + var currentValue = this.varHandle.get(instance); + this.putMethodHandle.invoke(instance, convertedVal); + return currentValue; + } + } + + /** + * Gets the current value of the wrapped field in the given instance and adds the given value to it. This method does + * not guarantee the requested memory semantics if the field is trusted final. + * + * @param instance the instance to get the value from, possibly null. + * @param value the value to add to the current field value. + * @return the old value of the given field in the given instance, possibly null. + * @throws Throwable if an unexpected exception occurs while reading or writing the field value. + */ + // NOTE: kind can only be INT or LONG + @SuppressWarnings("ConstantConditions") + public @Nullable Object getAndAdd( + @Nullable Object instance, + @Nullable Object value + ) throws Throwable { + var convertedVal = convertFieldValue(value, this.fieldType, this.kind); + if (this.putMethodHandle == null) { + return this.varHandle.getAndAdd(instance, convertedVal); + } else { + // field and converted value has to be a primitive at this point, so casting is safe + var oldVal = (Number) this.varHandle.get(instance); + var valueSum = ((Number) convertedVal).longValue() + oldVal.longValue(); + var fieldVal = convertFieldValue(valueSum, this.fieldType, this.kind); + this.putMethodHandle.invoke(instance, fieldVal); + return oldVal; + } + } + + /** + * Sets the value of the wrapped field in case the current value of the field is equal to the given expected value. + * This method does not guarantee the requested memory semantics if the field is trusted final. + * + * @param instance the instance that contains the field to compare and swap the value of, possibly null. + * @param expected the expected value of the field, possibly null. + * @param value the value to set into the given field, possibly null. + * @return true if the field was successfully set, false otherwise. + * @throws NullPointerException if the given type kind is null. + * @throws Throwable if an unexpected exception occurs while reading or writing the field value. + */ + public boolean compareAndSet( + @Nullable Object instance, + @Nullable Object expected, + @Nullable Object value + ) throws Throwable { + var convertedVal = convertFieldValue(value, this.fieldType, this.kind); + var convertedExp = convertFieldValue(expected, this.fieldType, this.kind); + if (this.putMethodHandle == null) { + return this.varHandle.compareAndSet(instance, convertedExp, convertedVal); + } else { + var witness = this.varHandle.get(instance); + var witnessIsExpected = this.kind.areValuesEqual(witness, convertedExp); + if (witnessIsExpected) { + this.putMethodHandle.invoke(instance, convertedVal); + return true; + } + return false; + } + } + + /** + * + * @return + */ + @VisibleForTesting + public @Nullable Field wrappedField() { + return this.fieldRef.get(); + } +} diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java index 5425b57e3c..060a7e3f61 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java @@ -32,7 +32,7 @@ final class FieldOffsetOps { private static final int FIELD_COUNT_IN_CLASS = Class.class.getDeclaredFields().length; - private static final Map FIELD_LOOKUP_CACHE = new ConcurrentHashMap<>(16, 0.9f, 1); + private static final Map FIELD_LOOKUP_CACHE = new ConcurrentHashMap<>(); private FieldOffsetOps() { throw new UnsupportedOperationException(); @@ -106,7 +106,7 @@ private static long fieldSlot(@NonNull Field field) { * @return the field with the given offset in the given base. * @throws NullPointerException if the given base is null. */ - public static @Nullable Field fieldFromOffset(@NonNull Object base, long offset) { + public static @Nullable FieldAccessor fieldFromOffset(@NonNull Object base, long offset) { if (offset < 0) { // bail out early, a field with a negative offset cannot exist return null; @@ -135,7 +135,8 @@ private static long fieldSlot(@NonNull Field field) { for (var c : classHierarchy.reversed()) { var fields = c.getDeclaredFields(); if (remaining < fields.length) { - return fields[(int) remaining]; + var field = fields[(int) remaining]; + return FieldAccessor.make(field); } remaining -= fields.length; diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOps.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOps.java deleted file mode 100644 index 4e722682d2..0000000000 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOps.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright 2019-2024 CloudNetService team & contributors - * - * 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 eu.cloudnetservice.wrapper.impl.transform.unsafe; - -import java.lang.invoke.MethodHandles; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.function.Function; -import lombok.NonNull; -import org.jetbrains.annotations.Nullable; - -/** - * Implements all unsafe operations that are related to on-heap fields. - * - * @since 4.0 - */ -final class FieldOps { - - private FieldOps() { - throw new UnsupportedOperationException(); - } - - /** - * Checks and converts the given field value if necessary. Throws an exception when an attempt is made to write an - * incompatible type into a reference field. - * - * @param value the value to convert. - * @param requiredType the field type. - * @param kind the field type kind. - * @return the value converted into a form that can be written into the target field. - * @throws NullPointerException if the given required type or required type kind is null. - * @throws UnsupportedOperationException when an incompatible value for the target reference field type is provided. - */ - private static @Nullable Object convertFieldValue( - @Nullable Object value, - @NonNull Class requiredType, - @NonNull ValueTypeKind kind - ) { - if (kind == ValueTypeKind.REF) { - if (value != null && !requiredType.isAssignableFrom(value.getClass())) { - // value is unsupported by the field, this type of method call was allowed with unsafe - // but is no longer allowed as we cannot support this with the safe replacements of the jvm - throw new UnsupportedOperationException( - "Tried to put value of type " + value.getClass() + " into field of type " + requiredType); - } - - return value; - } - - // convert primitive types between each other as close as possible - return switch (kind) { - case BYTE -> convertNumber(value, (byte) 0, Number::byteValue); - case SHORT -> convertNumber(value, (short) 0, Number::shortValue); - case INT -> convertNumber(value, 0, Number::intValue); - case LONG -> convertNumber(value, 0L, Number::longValue); - case FLOAT -> convertNumber(value, 0F, Number::floatValue); - case DOUBLE -> convertNumber(value, 0D, Number::doubleValue); - case BOOL -> switch (value) { - case Boolean b -> b; - case Number n -> n.byteValue() == 1; - case Character c -> c != '\0'; - case null, default -> false; - }; - case CHAR -> switch (value) { - case Character c -> c; - case Number n -> (char) n.byteValue(); - case Boolean b -> b ? '\1' : '\0'; - case null, default -> '\0'; - }; - default -> throw new AssertionError(); - }; - } - - /** - * Converts the given value into a number of the expected type, conditionally returning the given default value if the - * given value cannot be converted somehow. - * - * @param value the value to convert into the expected number. - * @param defaultValue the default value of the number type. - * @param numToType a conversion function for a general number to the expected number type. - * @return the given value, converted to a number of the requested type. - * @throws NullPointerException if the given default value or number converter is null. - */ - private static @NonNull Object convertNumber( - @Nullable Object value, - @NonNull Object defaultValue, - @NonNull Function numToType - ) { - return switch (value) { - case Number n -> numToType.apply(n); - case Boolean b -> numToType.apply(b ? 1 : 0); - case Character c -> numToType.apply((byte) c.charValue()); - case null, default -> defaultValue; - }; - } - - /** - * Gets the value of the given field in the given instance. - * - * @param field the field to get the value of. - * @param instance the instance to get the value from, possibly null. - * @param op the operation to use for getting the value. - * @return the value of the given field in the given instance, possibly null. - * @throws NullPointerException if the given field or get operation type is null. - * @throws Throwable if an unexpected exception occurs while reading the field value. - */ - static @Nullable Object fieldGet( - @NonNull Field field, - @Nullable Object instance, - @NonNull OpConstants.GetOp op - ) throws Throwable { - var lookup = OpConstants.TRUSTED_LOOKUP.get(); - var isStatic = Modifier.isStatic(field.getModifiers()); - var handle = lookup.unreflectVarHandle(field); - if (isStatic) { - return switch (op) { - case DEFAULT -> handle.get(); - case VOLATILE -> handle.getVolatile(); - }; - } else { - return switch (op) { - case DEFAULT -> handle.get(instance); - case VOLATILE -> handle.getVolatile(instance); - }; - } - } - - /** - * Puts the given value into the given field in the given instance. Does nothing in case the given value might not be - * supported by the given field. This method does not guarantee the requested memory semantics if the field is final. - * - * @param kind the type kind of the given field. - * @param field the field to set the value of. - * @param instance the instance to set the value in, possibly null. - * @param val the value to set into the given field, possibly null. - * @param op the operation to use for setting the value. - * @throws NullPointerException if the given type kind, field or set operation type is null. - * @throws Throwable if an unexpected exception occurs while writing the field value. - */ - static void fieldPut( - @NonNull ValueTypeKind kind, - @NonNull Field field, - @Nullable Object instance, - @Nullable Object val, - @NonNull OpConstants.SetOp op - ) throws Throwable { - var convertedVal = convertFieldValue(val, field.getType(), kind); - var lookup = OpConstants.TRUSTED_LOOKUP.get(); - var isStatic = Modifier.isStatic(field.getModifiers()); - if (Modifier.isFinal(field.getModifiers())) { - // the field is final, cannot use var handles, fall back to default impl - fieldPutMh(field, instance, convertedVal, lookup); - } else { - // field is non-final, can use var handles - var handle = lookup.unreflectVarHandle(field); - if (isStatic) { - switch (op) { - case DEFAULT -> handle.set(convertedVal); - case VOLATILE -> handle.setVolatile(convertedVal); - case RELEASE -> handle.setRelease(convertedVal); - } - } else { - switch (op) { - case DEFAULT -> handle.set(instance, convertedVal); - case VOLATILE -> handle.setVolatile(instance, convertedVal); - case RELEASE -> handle.setRelease(instance, convertedVal); - } - } - } - } - - /** - * Puts the given field value using method handles, only used as a fallback when var handles cannot be used. - * - * @param field the field to set the value of. - * @param instance the instance to set the value in, possibly null. - * @param val the value to set into the given field, possibly null. - * @throws NullPointerException if the given type kind, field or set operation type is null. - * @throws Throwable if an unexpected exception occurs while writing the field value. - */ - private static void fieldPutMh( - @NonNull Field field, - @Nullable Object instance, - @Nullable Object val, - @NonNull MethodHandles.Lookup lookup - ) throws Throwable { - var isStatic = Modifier.isStatic(field.getModifiers()); - if (isStatic) { - var setter = lookup.findStaticSetter(field.getDeclaringClass(), field.getName(), field.getType()); - setter.invoke(val); - } else { - var setter = lookup.findSetter(field.getDeclaringClass(), field.getName(), field.getType()); - setter.invoke(instance, val); - } - } - - /** - * Gets the value of the given field in the given instance and sets it to the given value. Returns null if the field - * value was null or the given value is not supported by the given field. This method does not guarantee the requested - * memory semantics if the field is final. - * - * @param kind the type kind of the given field. - * @param field the field to get the value of. - * @param instance the instance to get the value from, possibly null. - * @param val the value to set into the given field, possibly null. - * @return the old value of the given field in the given instance, possibly null. - * @throws NullPointerException if the given type kind or field is null. - * @throws Throwable if an unexpected exception occurs while reading or writing the field value. - */ - static @Nullable Object fieldGetPut( - @NonNull ValueTypeKind kind, - @NonNull Field field, - @Nullable Object instance, - @Nullable Object val - ) throws Throwable { - var convertedVal = convertFieldValue(val, field.getType(), kind); - var lookup = OpConstants.TRUSTED_LOOKUP.get(); - var isStatic = Modifier.isStatic(field.getModifiers()); - if (Modifier.isFinal(field.getModifiers())) { - // the field is final, cannot use var handles, fall back to default impl - var oldVal = fieldGet(field, instance, OpConstants.GetOp.DEFAULT); - fieldPutMh(field, instance, convertedVal, lookup); - return oldVal; - } else { - // field is non-final, can use var handles - var handle = lookup.unreflectVarHandle(field); - return isStatic ? handle.getAndSet(convertedVal) : handle.getAndSet(instance, convertedVal); - } - } - - /** - * Gets the current value of the given field in the given instance and adds the given value to it. This method does - * not guarantee the requested memory semantics if the field is final. - * - * @param kind the type kind of the given field. - * @param field the field to get and add to. - * @param instance the instance to get the value from, possibly null. - * @param value the value to add to the current field value. - * @return the old value of the given field in the given instance, possibly null. - * @throws NullPointerException if the given type kind or field is null. - * @throws Throwable if an unexpected exception occurs while reading or writing the field value. - */ - // NOTE: kind can only be INT or LONG - @SuppressWarnings("ConstantConditions") - static @Nullable Object fieldGetAdd( - @NonNull ValueTypeKind kind, - @NonNull Field field, - @Nullable Object instance, - @Nullable Object value - ) throws Throwable { - var convertedVal = convertFieldValue(value, field.getType(), kind); - var lookup = OpConstants.TRUSTED_LOOKUP.get(); - var isStatic = Modifier.isStatic(field.getModifiers()); - if (Modifier.isFinal(field.getModifiers())) { - // the field is final, cannot use var handles, fall back to default impl - // field and converted value has to be a primitive at this point, so casting is safe - var oldVal = (Number) fieldGet(field, instance, OpConstants.GetOp.DEFAULT); - var valueSum = ((Number) convertedVal).longValue() + oldVal.longValue(); - var fieldVal = convertFieldValue(valueSum, field.getType(), kind); - fieldPutMh(field, instance, fieldVal, lookup); - return oldVal; - } else { - // field is non-final, can use var handles - var handle = lookup.unreflectVarHandle(field); - return isStatic ? handle.getAndAdd(convertedVal) : handle.getAndAdd(instance, convertedVal); - } - } - - /** - * Sets the value of the given field in case the current value of the field is equal to the given expected value. This - * method does not guarantee the requested memory semantics if the field is final. - * - * @param kind the type kind of the given field. - * @param field the field to compare and swap the value of. - * @param instance the instance that contains the field to compare and swap the value of, possibly null. - * @param expected the expected value of the field, possibly null. - * @param value the value to set into the given field, possibly null. - * @return true if the field was successfully set, false otherwise. - * @throws NullPointerException if the given type kind or field is null. - * @throws Throwable if an unexpected exception occurs while reading or writing the field value. - */ - static boolean fieldComparePut( - @NonNull ValueTypeKind kind, - @NonNull Field field, - @Nullable Object instance, - @Nullable Object expected, - @Nullable Object value - ) throws Throwable { - var convertedVal = convertFieldValue(value, field.getType(), kind); - var convertedExp = convertFieldValue(expected, field.getType(), kind); - var lookup = OpConstants.TRUSTED_LOOKUP.get(); - var isStatic = Modifier.isStatic(field.getModifiers()); - if (Modifier.isFinal(field.getModifiers())) { - // the field is final, cannot use var handles, fall back to default impl - var witness = fieldGet(field, instance, OpConstants.GetOp.DEFAULT); - var witnessIsExpected = kind.areValuesEqual(witness, convertedExp); - if (witnessIsExpected) { - fieldPutMh(field, instance, convertedVal, lookup); - return true; - } - return false; - } else { - // field is non-final, can use var handles - var handle = lookup.unreflectVarHandle(field); - return isStatic - ? handle.compareAndSet(convertedExp, convertedVal) - : handle.compareAndSet(instance, convertedExp, convertedVal); - } - } -} diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDefiner.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDefiner.java index da70f6150c..e52355305b 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDefiner.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDefiner.java @@ -42,8 +42,8 @@ final class UnsafeReplacementDefiner { // classes that should be appended to the bootstrap class path private static final Set BOOTSTRAP_CLASS_PREFIXES = Set.of( "ArrayOps", + "FieldAccessor", "FieldOffsetOps", - "FieldOps", "LazyMemoizingSupplier", "MemoryControlOps", "MemoryOps", diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegate.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegate.java index 4784867272..72be87dff3 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegate.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegate.java @@ -303,8 +303,8 @@ public static Object unsafeStaticFieldBase(Field f) { } // requested read of static or instance field - var field = FieldOffsetOps.fieldFromOffset(object, offset); - return field == null ? null : FieldOps.fieldGet(field, object, op); + var fieldAccessor = FieldOffsetOps.fieldFromOffset(object, offset); + return fieldAccessor == null ? null : fieldAccessor.get(object, op); } catch (Throwable throwable) { UnsafeLogUtil.debug("Unable to unsafe get: [obj={}, offset={}, op={}]", object, offset, op, throwable); return null; @@ -593,10 +593,9 @@ private static void unsafePut( } // requested read of static or instance field - var field = FieldOffsetOps.fieldFromOffset(object, offset); - if (field != null) { - var typeKind = ValueTypeKind.of(field.getType()); - FieldOps.fieldPut(typeKind, field, object, value, op); + var fieldAccessor = FieldOffsetOps.fieldFromOffset(object, offset); + if (fieldAccessor != null) { + fieldAccessor.put(object, value, op); } } catch (Throwable throwable) { UnsafeLogUtil.debug( @@ -862,10 +861,9 @@ private static boolean unsafeCas( } // requested read of static or instance field - var field = FieldOffsetOps.fieldFromOffset(object, offset); - if (field != null) { - var typeKind = ValueTypeKind.of(field.getType()); - return FieldOps.fieldComparePut(typeKind, field, object, expected, value); + var fieldAccessor = FieldOffsetOps.fieldFromOffset(object, offset); + if (fieldAccessor != null) { + return fieldAccessor.compareAndSet(object, expected, value); } return false; @@ -920,10 +918,9 @@ public static boolean unsafeCasObject(Object object, long offset, Object expecte } // requested read of static or instance field - var field = FieldOffsetOps.fieldFromOffset(object, offset); - if (field != null) { - var typeKind = ValueTypeKind.of(field.getType()); - return FieldOps.fieldGetAdd(typeKind, field, object, delta); + var fieldAccessor = FieldOffsetOps.fieldFromOffset(object, offset); + if (fieldAccessor != null) { + return fieldAccessor.getAndAdd(object, delta); } return null; @@ -975,10 +972,9 @@ public static long unsafeGetAndAddLong(Object object, long offset, long delta) { } // requested read of static or instance field - var field = FieldOffsetOps.fieldFromOffset(object, offset); - if (field != null) { - var typeKind = ValueTypeKind.of(field.getType()); - return FieldOps.fieldGetPut(typeKind, field, object, value); + var fieldAccessor = FieldOffsetOps.fieldFromOffset(object, offset); + if (fieldAccessor != null) { + return fieldAccessor.getAndPut(object, value); } return null; diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java index b4d776e1f8..9cc28fc79a 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java @@ -131,6 +131,11 @@ enum ValueTypeKind { * @throws NullPointerException if the given type is null. */ static @NonNull ValueTypeKind of(@NonNull Class type) { + if (!type.isPrimitive()) { + // optimization to prevent expensive string operations in Class#descriptorString() + return REF; + } + return switch (type.descriptorString()) { case "B" -> BYTE; case "Z" -> BOOL; @@ -140,7 +145,7 @@ enum ValueTypeKind { case "J" -> LONG; case "F" -> FLOAT; case "D" -> DOUBLE; - default -> REF; + default -> throw new AssertionError(); }; } diff --git a/wrapper-jvm/impl/src/test/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegateTest.java b/wrapper-jvm/impl/src/test/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegateTest.java index 21c13194cf..52bed880b5 100644 --- a/wrapper-jvm/impl/src/test/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegateTest.java +++ b/wrapper-jvm/impl/src/test/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegateTest.java @@ -349,8 +349,9 @@ void testEachFieldInClassHasDifferentOffset() { case true -> UnsafeReplacementDelegate.unsafeStaticFieldBase(field); case false -> inst; }; - var resolvedField = FieldOffsetOps.fieldFromOffset(base, offset); - Assertions.assertEquals(field, resolvedField); + var resolved = FieldOffsetOps.fieldFromOffset(base, offset); + Assertions.assertNotNull(resolved); + Assertions.assertEquals(field, resolved.wrappedField()); } } @@ -376,8 +377,9 @@ void testEachFieldInClassHierarchyHasDifferentOffset() { case true -> UnsafeReplacementDelegate.unsafeStaticFieldBase(field); case false -> inst; }; - var resolvedField = FieldOffsetOps.fieldFromOffset(base, offset); - Assertions.assertEquals(field, resolvedField); + var resolved = FieldOffsetOps.fieldFromOffset(base, offset); + Assertions.assertNotNull(resolved); + Assertions.assertEquals(field, resolved.wrappedField()); } } while ((current = current.getSuperclass()) != null); } From 60b85abe80c805f144520d0ee12cc77545a60068 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Tue, 2 Sep 2025 14:05:17 +0200 Subject: [PATCH 2/3] cleanup, more optimizations --- .../impl/transform/unsafe/FieldAccessor.java | 17 ++- .../impl/transform/unsafe/FieldOffsetOps.java | 130 +++++++++++++----- .../impl/transform/unsafe/ValueTypeKind.java | 41 ++++-- 3 files changed, 139 insertions(+), 49 deletions(-) diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java index 814f4cb11f..acc4a97690 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldAccessor.java @@ -28,6 +28,8 @@ import org.jetbrains.annotations.VisibleForTesting; /** + * An accessor for a field in a class. + * * @since 4.0 */ final class FieldAccessor { @@ -40,8 +42,11 @@ final class FieldAccessor { private final MethodHandle putMethodHandle; // null if VarHandle can support set (field is not trusted final) /** + * Constructs a new field accessor from the given field. * - * @param field + * @param field the field to construct the accessor for. + * @throws NullPointerException if the given field is null. + * @throws ReflectiveOperationException if an exception occurs while getting the required reflective values. */ private FieldAccessor(@NonNull Field field) throws ReflectiveOperationException { this.fieldType = field.getType(); @@ -74,9 +79,11 @@ private FieldAccessor(@NonNull Field field) throws ReflectiveOperationException } /** + * Constructs a new field accessor from the given field. * - * @param field - * @return + * @param field the field to construct the accessor for. + * @throws NullPointerException if the given field is null. + * @throws IllegalStateException if an exception occurs while getting the required reflective values. */ public static @NonNull FieldAccessor make(@NonNull Field field) { try { @@ -287,8 +294,10 @@ public boolean compareAndSet( } /** + * Get the field that was used to create this accessor. Note that the return value might be null in case the field was + * garbage collected. Only intended for use in tests. * - * @return + * @return the field that was used to create this accessor. */ @VisibleForTesting public @Nullable Field wrappedField() { diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java index 060a7e3f61..f2b5f2b99c 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java @@ -16,11 +16,12 @@ package eu.cloudnetservice.wrapper.impl.transform.unsafe; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Arrays; import lombok.NonNull; import org.jetbrains.annotations.Nullable; @@ -32,7 +33,25 @@ final class FieldOffsetOps { private static final int FIELD_COUNT_IN_CLASS = Class.class.getDeclaredFields().length; - private static final Map FIELD_LOOKUP_CACHE = new ConcurrentHashMap<>(); + private static final ClassValue CLASS_FIELD_CACHE = new ClassValue<>() { + @Override + protected @NonNull PerClassFieldCache computeValue(@NonNull Class type) { + return new PerClassFieldCache(); + } + }; + + private static final VarHandle SLOT_CACHE; // access to PerClassFieldCache.accessorsPerFieldSlot + private static final VarHandle SLOT_ELEMENT; // access to elements in PerClassFieldCache.accessorsPerFieldSlot + + static { + try { + var lookup = MethodHandles.lookup(); + SLOT_CACHE = lookup.findVarHandle(PerClassFieldCache.class, "accessorsPerFieldSlot", FieldAccessor[].class); + SLOT_ELEMENT = MethodHandles.arrayElementVarHandle(FieldAccessor[].class); + } catch (NoSuchFieldException | IllegalAccessException exception) { + throw new ExceptionInInitializerError(exception); + } + } private FieldOffsetOps() { throw new UnsupportedOperationException(); @@ -107,52 +126,97 @@ private static long fieldSlot(@NonNull Field field) { * @throws NullPointerException if the given base is null. */ public static @Nullable FieldAccessor fieldFromOffset(@NonNull Object base, long offset) { - if (offset < 0) { - // bail out early, a field with a negative offset cannot exist + var offsetAsInt = (int) offset; + if (offset < 0 || offset != offsetAsInt) { + // bail out early, a field with such an index is impossible return null; } var clazz = base instanceof Class c ? c : base.getClass(); - var cacheKey = new FieldCacheKey(clazz, offset); - return FIELD_LOOKUP_CACHE.computeIfAbsent(cacheKey, _ -> { - var classHierarchy = new ArrayList>(); - for (var c = clazz; c != null; c = c.getSuperclass()) { - if (base instanceof Class && c == Object.class) { - // special case handling: if a class was provided as the instance to operate - // on, this can have 2 meanings: either the caller requests a static field or - // an instance field in the class "Class" which does usually not appear in - // instance.getDeclaredFields() [which is intended as the fields in Class are - // not inherited into whatever the given class is when looking though the - // reflection api] - we therefore need to insert the class "Class" into the - // hierarchy manually here to allow finding fields in there too - classHierarchy.add(Class.class); + var perClassCache = CLASS_FIELD_CACHE.get(clazz); + for (; ; ) { + var slots = perClassCache.accessorsPerFieldSlot; + if (offsetAsInt < slots.length) { + // check if the accessor was already computed and is cached + var accessor = (FieldAccessor) SLOT_ELEMENT.getVolatile(slots, offsetAsInt); + if (accessor != null) { + return accessor; + } + + // accessor is not yet cached, insert into cache + var resolvedAccessor = resolveFieldAccessor(clazz, base, offsetAsInt); + var didInsert = SLOT_ELEMENT.compareAndSet(slots, offsetAsInt, null, resolvedAccessor); + if (didInsert) { + return resolvedAccessor; } - classHierarchy.add(c); + // another thread did insert already, read it again + return (FieldAccessor) SLOT_ELEMENT.getVolatile(slots, offsetAsInt); } - var remaining = offset; - for (var c : classHierarchy.reversed()) { - var fields = c.getDeclaredFields(); - if (remaining < fields.length) { - var field = fields[(int) remaining]; - return FieldAccessor.make(field); - } + // slow path: expand array to insert new accessor into slot, then continue the loop + var newArrayLength = Math.max(offsetAsInt + 1, Math.max(8, slots.length << 1)); + var resizedSlots = Arrays.copyOf(slots, newArrayLength); + SLOT_CACHE.compareAndSet(perClassCache, slots, resizedSlots); + } + } - remaining -= fields.length; + /** + * Resolves a field accessor for the field in the given class with the given field offset. + * + * @param clazz the class to find the field in. + * @param base the field base to get the field based of. + * @param offset the offset of the field to find. + * @return an accessor for the field in the given class at the given offset. + * @throws NullPointerException if the given class or base is null. + * @throws IllegalArgumentException if an invalid offset was given. + */ + private static @NonNull FieldAccessor resolveFieldAccessor( + @NonNull Class clazz, + @NonNull Object base, + int offset + ) { + var classHierarchy = new ArrayList>(); + for (var c = clazz; c != null; c = c.getSuperclass()) { + if (base instanceof Class && c == Object.class) { + // special case handling: if a class was provided as the instance to operate + // on, this can have 2 meanings: either the caller requests a static field or + // an instance field in the class "Class" which does usually not appear in + // instance.getDeclaredFields() [which is intended as the fields in Class are + // not inherited into whatever the given class is when looking though the + // reflection api] - we therefore need to insert the class "Class" into the + // hierarchy manually here to allow finding fields in there too + classHierarchy.add(Class.class); } - return null; - }); + classHierarchy.add(c); + } + + var remaining = offset; + for (var c : classHierarchy.reversed()) { + var fields = c.getDeclaredFields(); + if (remaining < fields.length) { + var field = fields[remaining]; + return FieldAccessor.make(field); + } + + remaining -= fields.length; + } + + throw new IllegalArgumentException("caller provided invalid field offset"); } /** - * A cache key for a resolved field based on the base class and field offset. + * Cache for field accessors within a specific class. * - * @param clazz the class in which the field was resolved. - * @param offset the offset of the field that was resolved. + * @since 4.0 */ - private record FieldCacheKey(@NonNull Class clazz, long offset) { + private static final class PerClassFieldCache { + /** + * Array mapping of field slot to field accessor. + */ + @SuppressWarnings("FieldMayBeFinal") // modified via VarHandle + private volatile FieldAccessor[] accessorsPerFieldSlot = new FieldAccessor[0]; } } diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java index 9cc28fc79a..5d8ab40790 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/ValueTypeKind.java @@ -130,23 +130,40 @@ enum ValueTypeKind { * @return the value type kind for the given type. * @throws NullPointerException if the given type is null. */ + // IMPL NOTE: very hot path, readability doesn't really matter here static @NonNull ValueTypeKind of(@NonNull Class type) { if (!type.isPrimitive()) { - // optimization to prevent expensive string operations in Class#descriptorString() + // optimization to prevent the comparisons below return REF; } - return switch (type.descriptorString()) { - case "B" -> BYTE; - case "Z" -> BOOL; - case "C" -> CHAR; - case "S" -> SHORT; - case "I" -> INT; - case "J" -> LONG; - case "F" -> FLOAT; - case "D" -> DOUBLE; - default -> throw new AssertionError(); - }; + if (type == byte.class) { + return BYTE; + } + if (type == boolean.class) { + return BOOL; + } + if (type == char.class) { + return CHAR; + } + if (type == short.class) { + return SHORT; + } + if (type == int.class) { + return INT; + } + if (type == long.class) { + return LONG; + } + if (type == float.class) { + return FLOAT; + } + if (type == double.class) { + return DOUBLE; + } + + // can only reach here when called with void which should not happen + throw new AssertionError(); } /** From 650a8607396b0cd726ce1fd9bd3d772b863a11e0 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Wed, 3 Sep 2025 10:14:23 +0200 Subject: [PATCH 3/3] simplify using CAS --- .../wrapper/impl/transform/unsafe/FieldOffsetOps.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java index f2b5f2b99c..d9b25272b8 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/FieldOffsetOps.java @@ -144,14 +144,9 @@ private static long fieldSlot(@NonNull Field field) { } // accessor is not yet cached, insert into cache - var resolvedAccessor = resolveFieldAccessor(clazz, base, offsetAsInt); - var didInsert = SLOT_ELEMENT.compareAndSet(slots, offsetAsInt, null, resolvedAccessor); - if (didInsert) { - return resolvedAccessor; - } - - // another thread did insert already, read it again - return (FieldAccessor) SLOT_ELEMENT.getVolatile(slots, offsetAsInt); + var newAccessor = resolveFieldAccessor(clazz, base, offsetAsInt); + var storedAccessor = (FieldAccessor) SLOT_ELEMENT.compareAndExchange(slots, offsetAsInt, null, newAccessor); + return storedAccessor != null ? storedAccessor : newAccessor; } // slow path: expand array to insert new accessor into slot, then continue the loop