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
88 // Try to access shared Arena which might fail on GraalVM 25.0.0 if not enabled
89 // See https://github.com/netty/netty/issues/15762
90 Object shared = ofShared.invoke();
91 ((AutoCloseable) shared).close();
92
93 // allocate.type() = (Arena,long)MemorySegment
94 MethodHandle allocate = lookup.findVirtual(arenaCls, "allocate", methodType(memsegCls, long.class));
95 // asByteBuffer.type() = (MemorySegment)ByteBuffer
96 MethodHandle asByteBuffer = lookup.findVirtual(memsegCls, "asByteBuffer", methodType(ByteBuffer.class));
97 // address.type() = (MemorySegment)long
98 MethodHandle address = lookup.findVirtual(memsegCls, "address", methodType(long.class));
99 // bufClsCtor.type() = (AutoCloseable,ByteBuffer,long)CleanableDirectBufferImpl
100 MethodHandle bufClsCtor = lookup.findConstructor(bufCls,
101 methodType(void.class, AutoCloseable.class, ByteBuffer.class, long.class));
102 // The 'allocate' method takes a 'long' capacity, but we'll be providing an 'int'.
103 // Explicitly cast the 'long' to 'int' so we can use 'invokeExact'.
104 // allocateInt.type() = (Arena,int)MemorySegment
105 MethodHandle allocateInt = MethodHandles.explicitCastArguments(allocate,
106 methodType(memsegCls, arenaCls, int.class));
107 // Use the 'asByteBuffer' and 'address' methods as a filter, to transform the constructor into a method
108 // that takes two MemorySegment arguments instead of a ByteBuffer and a long argument.
109 // ctorArenaMemsegMemseg.type() = (Arena,MemorySegment,MemorySegment)CleanableDirectBufferImpl
110 MethodHandle ctorArenaMemsegMemseg = MethodHandles.explicitCastArguments(
111 MethodHandles.filterArguments(bufClsCtor, 1, asByteBuffer, address),
112 methodType(bufCls, arenaCls, memsegCls, memsegCls));
113 // Our method now takes two MemorySegment arguments, but we actually only want to pass one.
114 // Specifically, we want to get both the ByteBuffer and the memory address from the same MemorySegment
115 // instance.
116 // We permute the argument array such that the first MemorySegment argument gest passed to both
117 // parameters, and then the second parameter value gets ignored.
118 // ctorArenaMemsegNull.type() = (Arena,MemorySegment,MemorySegment)CleanableDirectBufferImpl
119 MethodHandle ctorArenaMemsegNull = MethodHandles.permuteArguments(ctorArenaMemsegMemseg,
120 methodType(bufCls, arenaCls, memsegCls, memsegCls), 0, 1, 1);
121 // With the second MemorySegment argument ignored, we can statically bind it to 'null' to effectively
122 // drop it from our parameter list.
123 MethodHandle ctorArenaMemseg = MethodHandles.insertArguments(
124 ctorArenaMemsegNull, 2, new Object[]{null});
125 // Use the 'allocateInt' method to transform the last MemorySegment argument of the constructor,
126 // into an (Arena,int) argument pair.
127 // ctorArenaArenaInt.type() = (Arena,Arena,int)CleanableDirectBufferImpl
128 MethodHandle ctorArenaArenaInt = MethodHandles.collectArguments(ctorArenaMemseg, 1, allocateInt);
129 // Our method now takes two Arena arguments, but we actually only want to pass one. Specifically, it's
130 // very important that it's the same arena we use for both allocation and deallocation.
131 // We permute the argument array such that the first Arena argument gets passed to both parameters,
132 // and the second parameter value gets ignored.
133 // ctorArenaNullInt.type() = (Arena,Arena,int)CleanableDirectBufferImpl
134 MethodHandle ctorArenaNullInt = MethodHandles.permuteArguments(ctorArenaArenaInt,
135 methodType(bufCls, arenaCls, arenaCls, int.class), 0, 0, 2);
136 // With the second Arena parameter value ignored, we can statically bind it to 'null' to effectively
137 // drop it from our parameter list.
138 // ctorArenaInt.type() = (Arena,int)CleanableDirectBufferImpl
139 MethodHandle ctorArenaInt = MethodHandles.insertArguments(ctorArenaNullInt, 1, new Object[]{null});
140 // Now we just need to create our Arena instance. We fold the Arena parameter into the 'ofShared'
141 // static method, so we effectively bind the argument to the result of calling that method.
142 // Since 'ofShared' takes no further parameters, we effectively eliminate the first parameter.
143 // This creates our method handle that takes an 'int' and returns a 'CleanableDirectBufferImpl'.
144 // ctorInt.type() = (int)CleanableDirectBufferImpl
145 method = MethodHandles.foldArguments(ctorArenaInt, ofShared);
146 error = null;
147 } catch (Throwable throwable) {
148 method = null;
149 error = throwable;
150 }
151 } else {
152 method = null;
153 error = new UnsupportedOperationException("java.lang.foreign.MemorySegment unavailable");
154 }
155 if (logger != null) {
156 if (error == null) {
157 logger.debug("java.nio.ByteBuffer.cleaner(): available");
158 } else {
159 logger.debug("java.nio.ByteBuffer.cleaner(): unavailable", error);
160 }
161 }
162 INVOKE_ALLOCATOR = method;
163 }
164
165 static boolean isSupported() {
166 return INVOKE_ALLOCATOR != null;
167 }
168
169 @SuppressWarnings("OverlyStrongTypeCast") // The cast is needed for 'invokeExact' semantics.
170 @Override
171 public CleanableDirectBuffer allocate(int capacity) {
172 try {
173 return (CleanableDirectBufferImpl) INVOKE_ALLOCATOR.invokeExact(capacity);
174 } catch (RuntimeException e) {
175 throw e; // Propagate the runtime exceptions that the Arena would normally throw.
176 } catch (Throwable e) {
177 throw new IllegalStateException("Unexpected allocation exception", e);
178 }
179 }
180
181 @Override
182 public void freeDirectBuffer(ByteBuffer buffer) {
183 throw new UnsupportedOperationException("Cannot clean arbitrary ByteBuffer instances");
184 }
185
186 private static final class CleanableDirectBufferImpl implements CleanableDirectBuffer {
187 private final AutoCloseable closeable;
188 private final ByteBuffer buffer;
189 private final long memoryAddress;
190
191 // NOTE: must be at least package-protected to allow calls from the method handles!
192 CleanableDirectBufferImpl(AutoCloseable closeable, ByteBuffer buffer, long memoryAddress) {
193 this.closeable = closeable;
194 this.buffer = buffer;
195 this.memoryAddress = memoryAddress;
196 }
197
198 @Override
199 public ByteBuffer buffer() {
200 return buffer;
201 }
202
203 @Override
204 public void clean() {
205 try {
206 closeable.close();
207 } catch (RuntimeException e) {
208 throw e; // Propagate the runtime exceptions that Arena would normally throw.
209 } catch (Exception e) {
210 throw new IllegalStateException("Unexpected close exception", e);
211 }
212 }
213
214 @Override
215 public boolean hasMemoryAddress() {
216 return true;
217 }
218
219 @Override
220 public long memoryAddress() {
221 return memoryAddress;
222 }
223 }
224 }