View Javadoc
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 }