Skip to content

Commit ace66c8

Browse files
committed
use method handles for entire direct buffer cleaning process
1 parent d28c6fb commit ace66c8

2 files changed

Lines changed: 69 additions & 52 deletions

File tree

wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegate.java

Lines changed: 45 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
import java.util.concurrent.atomic.AtomicLong;
3434
import java.util.concurrent.locks.LockSupport;
3535
import java.util.function.Consumer;
36-
import java.util.function.Function;
3736
import java.util.function.Supplier;
3837
import lombok.NonNull;
3938
import org.jetbrains.annotations.Nullable;
39+
import org.jetbrains.annotations.VisibleForTesting;
4040

4141
/**
4242
* Delegate class that holds current replacements for fields/methods defined in {@code sun.misc.Unsafe}.
@@ -50,15 +50,18 @@
5050
@Deprecated
5151
public final class UnsafeReplacementDelegate {
5252

53+
// accessor for cleaning up direct byte buffers
54+
@VisibleForTesting
55+
static final Consumer<ByteBuffer> BB_CLEANER_NOOP = _ -> {
56+
};
57+
@VisibleForTesting
58+
static final Supplier<Consumer<ByteBuffer>> BB_CLEANER = createByteBufferCleaner();
59+
5360
// counter for handing out field offsets; mapping for handed-out offsets to their actual field
5461
private static final Map<Long, Field> FIELD_OFFSET_TO_FIELD_LOOKUP = new ConcurrentHashMap<>();
5562
private static final AtomicLong FIELD_OFFSET_COUNTER =
5663
new AtomicLong(ThreadLocalRandom.current().nextInt(Short.MAX_VALUE, Integer.MAX_VALUE));
5764

58-
// accessors for cleaning up direct byte buffers
59-
private static final Supplier<Consumer<ByteBuffer>> BB_CLEANER = createByteBufferCleaner();
60-
private static final Supplier<Function<ByteBuffer, Object>> BB_ATTACHMENT_GETTER = createByteBufferAttachmentGetter();
61-
6265
// accessor for the operating system mx bean
6366
private static final Supplier<OperatingSystemMXBean> OS_MX_BEAN =
6467
new LazyMemoizingSupplier<>(ManagementFactory::getOperatingSystemMXBean);
@@ -102,35 +105,6 @@ private UnsafeReplacementDelegate() {
102105
});
103106
}
104107

105-
/**
106-
* Get a supplier that creates a function to resolve the attachment of a direct byte buffer. The function is only
107-
* created once on the first initialization of the supplier.
108-
*
109-
* @return a supplier that creates a function to resolve the attachment of a direct byte buffer.
110-
*/
111-
private static @NonNull Supplier<Function<ByteBuffer, Object>> createByteBufferAttachmentGetter() {
112-
return new LazyMemoizingSupplier<>(() -> {
113-
try {
114-
var lookup = OpConstants.TRUSTED_LOOKUP.get();
115-
var directBufferClass = Class.forName("sun.nio.ch.DirectBuffer");
116-
var attachementMethod = directBufferClass.getMethod("attachment");
117-
var attachmentMethodHandle = MethodHandles.explicitCastArguments(
118-
lookup.unreflect(attachementMethod),
119-
MethodType.methodType(Object.class, ByteBuffer.class));
120-
return buffer -> {
121-
try {
122-
return attachmentMethodHandle.invokeExact(buffer);
123-
} catch (Throwable _) {
124-
return null;
125-
}
126-
};
127-
} catch (Throwable throwable) {
128-
UnsafeLogUtil.debug("Unable to access byte buffer attachment method; assuming it's never attached", throwable);
129-
return _ -> null;
130-
}
131-
});
132-
}
133-
134108
/**
135109
* Get a supplier that creates a consumer to clean a direct byte buffer. The consumer is only created once on the
136110
* first initialization of the supplier.
@@ -142,31 +116,60 @@ private UnsafeReplacementDelegate() {
142116
try {
143117
var lookup = OpConstants.TRUSTED_LOOKUP.get();
144118

145-
// get the method handle to get the cleaner of the provided byte buffer (type: (ByteBuffer): Cleaner)
119+
// get the method handle to get the cleaner of the provided byte buffer (type: (ByteBuffer):Cleaner)
146120
var directBufferClass = Class.forName("sun.nio.ch.DirectBuffer");
147121
var cleanerMethod = directBufferClass.getDeclaredMethod("cleaner");
148122
var cleanerHandle = MethodHandles.explicitCastArguments(
149123
lookup.unreflect(cleanerMethod),
150124
MethodType.methodType(cleanerMethod.getReturnType(), ByteBuffer.class));
151125

152-
// get the method handle to invoke the clean method on the Cleaner class (type: (Cleaner): void)
126+
// get the method handle to invoke the clean method on the Cleaner class (type: (Cleaner):void)
153127
var cleanerClass = Class.forName("jdk.internal.ref.Cleaner");
154128
var cleanMethod = cleanerClass.getDeclaredMethod("clean");
155129
var cleanHandle = lookup.unreflect(cleanMethod);
156130

157-
// adapt the clean() method handle by pre-processing it with the result of the cleaner retrieval method handle
158-
// this results in a chained invocation like clean(buffer.cleaner())
159-
var cleanBufferHandle = MethodHandles.filterArguments(cleanHandle, 0, cleanerHandle);
131+
// adapt the clean() method handle by pre-processing it with the result of the cleaner retrieval
132+
// method handle, this results in a chained invocation (type: (ByteBuffer):void)
133+
var cleanBufferHandle = MethodHandles.filterReturnValue(cleanerHandle, cleanHandle);
134+
135+
// get a method handle to check if a buffer has an attachment (type: (ByteBuffer):boolean)
136+
var attachementMethod = directBufferClass.getMethod("attachment");
137+
var attachmentHandle = MethodHandles.explicitCastArguments(
138+
lookup.unreflect(attachementMethod),
139+
MethodType.methodType(attachementMethod.getReturnType(), ByteBuffer.class));
140+
var isNullHandle = lookup.findStatic(
141+
Objects.class,
142+
"isNull",
143+
MethodType.methodType(boolean.class, Object.class));
144+
var isAttachmentNullHandle = MethodHandles.collectArguments(isNullHandle, 0, attachmentHandle);
145+
146+
// get a method handle that throws an IAE with the message 'duplicate or slice' (type: ():void)
147+
// this handle needs to then be adapted to add an extra, ignored ByteBuffer param (type: (ByteBuffer):void)
148+
var iaeMessageCtrHandle = lookup.findConstructor(
149+
IllegalArgumentException.class,
150+
MethodType.methodType(void.class, String.class));
151+
var dupOrSliceIaeCtrHandle = MethodHandles.insertArguments(iaeMessageCtrHandle, 0, "duplicate or slice");
152+
var throwIaeHandle = MethodHandles.throwException(void.class, IllegalArgumentException.class);
153+
var throwDupOrSliceHandle = MethodHandles.collectArguments(throwIaeHandle, 0, dupOrSliceIaeCtrHandle);
154+
var throwDupOrSliceHandleWithBBArg = MethodHandles.dropArguments(throwDupOrSliceHandle, 0, ByteBuffer.class);
155+
156+
// construct a method handle that conditionally invokes 'bb.cleaner().clean()' or throws an
157+
// IAE depending on the fact if the provided ByteBuffer has an attachment or not
158+
var cleanIfNotAttachmentHandle = MethodHandles.guardWithTest(
159+
isAttachmentNullHandle,
160+
cleanBufferHandle,
161+
throwDupOrSliceHandleWithBBArg);
160162
return buffer -> {
161163
try {
162-
cleanBufferHandle.invokeExact(buffer);
164+
cleanIfNotAttachmentHandle.invokeExact(buffer);
165+
} catch (IllegalArgumentException exception) {
166+
throw exception;
163167
} catch (Throwable _) {
164168
}
165169
};
166170
} catch (Throwable throwable) {
167171
UnsafeLogUtil.debug("Unable to access byte buffer cleaning methods; falling back to no cleaning", throwable);
168-
return _ -> { // unable to clean direct buffers
169-
};
172+
return BB_CLEANER_NOOP; // unable to clean direct buffers
170173
}
171174
});
172175
}
@@ -1275,12 +1278,6 @@ public static void unsafeInvokeCleaner(ByteBuffer buffer) {
12751278
throw new IllegalArgumentException("buffer is non-direct"); // mimics current behavior
12761279
}
12771280

1278-
var attachmentGetter = BB_ATTACHMENT_GETTER.get();
1279-
var attachment = attachmentGetter.apply(buffer);
1280-
if (attachment != null) {
1281-
throw new IllegalArgumentException("duplicate or slice"); // mimics current behavior
1282-
}
1283-
12841281
var cleanerInvoker = BB_CLEANER.get();
12851282
cleanerInvoker.accept(buffer);
12861283
}

wrapper-jvm/impl/src/test/java/eu/cloudnetservice/wrapper/impl/transform/unsafe/UnsafeReplacementDelegateTest.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -826,10 +826,30 @@ void testDefineClass() {
826826
@Test
827827
void testInvokeCleaner() {
828828
// can't really test if it worked, but can at least test if the method handle init works
829-
Assertions.assertThrows(
830-
IllegalArgumentException.class,
831-
() -> UnsafeReplacementDelegate.unsafeInvokeCleaner(ByteBuffer.allocate(1)));
832-
Assertions.assertDoesNotThrow(() -> UnsafeReplacementDelegate.unsafeInvokeCleaner(ByteBuffer.allocateDirect(1)));
829+
{
830+
var buffer = ByteBuffer.allocate(1);
831+
var thrown = Assertions.assertThrows(
832+
IllegalArgumentException.class,
833+
() -> UnsafeReplacementDelegate.unsafeInvokeCleaner(buffer));
834+
Assertions.assertNotNull(thrown.getMessage());
835+
Assertions.assertEquals("buffer is non-direct", thrown.getMessage());
836+
}
837+
838+
{
839+
var buffer = ByteBuffer.allocateDirect(5);
840+
var bufferSlice = buffer.slice(2, 2);
841+
var thrown = Assertions.assertThrows(
842+
IllegalArgumentException.class,
843+
() -> UnsafeReplacementDelegate.unsafeInvokeCleaner(bufferSlice));
844+
Assertions.assertNotNull(thrown.getMessage());
845+
Assertions.assertEquals("duplicate or slice", thrown.getMessage());
846+
Assertions.assertDoesNotThrow(() -> UnsafeReplacementDelegate.unsafeInvokeCleaner(buffer));
847+
}
848+
849+
{
850+
var cleanerConsumer = UnsafeReplacementDelegate.BB_CLEANER.get();
851+
Assertions.assertNotSame(UnsafeReplacementDelegate.BB_CLEANER_NOOP, cleanerConsumer);
852+
}
833853
}
834854

835855
@Test

0 commit comments

Comments
 (0)