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.lang.reflect.Array;
24  import java.nio.ByteBuffer;
25  
26  import static java.lang.invoke.MethodType.methodType;
27  
28  public class CleanerJava24Linker implements Cleaner {
29      private static final InternalLogger logger;
30  
31      private static final MethodHandle INVOKE_MALLOC;
32      private static final MethodHandle INVOKE_CREATE_BYTEBUFFER;
33      private static final MethodHandle INVOKE_FREE;
34  
35      static {
36          boolean suitableJavaVersion;
37          if (System.getProperty("org.graalvm.nativeimage.imagecode") != null) {
38              // native image supports this since 25, but we don't use PlatformDependent0 here, since
39              // we need to initialize CleanerJava24Linker at build time.
40              String v = System.getProperty("java.specification.version");
41              try {
42                  suitableJavaVersion = Integer.parseInt(v) >= 25;
43              } catch (NumberFormatException e) {
44                  suitableJavaVersion = false;
45              }
46              // also need to prevent initializing the logger at build time
47              logger = null;
48          } else {
49              // Only attempt to use MemorySegments on Java 24 or greater, where warnings about Unsafe
50              // memory access operations start to appear.
51              // The following JDK bugs do NOT affect our implementation because the memory segments we
52              // create are associated with the GLOBAL_SESSION:
53              // - https://bugs.openjdk.org/browse/JDK-8357145
54              // - https://bugs.openjdk.org/browse/JDK-8357268
55              suitableJavaVersion = PlatformDependent0.javaVersion() >= 24;
56              logger = InternalLoggerFactory.getInstance(CleanerJava24Linker.class);
57          }
58  
59          MethodHandle mallocMethod;
60          MethodHandle wrapMethod;
61          MethodHandle freeMethod;
62          Throwable error;
63  
64          if (suitableJavaVersion) {
65              try {
66                  // First, we need to check if we have access to "restricted" methods through the Java Module system.
67                  MethodHandles.Lookup lookup = MethodHandles.lookup();
68                  Class<?> moduleCls = Class.forName("java.lang.Module");
69                  MethodHandle getModule = lookup.findVirtual(
70                          Class.class, "getModule", methodType(moduleCls));
71                  MethodHandle isNativeAccessEnabledModule = lookup.findVirtual(
72                          moduleCls, "isNativeAccessEnabled", methodType(boolean.class));
73                  MethodHandle isNativeAccessEnabledForClass = MethodHandles.filterArguments(
74                          isNativeAccessEnabledModule, 0, getModule);
75                  boolean isNativeAccessEnabled =
76                          (boolean) isNativeAccessEnabledForClass.invokeExact(CleanerJava24Linker.class);
77                  if (!isNativeAccessEnabled) {
78                      throw new UnsupportedOperationException(
79                              "Native access (restricted methods) is not enabled for the io.netty.common module.");
80                  }
81  
82                  // Second, we need to check the size of a pointer address. For simplicity, we'd like to assume the size
83                  // of an address is the same as a Java long. So effectively, we're only enabled on 64-bit platforms.
84                  Class<?> memoryLayoutCls = Class.forName("java.lang.foreign.MemoryLayout");
85                  Class<?> memoryLayoutArrayCls = Class.forName("[Ljava.lang.foreign.MemoryLayout;");
86                  Class<?> valueLayoutCls = Class.forName("java.lang.foreign.ValueLayout");
87                  Class<?> valueLayoutAddressCls = Class.forName("java.lang.foreign.AddressLayout");
88                  MethodHandle addressLayoutGetter = lookup.findStaticGetter(
89                          valueLayoutCls, "ADDRESS", valueLayoutAddressCls);
90                  MethodHandle byteSize = lookup.findVirtual(valueLayoutAddressCls, "byteSize", methodType(long.class));
91                  MethodHandle byteSizeOfAddress = MethodHandles.foldArguments(byteSize, addressLayoutGetter);
92                  long addressSize = (long) byteSizeOfAddress.invokeExact();
93                  if (addressSize != Long.BYTES) {
94                      throw new UnsupportedOperationException(
95                              "Linking to malloc and free is only supported on 64-bit platforms.");
96                  }
97  
98                  // Finally, we create three method handles, for malloc, free, and for wrapping an address in a
99                  // ByteBuffer. Effectively, we need the equivalent of these three code snippets:
100                 //
101                 //        MemorySegment mallocPtr = Linker.nativeLinker().defaultLookup().find("malloc").get();
102                 //        MethodHandle malloc = Linker.nativeLinker().downcallHandle(
103                 //                mallocPtr, FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG));
104                 //
105                 //        MemorySegment freePtr = Linker.nativeLinker().defaultLookup().find("free").get();
106                 //        MethodHandle free = Linker.nativeLinker().downcallHandle(
107                 //                freePtr, FunctionDescriptor.ofVoid(ValueLayout.JAVA_LONG));
108                 //
109                 //        ByteBuffer byteBuffer = MemorySegment.ofAddress(addr).asByteBuffer();
110                 Class<?> ofLongValueLayoutCls = Class.forName("java.lang.foreign.ValueLayout$OfLong");
111                 Class<?> linkerCls = Class.forName("java.lang.foreign.Linker");
112                 Class<?> linkerOptionCls = Class.forName("java.lang.foreign.Linker$Option");
113                 Class<?> linkerOptionArrayCls = Class.forName("[Ljava.lang.foreign.Linker$Option;");
114                 Class<?> symbolLookupCls = Class.forName("java.lang.foreign.SymbolLookup");
115                 Class<?> memSegCls = Class.forName("java.lang.foreign.MemorySegment");
116                 Class<?> funcDescCls = Class.forName("java.lang.foreign.FunctionDescriptor");
117 
118                 MethodHandle nativeLinker = lookup.findStatic(linkerCls, "nativeLinker", methodType(linkerCls));
119                 MethodHandle defaultLookupStatic = MethodHandles.foldArguments(
120                         lookup.findVirtual(linkerCls, "defaultLookup", methodType(symbolLookupCls)),
121                         nativeLinker);
122                 MethodHandle downcallHandleStatic = MethodHandles.foldArguments(
123                         lookup.findVirtual(linkerCls, "downcallHandle",
124                                 methodType(MethodHandle.class, memSegCls, funcDescCls, linkerOptionArrayCls)),
125                         nativeLinker);
126                 MethodHandle findSymbol = MethodHandles.foldArguments(
127                         lookup.findVirtual(symbolLookupCls, "findOrThrow", methodType(memSegCls, String.class)),
128                         defaultLookupStatic);
129 
130                 // Constructing the malloc (long)long handle
131                 Object longLayout = lookup.findStaticGetter(valueLayoutCls, "JAVA_LONG", ofLongValueLayoutCls).invoke();
132                 Object layoutArray = Array.newInstance(memoryLayoutCls, 1);
133                 Array.set(layoutArray, 0, longLayout);
134                 MethodHandle mallocFuncDesc = MethodHandles.insertArguments(
135                         lookup.findStatic(funcDescCls, "of",
136                                 methodType(funcDescCls, memoryLayoutCls, memoryLayoutArrayCls)),
137                         0, longLayout, layoutArray);
138                 MethodHandle mallocLinker = MethodHandles.foldArguments(
139                         MethodHandles.foldArguments(downcallHandleStatic,
140                                 MethodHandles.foldArguments(findSymbol,
141                                         MethodHandles.constant(String.class, "malloc"))),
142                         mallocFuncDesc);
143                 mallocMethod = (MethodHandle) mallocLinker.invoke(Array.newInstance(linkerOptionCls, 0));
144 
145                 // Constructing the free (long)void handle
146                 MethodHandle freeFuncDesc = MethodHandles.insertArguments(
147                         lookup.findStatic(funcDescCls, "ofVoid",
148                                 methodType(funcDescCls, memoryLayoutArrayCls)),
149                         0, layoutArray);
150                 MethodHandle freeLinker = MethodHandles.foldArguments(
151                         MethodHandles.foldArguments(downcallHandleStatic,
152                                 MethodHandles.foldArguments(findSymbol,
153                                         MethodHandles.constant(String.class, "free"))),
154                         freeFuncDesc);
155                 freeMethod = (MethodHandle) freeLinker.invoke(Array.newInstance(linkerOptionCls, 0));
156 
157                 // Constructing the wrapper (long, long)ByteBuffer handle
158                 MethodHandle ofAddress = lookup.findStatic(memSegCls, "ofAddress", methodType(memSegCls, long.class));
159                 MethodHandle reinterpret = lookup.findVirtual(memSegCls, "reinterpret",
160                         methodType(memSegCls, long.class));
161                 MethodHandle asByteBuffer = lookup.findVirtual(memSegCls, "asByteBuffer", methodType(ByteBuffer.class));
162                 wrapMethod = MethodHandles.filterReturnValue(
163                         MethodHandles.filterArguments(reinterpret, 0, ofAddress),
164                         asByteBuffer);
165 
166                 error = null;
167             } catch (Throwable throwable) {
168                 mallocMethod = null;
169                 wrapMethod = null;
170                 freeMethod = null;
171                 error = throwable;
172             }
173         } else {
174             mallocMethod = null;
175             wrapMethod = null;
176             freeMethod = null;
177             error = new UnsupportedOperationException("java.lang.foreign.MemorySegment unavailable");
178         }
179 
180         if (logger != null) {
181             if (error == null) {
182                 logger.debug("java.nio.ByteBuffer.cleaner(): available");
183             } else {
184                 logger.debug("java.nio.ByteBuffer.cleaner(): unavailable", error);
185             }
186         }
187         INVOKE_MALLOC = mallocMethod;
188         INVOKE_CREATE_BYTEBUFFER = wrapMethod;
189         INVOKE_FREE = freeMethod;
190     }
191 
192     static boolean isSupported() {
193         return INVOKE_MALLOC != null;
194     }
195 
196     @Override
197     public CleanableDirectBuffer allocate(int capacity) {
198         return new CleanableDirectBufferImpl(capacity);
199     }
200 
201     @Override
202     public void freeDirectBuffer(ByteBuffer buffer) {
203         throw new UnsupportedOperationException("Cannot clean arbitrary ByteBuffer instances");
204     }
205 
206     static long malloc(int capacity) {
207         final long addr;
208         try {
209             addr = (long) INVOKE_MALLOC.invokeExact((long) capacity);
210         } catch (Throwable e) {
211             throw new Error(e); // Should not happen.
212         }
213         if (addr == 0) {
214             throw new OutOfMemoryError("malloc(2) failed to allocate " + capacity + " bytes");
215         }
216         return addr;
217     }
218 
219     static void free(long memoryAddress) {
220         try {
221             INVOKE_FREE.invokeExact(memoryAddress);
222         } catch (Throwable e) {
223             throw new Error(e); // Should not happen.
224         }
225     }
226 
227     private static final class CleanableDirectBufferImpl implements CleanableDirectBuffer {
228         private final ByteBuffer buffer;
229         private final long memoryAddress;
230 
231         private CleanableDirectBufferImpl(int capacity) {
232             long addr = malloc(capacity);
233             try {
234                 memoryAddress = addr;
235                 buffer = (ByteBuffer) INVOKE_CREATE_BYTEBUFFER.invokeExact(addr, (long) capacity);
236             } catch (Throwable throwable) {
237                 Error error = new Error(throwable);
238                 try {
239                     free(addr);
240                 } catch (Throwable e) {
241                     error.addSuppressed(e);
242                 }
243                 throw error;
244             }
245         }
246 
247         @Override
248         public ByteBuffer buffer() {
249             return buffer;
250         }
251 
252         @Override
253         public void clean() {
254             free(memoryAddress);
255         }
256 
257         @Override
258         public boolean hasMemoryAddress() {
259             return true;
260         }
261 
262         @Override
263         public long memoryAddress() {
264             return memoryAddress;
265         }
266     }
267 }