1 /*
2 * Copyright 2025 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.util.internal;
17
18 import io.netty.util.internal.logging.InternalLogger;
19 import io.netty.util.internal.logging.InternalLoggerFactory;
20
21 import java.lang.invoke.MethodHandle;
22 import java.lang.invoke.MethodHandles;
23 import java.nio.ByteBuffer;
24
25 import static java.lang.invoke.MethodType.methodType;
26
27 /**
28 * Provide a way to clean direct {@link ByteBuffer} instances on Java 24+,
29 * where we don't have {@code Unsafe} available, but we have memory segments.
30 */
31 final class CleanerJava25 implements Cleaner {
32 private static final InternalLogger logger;
33
34 private static final MethodHandle INVOKE_ALLOCATOR;
35
36 static {
37 boolean suitableJavaVersion;
38 if (System.getProperty("org.graalvm.nativeimage.imagecode") != null) {
39 // native image supports this since 25, but we don't use PlatformDependent0 here, since
40 // we need to initialize CleanerJava25 at build time.
41 String v = System.getProperty("java.specification.version");
42 try {
43 suitableJavaVersion = Integer.parseInt(v) >= 25;
44 } catch (NumberFormatException e) {
45 suitableJavaVersion = false;
46 }
47 // also need to prevent initializing the logger at build time
48 logger = null;
49 } else {
50 // Only attempt to use MemorySegments on Java 25 or greater, because of the following JDK bugs:
51 // - https://bugs.openjdk.org/browse/JDK-8357145
52 // - https://bugs.openjdk.org/browse/JDK-8357268
53 suitableJavaVersion = PlatformDependent0.javaVersion() >= 25;
54 logger = InternalLoggerFactory.getInstance(CleanerJava25.class);
55 }
56
57 MethodHandle method;
58 Throwable error;
59 if (suitableJavaVersion) {
60 try {
61 // Here we compose and construct a MethodHandle that takes an 'int' capacity argument,
62 // and produces a 'CleanableDirectBufferImpl' instance.
63 // The method handle will create a new shared Arena instance, allocate a MemorySegment from it,
64 // convert the MemorySegment to a ByteBuffer and a memory address, and then pass both the Arena,
65 // the ByteBuffer, and the memory address to the CleanableDirectBufferImpl constructor,
66 // returning the resulting object.
67 //
68 // Effectively, we are recreating the following the Java code through MethodHandles alone:
69 //
70 // Arena arena = Arena.ofShared();
71 // MemorySegment segment = arena.allocate(size);
72 // return new CleanableDirectBufferImpl(
73 // (AutoCloseable) arena,
74 // segment.asByteBuffer(),
75 // segment.address());
76 //
77 // First, we need the types we'll use to set this all up.
78 Class<?> arenaCls = Class.forName("java.lang.foreign.Arena");
79 Class<?> memsegCls = Class.forName("java.lang.foreign.MemorySegment");
80 Class<CleanableDirectBufferImpl> bufCls = CleanableDirectBufferImpl.class;
81 // Acquire the private look up, so we can access the package-private 'CleanableDirectBufferImpl'
82 // constructor.
83 MethodHandles.Lookup lookup = MethodHandles.lookup();
84
85 // ofShared.type() = ()Arena
86 MethodHandle ofShared = lookup.findStatic(arenaCls, "ofShared", methodType(arenaCls));
87 // allocate.type() = (Arena,long)MemorySegment
88 MethodHandle allocate = lookup.findVirtual(arenaCls, "allocate", methodType(memsegCls, long.class));
89 // asByteBuffer.type() = (MemorySegment)ByteBuffer
90 MethodHandle asByteBuffer = lookup.findVirtual(memsegCls, "asByteBuffer", methodType(ByteBuffer.class));
91 // address.type() = (MemorySegment)long
92 MethodHandle address = lookup.findVirtual(memsegCls, "address", methodType(long.class));
93 // bufClsCtor.type() = (AutoCloseable,ByteBuffer,long)CleanableDirectBufferImpl
94 MethodHandle bufClsCtor = lookup.findConstructor(bufCls,
95 methodType(void.class, AutoCloseable.class, ByteBuffer.class, long.class));
96 // The 'allocate' method takes a 'long' capacity, but we'll be providing an 'int'.
97 // Explicitly cast the 'long' to 'int' so we can use 'invokeExact'.
98 // allocateInt.type() = (Arena,int)MemorySegment
99 MethodHandle allocateInt = MethodHandles.explicitCastArguments(allocate,
100 methodType(memsegCls, arenaCls, int.class));
101 // Use the 'asByteBuffer' and 'address' methods as a filter, to transform the constructor into a method
102 // that takes two MemorySegment arguments instead of a ByteBuffer and a long argument.
103 // ctorArenaMemsegMemseg.type() = (Arena,MemorySegment,MemorySegment)CleanableDirectBufferImpl
104 MethodHandle ctorArenaMemsegMemseg = MethodHandles.explicitCastArguments(
105 MethodHandles.filterArguments(bufClsCtor, 1, asByteBuffer, address),
106 methodType(bufCls, arenaCls, memsegCls, memsegCls));
107 // Our method now takes two MemorySegment arguments, but we actually only want to pass one.
108 // Specifically, we want to get both the ByteBuffer and the memory address from the same MemorySegment
109 // instance.
110 // We permute the argument array such that the first MemorySegment argument gest passed to both
111 // parameters, and then the second parameter value gets ignored.
112 // ctorArenaMemsegNull.type() = (Arena,MemorySegment,MemorySegment)CleanableDirectBufferImpl
113 MethodHandle ctorArenaMemsegNull = MethodHandles.permuteArguments(ctorArenaMemsegMemseg,
114 methodType(bufCls, arenaCls, memsegCls, memsegCls), 0, 1, 1);
115 // With the second MemorySegment argument ignored, we can statically bind it to 'null' to effectively
116 // drop it from our parameter list.
117 MethodHandle ctorArenaMemseg = MethodHandles.insertArguments(
118 ctorArenaMemsegNull, 2, new Object[]{null});
119 // Use the 'allocateInt' method to transform the last MemorySegment argument of the constructor,
120 // into an (Arena,int) argument pair.
121 // ctorArenaArenaInt.type() = (Arena,Arena,int)CleanableDirectBufferImpl
122 MethodHandle ctorArenaArenaInt = MethodHandles.collectArguments(ctorArenaMemseg, 1, allocateInt);
123 // Our method now takes two Arena arguments, but we actually only want to pass one. Specifically, it's
124 // very important that it's the same arena we use for both allocation and deallocation.
125 // We permute the argument array such that the first Arena argument gets passed to both parameters,
126 // and the second parameter value gets ignored.
127 // ctorArenaNullInt.type() = (Arena,Arena,int)CleanableDirectBufferImpl
128 MethodHandle ctorArenaNullInt = MethodHandles.permuteArguments(ctorArenaArenaInt,
129 methodType(bufCls, arenaCls, arenaCls, int.class), 0, 0, 2);
130 // With the second Arena parameter value ignored, we can statically bind it to 'null' to effectively
131 // drop it from our parameter list.
132 // ctorArenaInt.type() = (Arena,int)CleanableDirectBufferImpl
133 MethodHandle ctorArenaInt = MethodHandles.insertArguments(ctorArenaNullInt, 1, new Object[]{null});
134 // Now we just need to create our Arena instance. We fold the Arena parameter into the 'ofShared'
135 // static method, so we effectively bind the argument to the result of calling that method.
136 // Since 'ofShared' takes no further parameters, we effectively eliminate the first parameter.
137 // This creates our method handle that takes an 'int' and returns a 'CleanableDirectBufferImpl'.
138 // ctorInt.type() = (int)CleanableDirectBufferImpl
139 method = MethodHandles.foldArguments(ctorArenaInt, ofShared);
140 error = null;
141 } catch (Throwable throwable) {
142 method = null;
143 error = throwable;
144 }
145 } else {
146 method = null;
147 error = new UnsupportedOperationException("java.lang.foreign.MemorySegment unavailable");
148 }
149 if (logger != null) {
150 if (error == null) {
151 logger.debug("java.nio.ByteBuffer.cleaner(): available");
152 } else {
153 logger.debug("java.nio.ByteBuffer.cleaner(): unavailable", error);
154 }
155 }
156 INVOKE_ALLOCATOR = method;
157 }
158
159 static boolean isSupported() {
160 return INVOKE_ALLOCATOR != null;
161 }
162
163 @SuppressWarnings("OverlyStrongTypeCast") // The cast is needed for 'invokeExact' semantics.
164 @Override
165 public CleanableDirectBuffer allocate(int capacity) {
166 try {
167 return (CleanableDirectBufferImpl) INVOKE_ALLOCATOR.invokeExact(capacity);
168 } catch (RuntimeException e) {
169 throw e; // Propagate the runtime exceptions that the Arena would normally throw.
170 } catch (Throwable e) {
171 throw new IllegalStateException("Unexpected allocation exception", e);
172 }
173 }
174
175 @Override
176 public void freeDirectBuffer(ByteBuffer buffer) {
177 throw new UnsupportedOperationException("Cannot clean arbitrary ByteBuffer instances");
178 }
179
180 private static final class CleanableDirectBufferImpl implements CleanableDirectBuffer {
181 private final AutoCloseable closeable;
182 private final ByteBuffer buffer;
183 private final long memoryAddress;
184
185 // NOTE: must be at least package-protected to allow calls from the method handles!
186 CleanableDirectBufferImpl(AutoCloseable closeable, ByteBuffer buffer, long memoryAddress) {
187 this.closeable = closeable;
188 this.buffer = buffer;
189 this.memoryAddress = memoryAddress;
190 }
191
192 @Override
193 public ByteBuffer buffer() {
194 return buffer;
195 }
196
197 @Override
198 public void clean() {
199 try {
200 closeable.close();
201 } catch (RuntimeException e) {
202 throw e; // Propagate the runtime exceptions that Arena would normally throw.
203 } catch (Exception e) {
204 throw new IllegalStateException("Unexpected close exception", e);
205 }
206 }
207
208 @Override
209 public boolean hasMemoryAddress() {
210 return true;
211 }
212
213 @Override
214 public long memoryAddress() {
215 return memoryAddress;
216 }
217 }
218 }