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                  // 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 }