From 46e521dc98da2401b2760b7e49f1b622e8e16cfe Mon Sep 17 00:00:00 2001 From: Florian Bernard Date: Mon, 5 Oct 2026 09:32:19 +0200 Subject: [PATCH 1/7] [refacto] extract unsafe access behind an accessor - Move the sun.misc.Unsafe code from MemoryUtil into UnsafeMemoryAccessor - MemoryUtil delegates every low-level operation to a MemoryUtilAccessor - No behavior change: UnsafeMemoryAccessor is the only accessor --- .../apache/arrow/memory/util/MemoryUtil.java | 201 ++----------- .../arrow/memory/util/MemoryUtilAccessor.java | 62 ++++ .../memory/util/UnsafeMemoryAccessor.java | 265 ++++++++++++++++++ 3 files changed, 347 insertions(+), 181 deletions(-) create mode 100644 memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtilAccessor.java create mode 100644 memory/memory-core/src/main/java/org/apache/arrow/memory/util/UnsafeMemoryAccessor.java diff --git a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java index be0749a215..9f37032c36 100644 --- a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java +++ b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java @@ -16,151 +16,18 @@ */ package org.apache.arrow.memory.util; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InaccessibleObjectException; -import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.AccessController; -import java.security.PrivilegedAction; -import org.checkerframework.checker.nullness.qual.Nullable; -import sun.misc.Unsafe; /** Utilities for memory related operations. */ public class MemoryUtil { - private static final org.slf4j.Logger logger = - org.slf4j.LoggerFactory.getLogger(MemoryUtil.class); - - private static final @Nullable Constructor DIRECT_BUFFER_CONSTRUCTOR; - - /** The unsafe object from which to access the off-heap memory. */ - private static final Unsafe UNSAFE; - - /** The start offset of array data relative to the start address of the array object. */ - private static final long BYTE_ARRAY_BASE_OFFSET; - - /** The offset of the address field with the {@link java.nio.ByteBuffer} object. */ - private static final long BYTE_BUFFER_ADDRESS_OFFSET; /** If the native byte order is little-endian. */ public static final boolean LITTLE_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN; - // Java 1.8, 9, 11, 17, 21 becomes 1, 9, 11, 17, and 21. - @SuppressWarnings("StringSplitter") - private static final int majorVersion = - Integer.parseInt(System.getProperty("java.specification.version").split("\\D+")[0]); - - static { - try { - // try to get the unsafe object - final Object maybeUnsafe = - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - @SuppressWarnings({"nullness:argument", "nullness:return"}) - // incompatible argument for parameter obj of Field.get - // incompatible types in return - public Object run() { - try { - final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); - unsafeField.setAccessible(true); - return unsafeField.get(null); - } catch (Throwable e) { - return e; - } - } - }); - - if (maybeUnsafe instanceof Throwable) { - throw (Throwable) maybeUnsafe; - } - - UNSAFE = (Unsafe) maybeUnsafe; - - // get the offset of the data inside a byte array object - BYTE_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); - - // get the offset of the address field in a java.nio.Buffer object - long maybeOffset; - Field addressField = java.nio.Buffer.class.getDeclaredField("address"); - try { - addressField.setAccessible(true); - maybeOffset = UNSAFE.objectFieldOffset(addressField); - } catch (InaccessibleObjectException e) { - maybeOffset = -1; - logger.debug( - "Cannot access the address field of java.nio.Buffer. DirectBuffer operations wont be available", - e); - } - BYTE_BUFFER_ADDRESS_OFFSET = maybeOffset; + private static final MemoryUtilAccessor ACCESSOR = UnsafeMemoryAccessor.INSTANCE; - Constructor directBufferConstructor; - long address = -1; - final ByteBuffer direct = ByteBuffer.allocateDirect(1); - try { - - final Object maybeDirectBufferConstructor = - AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public Object run() { - try { - final Constructor constructor = - (majorVersion >= 21) - ? direct.getClass().getDeclaredConstructor(long.class, long.class) - : direct.getClass().getDeclaredConstructor(long.class, int.class); - constructor.setAccessible(true); - logger.debug("Constructor for direct buffer found and made accessible"); - return constructor; - } catch (NoSuchMethodException e) { - logger.debug("Cannot get constructor for direct buffer allocation", e); - return e; - } catch (SecurityException e) { - logger.debug("Cannot get constructor for direct buffer allocation", e); - return e; - } catch (InaccessibleObjectException e) { - logger.debug("Cannot get constructor for direct buffer allocation", e); - return e; - } - } - }); - - if (maybeDirectBufferConstructor instanceof Constructor) { - address = UNSAFE.allocateMemory(1); - // try to use the constructor now - try { - ((Constructor) maybeDirectBufferConstructor).newInstance(address, 1); - directBufferConstructor = (Constructor) maybeDirectBufferConstructor; - logger.debug("direct buffer constructor: available"); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - logger.warn("unable to instantiate a direct buffer via constructor", e); - directBufferConstructor = null; - } - } else { - logger.debug( - "direct buffer constructor: unavailable", (Throwable) maybeDirectBufferConstructor); - directBufferConstructor = null; - } - } finally { - if (address != -1) { - UNSAFE.freeMemory(address); - } - } - DIRECT_BUFFER_CONSTRUCTOR = directBufferConstructor; - } catch (Throwable e) { - // This exception will get swallowed, but it's necessary for the static analysis that ensures - // the static fields above get initialized - final RuntimeException failure = - new RuntimeException( - "Failed to initialize MemoryUtil. You must start Java with " - + "`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` " - + "(See https://arrow.apache.org/docs/java/install.html)", - e); - failure.printStackTrace(); - throw failure; - } - } + private MemoryUtil() {} /** * Given a {@link ByteBuffer}, gets the address the underlying memory space. @@ -169,103 +36,75 @@ public Object run() { * @return address of the underlying memory. */ public static long getByteBufferAddress(ByteBuffer buf) { - if (BYTE_BUFFER_ADDRESS_OFFSET != -1) { - return UNSAFE.getLong(buf, BYTE_BUFFER_ADDRESS_OFFSET); - } - throw new UnsupportedOperationException( - "Byte buffer address cannot be obtained because sun.misc.Unsafe or java.nio.DirectByteBuffer.(long, int) is not available"); + return ACCESSOR.getByteBufferAddress(buf); } - private MemoryUtil() {} - /** Create nio byte buffer. */ public static ByteBuffer directBuffer(long address, int capacity) { - if (DIRECT_BUFFER_CONSTRUCTOR != null) { - if (capacity < 0) { - throw new IllegalArgumentException("Capacity is negative, has to be positive or 0"); - } - try { - return (ByteBuffer) DIRECT_BUFFER_CONSTRUCTOR.newInstance(address, capacity); - } catch (Throwable cause) { - throw new Error(cause); - } - } - throw new UnsupportedOperationException( - "sun.misc.Unsafe or java.nio.DirectByteBuffer.(long, int) not available"); - } - - @SuppressWarnings( - "nullness:argument") // to handle null assignment on third party dependency: Unsafe - private static void copyMemory( - @Nullable Object srcBase, - long srcOffset, - @Nullable Object destBase, - long destOffset, - long bytes) { - UNSAFE.copyMemory(srcBase, srcOffset, destBase, destOffset, bytes); + return ACCESSOR.directBuffer(address, capacity); } public static void copyMemory(long srcAddress, long destAddress, long bytes) { - UNSAFE.copyMemory(srcAddress, destAddress, bytes); + ACCESSOR.copyMemory(srcAddress, destAddress, bytes); } public static void copyToMemory(byte[] src, long srcIndex, long destAddress, long bytes) { - copyMemory(src, BYTE_ARRAY_BASE_OFFSET + srcIndex, null, destAddress, bytes); + ACCESSOR.copyToMemory(src, srcIndex, destAddress, bytes); } public static void copyFromMemory(long srcAddress, byte[] dest, long destIndex, long bytes) { - copyMemory(null, srcAddress, dest, BYTE_ARRAY_BASE_OFFSET + destIndex, bytes); + ACCESSOR.copyFromMemory(srcAddress, dest, destIndex, bytes); } public static byte getByte(long address) { - return UNSAFE.getByte(address); + return ACCESSOR.getByte(address); } public static void putByte(long address, byte value) { - UNSAFE.putByte(address, value); + ACCESSOR.putByte(address, value); } public static short getShort(long address) { - return UNSAFE.getShort(address); + return ACCESSOR.getShort(address); } public static void putShort(long address, short value) { - UNSAFE.putShort(address, value); + ACCESSOR.putShort(address, value); } public static int getInt(long address) { - return UNSAFE.getInt(address); + return ACCESSOR.getInt(address); } public static void putInt(long address, int value) { - UNSAFE.putInt(address, value); + ACCESSOR.putInt(address, value); } public static long getLong(long address) { - return UNSAFE.getLong(address); + return ACCESSOR.getLong(address); } public static void putLong(long address, long value) { - UNSAFE.putLong(address, value); + ACCESSOR.putLong(address, value); } public static void setMemory(long address, long bytes, byte value) { - UNSAFE.setMemory(address, bytes, value); + ACCESSOR.setMemory(address, bytes, value); } public static int getInt(byte[] bytes, int index) { - return UNSAFE.getInt(bytes, BYTE_ARRAY_BASE_OFFSET + index); + return ACCESSOR.getInt(bytes, index); } public static long getLong(byte[] bytes, int index) { - return UNSAFE.getLong(bytes, BYTE_ARRAY_BASE_OFFSET + index); + return ACCESSOR.getLong(bytes, index); } public static long allocateMemory(long bytes) { - return UNSAFE.allocateMemory(bytes); + return ACCESSOR.allocateMemory(bytes); } public static void freeMemory(long address) { - UNSAFE.freeMemory(address); + ACCESSOR.freeMemory(address); } } diff --git a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtilAccessor.java b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtilAccessor.java new file mode 100644 index 0000000000..10acddbc52 --- /dev/null +++ b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtilAccessor.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.util; + +import java.nio.ByteBuffer; + +/** + * Pluggable backend for {@link MemoryUtil}'s low-level memory operations. The default + * implementation ({@link UnsafeMemoryAccessor}) is backed by {@code sun.misc.Unsafe}; the {@code + * arrow-memory-ffm} module supplies an alternative backed by {@code java.lang.foreign}. + */ +public interface MemoryUtilAccessor { + long allocateMemory(long bytes); + + void freeMemory(long address); + + byte getByte(long address); + + void putByte(long address, byte value); + + short getShort(long address); + + void putShort(long address, short value); + + int getInt(long address); + + void putInt(long address, int value); + + long getLong(long address); + + void putLong(long address, long value); + + void setMemory(long address, long bytes, byte value); + + void copyMemory(long srcAddress, long destAddress, long bytes); + + void copyToMemory(byte[] src, long srcIndex, long destAddress, long bytes); + + void copyFromMemory(long srcAddress, byte[] dest, long destIndex, long bytes); + + int getInt(byte[] bytes, int index); + + long getLong(byte[] bytes, int index); + + long getByteBufferAddress(ByteBuffer buf); + + ByteBuffer directBuffer(long address, int capacity); +} diff --git a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/UnsafeMemoryAccessor.java b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/UnsafeMemoryAccessor.java new file mode 100644 index 0000000000..08474f2f46 --- /dev/null +++ b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/UnsafeMemoryAccessor.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.InvocationTargetException; +import java.nio.ByteBuffer; +import java.security.AccessController; +import java.security.PrivilegedAction; +import org.checkerframework.checker.nullness.qual.Nullable; +import sun.misc.Unsafe; + +/** {@link MemoryUtilAccessor} backed by {@code sun.misc.Unsafe}. The default accessor. */ +final class UnsafeMemoryAccessor implements MemoryUtilAccessor { + private static final org.slf4j.Logger logger = + org.slf4j.LoggerFactory.getLogger(UnsafeMemoryAccessor.class); + + static final MemoryUtilAccessor INSTANCE = new UnsafeMemoryAccessor(); + + private static final @Nullable Constructor DIRECT_BUFFER_CONSTRUCTOR; + private static final Unsafe UNSAFE; + private static final long BYTE_ARRAY_BASE_OFFSET; + private static final long BYTE_BUFFER_ADDRESS_OFFSET; + + // Java 1.8, 9, 11, 17, 21 becomes 1, 9, 11, 17, and 21. + @SuppressWarnings("StringSplitter") + private static final int majorVersion = + Integer.parseInt(System.getProperty("java.specification.version").split("\\D+")[0]); + + static { + try { + // try to get the unsafe object + final Object maybeUnsafe = + AccessController.doPrivileged( + new PrivilegedAction() { + @Override + @SuppressWarnings({"nullness:argument", "nullness:return"}) + // incompatible argument for parameter obj of Field.get + // incompatible types in return + public Object run() { + try { + final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + return unsafeField.get(null); + } catch (Throwable e) { + return e; + } + } + }); + + if (maybeUnsafe instanceof Throwable) { + throw (Throwable) maybeUnsafe; + } + + UNSAFE = (Unsafe) maybeUnsafe; + + // get the offset of the data inside a byte array object + BYTE_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + + // get the offset of the address field in a java.nio.Buffer object + long maybeOffset; + Field addressField = java.nio.Buffer.class.getDeclaredField("address"); + try { + addressField.setAccessible(true); + maybeOffset = UNSAFE.objectFieldOffset(addressField); + } catch (InaccessibleObjectException e) { + maybeOffset = -1; + logger.debug( + "Cannot access the address field of java.nio.Buffer. DirectBuffer operations wont be available", + e); + } + BYTE_BUFFER_ADDRESS_OFFSET = maybeOffset; + + Constructor directBufferConstructor; + long address = -1; + final ByteBuffer direct = ByteBuffer.allocateDirect(1); + try { + + final Object maybeDirectBufferConstructor = + AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public Object run() { + try { + final Constructor constructor = + (majorVersion >= 21) + ? direct.getClass().getDeclaredConstructor(long.class, long.class) + : direct.getClass().getDeclaredConstructor(long.class, int.class); + constructor.setAccessible(true); + logger.debug("Constructor for direct buffer found and made accessible"); + return constructor; + } catch (NoSuchMethodException e) { + logger.debug("Cannot get constructor for direct buffer allocation", e); + return e; + } catch (SecurityException e) { + logger.debug("Cannot get constructor for direct buffer allocation", e); + return e; + } catch (InaccessibleObjectException e) { + logger.debug("Cannot get constructor for direct buffer allocation", e); + return e; + } + } + }); + + if (maybeDirectBufferConstructor instanceof Constructor) { + address = UNSAFE.allocateMemory(1); + // try to use the constructor now + try { + ((Constructor) maybeDirectBufferConstructor).newInstance(address, 1); + directBufferConstructor = (Constructor) maybeDirectBufferConstructor; + logger.debug("direct buffer constructor: available"); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + logger.warn("unable to instantiate a direct buffer via constructor", e); + directBufferConstructor = null; + } + } else { + logger.debug( + "direct buffer constructor: unavailable", (Throwable) maybeDirectBufferConstructor); + directBufferConstructor = null; + } + } finally { + if (address != -1) { + UNSAFE.freeMemory(address); + } + } + DIRECT_BUFFER_CONSTRUCTOR = directBufferConstructor; + } catch (Throwable e) { + // This exception will get swallowed, but it's necessary for the static analysis that ensures + // the static fields above get initialized + final RuntimeException failure = + new RuntimeException( + "Failed to initialize MemoryUtil. You must start Java with " + + "`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` " + + "(See https://arrow.apache.org/docs/java/install.html)", + e); + failure.printStackTrace(); + throw failure; + } + } + + private UnsafeMemoryAccessor() {} + + @Override + public long getByteBufferAddress(ByteBuffer buf) { + if (BYTE_BUFFER_ADDRESS_OFFSET != -1) { + return UNSAFE.getLong(buf, BYTE_BUFFER_ADDRESS_OFFSET); + } + throw new UnsupportedOperationException( + "Byte buffer address cannot be obtained because sun.misc.Unsafe or java.nio.DirectByteBuffer.(long, int) is not available"); + } + + @Override + public ByteBuffer directBuffer(long address, int capacity) { + if (DIRECT_BUFFER_CONSTRUCTOR != null) { + if (capacity < 0) { + throw new IllegalArgumentException("Capacity is negative, has to be positive or 0"); + } + try { + return (ByteBuffer) DIRECT_BUFFER_CONSTRUCTOR.newInstance(address, capacity); + } catch (Throwable cause) { + throw new Error(cause); + } + } + throw new UnsupportedOperationException( + "sun.misc.Unsafe or java.nio.DirectByteBuffer.(long, int) not available"); + } + + @Override + public void copyMemory(long srcAddress, long destAddress, long bytes) { + UNSAFE.copyMemory(srcAddress, destAddress, bytes); + } + + @Override + @SuppressWarnings("nullness:argument") + public void copyToMemory(byte[] src, long srcIndex, long destAddress, long bytes) { + UNSAFE.copyMemory(src, BYTE_ARRAY_BASE_OFFSET + srcIndex, null, destAddress, bytes); + } + + @Override + @SuppressWarnings("nullness:argument") + public void copyFromMemory(long srcAddress, byte[] dest, long destIndex, long bytes) { + UNSAFE.copyMemory(null, srcAddress, dest, BYTE_ARRAY_BASE_OFFSET + destIndex, bytes); + } + + @Override + public byte getByte(long address) { + return UNSAFE.getByte(address); + } + + @Override + public void putByte(long address, byte value) { + UNSAFE.putByte(address, value); + } + + @Override + public short getShort(long address) { + return UNSAFE.getShort(address); + } + + @Override + public void putShort(long address, short value) { + UNSAFE.putShort(address, value); + } + + @Override + public int getInt(long address) { + return UNSAFE.getInt(address); + } + + @Override + public void putInt(long address, int value) { + UNSAFE.putInt(address, value); + } + + @Override + public long getLong(long address) { + return UNSAFE.getLong(address); + } + + @Override + public void putLong(long address, long value) { + UNSAFE.putLong(address, value); + } + + @Override + public void setMemory(long address, long bytes, byte value) { + UNSAFE.setMemory(address, bytes, value); + } + + @Override + public int getInt(byte[] bytes, int index) { + return UNSAFE.getInt(bytes, BYTE_ARRAY_BASE_OFFSET + index); + } + + @Override + public long getLong(byte[] bytes, int index) { + return UNSAFE.getLong(bytes, BYTE_ARRAY_BASE_OFFSET + index); + } + + @Override + public long allocateMemory(long bytes) { + return UNSAFE.allocateMemory(bytes); + } + + @Override + public void freeMemory(long address) { + UNSAFE.freeMemory(address); + } +} From 32eb8721c2653e89a469b46bbebfd5f10503852f Mon Sep 17 00:00:00 2001 From: Florian Bernard Date: Mon, 5 Oct 2026 09:32:19 +0200 Subject: [PATCH 2/7] [feat] add arrow-memory-ffm module - New opt-in module (JDK 22+, java.lang.foreign per JEP 454), only part of the Maven reactor when building with a JDK 22+ launcher - FfmMemoryAccessor implements MemoryUtilAccessor with MemorySegment and Arena instead of sun.misc.Unsafe and reflection - FfmAllocationManager allocates one Arena per buffer, mirroring UnsafeAllocationManager, with a DefaultAllocationManagerFactory for CheckAllocator's classpath scan - arrow.memory.accessor.type=FFM selects the FFM accessor and fails with an actionable message if arrow-memory-ffm is missing; unknown values warn and fall back to Unsafe - arrow.allocation.manager.type=FFM selects FfmAllocationManager and, when arrow.memory.accessor.type is unset, the FFM accessor too, falling back to Unsafe with a warning if the module is missing - Isolated Surefire executions cover each property combination, with and without add-opens - Add the module to the BOM and to the install and overview docs --- bom/pom.xml | 5 + docs/source/install.rst | 1 + docs/source/overview.rst | 3 + memory/memory-core/pom.xml | 39 ++++ .../apache/arrow/memory/CheckAllocator.java | 14 +- .../DefaultAllocationManagerOption.java | 21 ++ .../apache/arrow/memory/util/MemoryUtil.java | 93 +++++++- .../TestMemoryUtilAccessorResolution.java | 41 ++++ ...stMemoryUtilAllocationManagerFallback.java | 38 ++++ memory/memory-ffm/pom.xml | 138 ++++++++++++ .../memory-ffm/src/main/java/module-info.java | 22 ++ .../ffm/DefaultAllocationManagerFactory.java | 37 ++++ .../memory/ffm/FfmAllocationManager.java | 72 ++++++ .../arrow/memory/ffm/FfmMemoryAccessor.java | 208 ++++++++++++++++++ .../memory/ffm/TestAllocationManagerFfm.java | 43 ++++ ...tAllocationManagerTypeImpliesAccessor.java | 47 ++++ .../memory/ffm/TestFfmAllocationManager.java | 75 +++++++ .../memory/ffm/TestFfmArrowBufByteBuffer.java | 141 ++++++++++++ .../memory/ffm/TestFfmMemoryAccessor.java | 136 ++++++++++++ .../TestFfmMemoryAccessorWithoutAddOpens.java | 54 +++++ .../memory/ffm/TestMemoryUtilFfmWiring.java | 42 ++++ memory/pom.xml | 13 ++ 22 files changed, 1281 insertions(+), 2 deletions(-) create mode 100644 memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAccessorResolution.java create mode 100644 memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAllocationManagerFallback.java create mode 100644 memory/memory-ffm/pom.xml create mode 100644 memory/memory-ffm/src/main/java/module-info.java create mode 100644 memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/DefaultAllocationManagerFactory.java create mode 100644 memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java create mode 100644 memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerFfm.java create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerTypeImpliesAccessor.java create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmArrowBufByteBuffer.java create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessorWithoutAddOpens.java create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestMemoryUtilFfmWiring.java diff --git a/bom/pom.xml b/bom/pom.xml index 8638f4d08f..a7de29d5af 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -174,6 +174,11 @@ under the License. arrow-memory-core ${project.version} + + org.apache.arrow + arrow-memory-ffm + ${project.version} + org.apache.arrow arrow-memory-netty diff --git a/docs/source/install.rst b/docs/source/install.rst index 904b004995..8a5074e0ed 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -35,6 +35,7 @@ Note that some JDK internals must be exposed by adding these flags to the ``java - ``--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`` (always required) - ``--enable-native-access=io.netty.common`` (Java 25+, when using ``arrow-memory-netty``) - ``--sun-misc-unsafe-memory-access=allow`` (Java 25+; not stricly necessary, but suppresses certain warnings) +- ``--enable-native-access=ALL-UNNAMED`` (when using ``arrow-memory-ffm``, which also requires JDK 22+) .. code-block:: shell diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 1188054114..4da22fd16b 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -42,6 +42,9 @@ but some modules are JNI bindings to the C++ library. * - arrow-memory-netty - Memory management implementation based on Netty. - Native + * - arrow-memory-ffm + - Memory management implementation based on the Java Foreign Function and Memory API. Requires JDK 22+. + - Native * - arrow-vector - An off-heap reference implementation for Arrow columnar data format. - Native diff --git a/memory/memory-core/pom.xml b/memory/memory-core/pom.xml index f047999070..72af3e5349 100644 --- a/memory/memory-core/pom.xml +++ b/memory/memory-core/pom.xml @@ -86,6 +86,10 @@ under the License. **/TestOpens.java + + **/TestMemoryUtilAccessorResolution.java + + **/TestMemoryUtilAllocationManagerFallback.java @@ -105,6 +109,41 @@ under the License. + + + ffm-accessor-resolution-tests + + test + + test + + + + **/TestMemoryUtilAccessorResolution.java + + + FFM + + + + + + ffm-allocation-manager-fallback-tests + + test + + test + + + + **/TestMemoryUtilAllocationManagerFallback.java + + + FFM + + + diff --git a/memory/memory-core/src/main/java/org/apache/arrow/memory/CheckAllocator.java b/memory/memory-core/src/main/java/org/apache/arrow/memory/CheckAllocator.java index f595858ebf..76b3ffe2c6 100644 --- a/memory/memory-core/src/main/java/org/apache/arrow/memory/CheckAllocator.java +++ b/memory/memory-core/src/main/java/org/apache/arrow/memory/CheckAllocator.java @@ -36,6 +36,8 @@ final class CheckAllocator { "org/apache/arrow/memory/unsafe/DefaultAllocationManagerFactory.class"; private static final String ALLOCATOR_PATH_NETTY = "org/apache/arrow/memory/netty/DefaultAllocationManagerFactory.class"; + private static final String ALLOCATOR_PATH_FFM = + "org/apache/arrow/memory/ffm/DefaultAllocationManagerFactory.class"; private CheckAllocator() {} @@ -52,6 +54,9 @@ static String check() { } else if (rootAllocator.getPath().contains("memory-netty") || rootAllocator.getPath().contains("/org/apache/arrow/memory/netty/")) { return "org.apache.arrow.memory.netty.DefaultAllocationManagerFactory"; + } else if (rootAllocator.getPath().contains("memory-ffm") + || rootAllocator.getPath().contains("/org/apache/arrow/memory/ffm/")) { + return "org.apache.arrow.memory.ffm.DefaultAllocationManagerFactory"; } else { throw new IllegalStateException( "Unknown allocation manager type to infer. Current: " + rootAllocator.getPath()); @@ -74,6 +79,9 @@ private static Set scanClasspath() { if (!paths.hasMoreElements()) { paths = ClassLoader.getSystemResources(ALLOCATOR_PATH_NETTY); } + if (!paths.hasMoreElements()) { + paths = ClassLoader.getSystemResources(ALLOCATOR_PATH_FFM); + } } else { paths = allocatorClassLoader.getResources(ALLOCATOR_PATH_CORE); if (!paths.hasMoreElements()) { @@ -82,6 +90,9 @@ private static Set scanClasspath() { if (!paths.hasMoreElements()) { paths = allocatorClassLoader.getResources(ALLOCATOR_PATH_NETTY); } + if (!paths.hasMoreElements()) { + paths = allocatorClassLoader.getResources(ALLOCATOR_PATH_FFM); + } } while (paths.hasMoreElements()) { URL path = paths.nextElement(); @@ -106,7 +117,8 @@ private static URL assertOnlyOne(Set urls) { if (urls.isEmpty()) { throw new RuntimeException( "No DefaultAllocationManager found on classpath. Can't allocate Arrow buffers." - + " Please consider adding arrow-memory-netty or arrow-memory-unsafe as a dependency."); + + " Please consider adding arrow-memory-netty, arrow-memory-unsafe, or" + + " arrow-memory-ffm as a dependency."); } return urls.iterator().next(); } diff --git a/memory/memory-core/src/main/java/org/apache/arrow/memory/DefaultAllocationManagerOption.java b/memory/memory-core/src/main/java/org/apache/arrow/memory/DefaultAllocationManagerOption.java index b5e5080171..bb4d14d3c0 100644 --- a/memory/memory-core/src/main/java/org/apache/arrow/memory/DefaultAllocationManagerOption.java +++ b/memory/memory-core/src/main/java/org/apache/arrow/memory/DefaultAllocationManagerOption.java @@ -44,6 +44,13 @@ public enum AllocationManagerType { /** Unsafe based allocation manager. */ Unsafe, + /** + * FFM (java.lang.foreign) based allocation manager. Also switches {@code + * org.apache.arrow.memory.util.MemoryUtil} to its FFM-based accessor (avoiding {@code + * sun.misc.Unsafe} entirely) unless {@code arrow.memory.accessor.type} says otherwise. + */ + FFM, + /** Unknown type. */ Unknown, } @@ -87,6 +94,9 @@ static AllocationManager.Factory getDefaultAllocationManagerFactory() { case Unsafe: DEFAULT_ALLOCATION_MANAGER_FACTORY = getUnsafeFactory(); break; + case FFM: + DEFAULT_ALLOCATION_MANAGER_FACTORY = getFfmFactory(); + break; case Unknown: LOGGER.info("allocation manager type not specified, using netty as the default type"); DEFAULT_ALLOCATION_MANAGER_FACTORY = getFactory(CheckAllocator.check()); @@ -131,4 +141,15 @@ private static AllocationManager.Factory getNettyFactory() { e); } } + + private static AllocationManager.Factory getFfmFactory() { + try { + return getFactory("org.apache.arrow.memory.ffm.FfmAllocationManager"); + } catch (RuntimeException e) { + throw new RuntimeException( + "Please add arrow-memory-ffm to your classpath," + + " No DefaultAllocationManager found to instantiate an FfmAllocationManager", + e); + } + } } diff --git a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java index 9f37032c36..f70418fe2e 100644 --- a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java +++ b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java @@ -16,8 +16,11 @@ */ package org.apache.arrow.memory.util; +import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import org.apache.arrow.memory.DefaultAllocationManagerOption; +import org.apache.arrow.util.VisibleForTesting; /** Utilities for memory related operations. */ public class MemoryUtil { @@ -25,10 +28,88 @@ public class MemoryUtil { /** If the native byte order is little-endian. */ public static final boolean LITTLE_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN; - private static final MemoryUtilAccessor ACCESSOR = UnsafeMemoryAccessor.INSTANCE; + /** + * The system property used to select the {@link MemoryUtilAccessor} implementation. When unset, + * this defaults to FFM if {@link + * DefaultAllocationManagerOption#ALLOCATION_MANAGER_TYPE_PROPERTY_NAME} (or its environment + * variable) is set to {@code FFM}, so that selecting the FFM allocation manager avoids {@code + * sun.misc.Unsafe} entirely unless this property overrides it. + */ + public static final String MEMORY_ACCESSOR_TYPE_PROPERTY_NAME = "arrow.memory.accessor.type"; + + private static final org.slf4j.Logger logger = + org.slf4j.LoggerFactory.getLogger(MemoryUtil.class); + + private static final MemoryUtilAccessor ACCESSOR = resolveAccessor(); private MemoryUtil() {} + /** Returns the fully qualified class name of the {@link MemoryUtilAccessor} in use. */ + @VisibleForTesting + public static String getAccessorClassName() { + return ACCESSOR.getClass().getName(); + } + + private static MemoryUtilAccessor resolveAccessor() { + String type = System.getProperty(MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, ""); + if ("FFM".equals(type)) { + logger.info( + "{}=FFM, loading org.apache.arrow.memory.ffm.FfmMemoryAccessor", + MEMORY_ACCESSOR_TYPE_PROPERTY_NAME); + return loadFfmAccessor(); + } + if (type.isEmpty() + && DefaultAllocationManagerOption.getDefaultAllocationManagerType() + == DefaultAllocationManagerOption.AllocationManagerType.FFM) { + try { + MemoryUtilAccessor accessor = loadFfmAccessor(); + logger.info( + "{}=FFM, also loading org.apache.arrow.memory.ffm.FfmMemoryAccessor to avoid" + + " sun.misc.Unsafe (override with {}=Unsafe if this is not wanted)", + DefaultAllocationManagerOption.ALLOCATION_MANAGER_TYPE_PROPERTY_NAME, + MEMORY_ACCESSOR_TYPE_PROPERTY_NAME); + return accessor; + } catch (RuntimeException e) { + // Unlike an explicit arrow.memory.accessor.type=FFM request, this preference is only + // inferred from a different property, which may not even be load-bearing (e.g. a + // caller-supplied custom AllocationManager.Factory that never reads it). Fall back + // instead of poisoning MemoryUtil's for the rest of the JVM's life (JLS 12.4.2). + logger.warn( + "{}=FFM but arrow-memory-ffm is not on the classpath; falling back to Unsafe for {}" + + " (set {}=Unsafe to silence this warning)", + DefaultAllocationManagerOption.ALLOCATION_MANAGER_TYPE_PROPERTY_NAME, + MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, + MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, + e); + } + } + if (!"Unsafe".equals(type) && !type.isEmpty()) { + logger.warn( + "Unrecognized {}={}, falling back to Unsafe (valid values: Unsafe, FFM)", + MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, + type); + } + return UnsafeMemoryAccessor.INSTANCE; + } + + @SuppressWarnings({"nullness:argument", "nullness:return"}) + private static MemoryUtilAccessor loadFfmAccessor() { + try { + Field field = + Class.forName("org.apache.arrow.memory.ffm.FfmMemoryAccessor") + .getDeclaredField("INSTANCE"); + field.setAccessible(true); + return (MemoryUtilAccessor) field.get(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException( + "Please add arrow-memory-ffm to your classpath," + + " no FfmMemoryAccessor found to satisfy " + + MEMORY_ACCESSOR_TYPE_PROPERTY_NAME + + "=FFM", + e); + } + } + /** * Given a {@link ByteBuffer}, gets the address the underlying memory space. * @@ -104,6 +185,16 @@ public static long allocateMemory(long bytes) { return ACCESSOR.allocateMemory(bytes); } + /** + * Frees native memory at the given address. + * + *

Behavior depends on the accessor selected by {@value #MEMORY_ACCESSOR_TYPE_PROPERTY_NAME}. + * The default Unsafe accessor frees any valid native address, whatever allocated it. The FFM + * accessor ({@code org.apache.arrow.memory.ffm.FfmMemoryAccessor}) only frees addresses that came + * from its own {@link #allocateMemory}, because it releases the owning {@code Arena} rather than + * the address; an address from any other source (JNI, a foreign {@code malloc}) is a silent no-op + * there. + */ public static void freeMemory(long address) { ACCESSOR.freeMemory(address); } diff --git a/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAccessorResolution.java b/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAccessorResolution.java new file mode 100644 index 0000000000..ed299eceeb --- /dev/null +++ b/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAccessorResolution.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.util; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class TestMemoryUtilAccessorResolution { + + @Test + public void unknownFfmAccessorTypeFailsWithActionableMessage() { + // This test runs in its own JVM (see memory-core/pom.xml Surefire execution added below) + // with -Darrow.memory.accessor.type=FFM and arrow-memory-ffm NOT on the classpath. + // + // MemoryUtil.getByte(0) triggers MemoryUtil class initialization for the first time in this + // JVM, which resolves the accessor eagerly via a static final field. Since the resolution + // failure happens inside , the JVM wraps the RuntimeException thrown by + // resolveAccessor()/loadFfmAccessor() in an ExceptionInInitializerError (JLS 12.4.2); the + // original RuntimeException with the actionable message is available as its cause. + Throwable thrown = assertThrows(Throwable.class, () -> MemoryUtil.getByte(0)); + Throwable cause = thrown instanceof ExceptionInInitializerError ? thrown.getCause() : thrown; + assertTrue(cause instanceof RuntimeException); + assertTrue(cause.getMessage() != null && cause.getMessage().contains("arrow-memory-ffm")); + } +} diff --git a/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAllocationManagerFallback.java b/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAllocationManagerFallback.java new file mode 100644 index 0000000000..c956b3f3bb --- /dev/null +++ b/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilAllocationManagerFallback.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TestMemoryUtilAllocationManagerFallback { + + @Test + public void allocationManagerTypeFfmFallsBackToUnsafeWithoutFfmOnClasspath() { + // This test runs in its own JVM (see memory-core/pom.xml Surefire execution added below) + // with -Darrow.allocation.manager.type=FFM, arrow.memory.accessor.type left unset, and + // arrow-memory-ffm NOT on the classpath. + // + // Unlike an explicit arrow.memory.accessor.type=FFM request, this preference is only inferred + // from a different property. MemoryUtil must fall back to Unsafe instead of failing inside + // , which would otherwise permanently poison the class for the rest of the JVM's life + // (JLS 12.4.2) over a signal that may not even be load-bearing. + assertEquals( + "org.apache.arrow.memory.util.UnsafeMemoryAccessor", MemoryUtil.getAccessorClassName()); + } +} diff --git a/memory/memory-ffm/pom.xml b/memory/memory-ffm/pom.xml new file mode 100644 index 0000000000..c4eea71940 --- /dev/null +++ b/memory/memory-ffm/pom.xml @@ -0,0 +1,138 @@ + + + + 4.0.0 + + org.apache.arrow + arrow-memory + 20.0.0-SNAPSHOT + + + arrow-memory-ffm + Arrow Memory - FFM + Allocator and utils for allocating memory in Arrow based on the Java Foreign Function and Memory API + + + + 22 + + + + + org.apache.arrow + arrow-memory-core + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + ALL-UNNAMED + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + **/TestMemoryUtilFfmWiring.java + **/TestFfmArrowBufByteBuffer.java + + **/TestAllocationManagerTypeImpliesAccessor.java + + **/TestFfmMemoryAccessorWithoutAddOpens.java + + + + + + ffm-accessor-wiring-test + + test + + test + + + + **/TestMemoryUtilFfmWiring.java + **/TestFfmArrowBufByteBuffer.java + + + FFM + + + + + + allocation-manager-type-implies-accessor-test + + test + + test + + + + **/TestAllocationManagerTypeImpliesAccessor.java + + + FFM + + + + + + no-add-opens-tests + + test + + test + + + + + **/TestFfmMemoryAccessorWithoutAddOpens.java + + + + + + + + diff --git a/memory/memory-ffm/src/main/java/module-info.java b/memory/memory-ffm/src/main/java/module-info.java new file mode 100644 index 0000000000..7fa26e4ce3 --- /dev/null +++ b/memory/memory-ffm/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module org.apache.arrow.memory.ffm { + exports org.apache.arrow.memory.ffm to + org.apache.arrow.memory.core; + + requires org.apache.arrow.memory.core; +} diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/DefaultAllocationManagerFactory.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/DefaultAllocationManagerFactory.java new file mode 100644 index 0000000000..41ff8cdc47 --- /dev/null +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/DefaultAllocationManagerFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import org.apache.arrow.memory.AllocationManager; +import org.apache.arrow.memory.ArrowBuf; +import org.apache.arrow.memory.BufferAllocator; + +/** The default Allocation Manager Factory for this module. */ +public class DefaultAllocationManagerFactory implements AllocationManager.Factory { + + public static final AllocationManager.Factory FACTORY = FfmAllocationManager.FACTORY; + + @Override + public AllocationManager create(BufferAllocator accountingAllocator, long size) { + return FACTORY.create(accountingAllocator, size); + } + + @Override + public ArrowBuf empty() { + return FfmAllocationManager.FACTORY.empty(); + } +} diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java new file mode 100644 index 0000000000..a5ea333391 --- /dev/null +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import org.apache.arrow.memory.AllocationManager; +import org.apache.arrow.memory.ArrowBuf; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.ReferenceManager; + +/** Allocation manager based on the Foreign Function & Memory API. */ +public final class FfmAllocationManager extends AllocationManager { + + private static final Arena EMPTY_ARENA = Arena.ofShared(); + private static final ArrowBuf EMPTY = + new ArrowBuf(ReferenceManager.NO_OP, null, 0, EMPTY_ARENA.allocate(0).address()); + + public static final AllocationManager.Factory FACTORY = + new Factory() { + @Override + public AllocationManager create(BufferAllocator accountingAllocator, long size) { + return new FfmAllocationManager(accountingAllocator, size); + } + + @Override + public ArrowBuf empty() { + return EMPTY; + } + }; + + private final Arena arena; + private final long allocatedSize; + private final long allocatedAddress; + + FfmAllocationManager(BufferAllocator accountingAllocator, long requestedSize) { + super(accountingAllocator); + this.arena = Arena.ofShared(); + MemorySegment segment = arena.allocate(requestedSize); + this.allocatedAddress = segment.address(); + this.allocatedSize = requestedSize; + } + + @Override + public long getSize() { + return allocatedSize; + } + + @Override + protected long memoryAddress() { + return allocatedAddress; + } + + @Override + protected void release0() { + arena.close(); + } +} diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java new file mode 100644 index 0000000000..5fbdff8186 --- /dev/null +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.arrow.memory.util.MemoryUtilAccessor; + +/** + * {@link MemoryUtilAccessor} backed by {@code java.lang.foreign} ({@link MemorySegment}/{@link + * Arena}). Does not use {@code sun.misc.Unsafe} or reflection into {@code java.nio} internals, so + * it requires neither {@code --add-opens} nor {@code sun.misc.Unsafe} availability. + * + *

Required JVM flag. This accessor calls the restricted method {@link + * MemorySegment#reinterpret(long)}. That is a real, current requirement, not a caveat that this + * module already handles for you: classpath (unnamed-module) consumers must pass {@code + * --enable-native-access=ALL-UNNAMED} and module-path consumers must pass {@code + * --enable-native-access=org.apache.arrow.memory.ffm} on the JVM command line. Without it the JVM + * prints a warning on every restricted call today, and a future JDK that enables restricted-method + * enforcement by default will turn that into a hard {@link IllegalCallerException}. The {@code + * Enable-Native-Access: ALL-UNNAMED} entry in this module's jar manifest does not cover + * this: the JVM only honours that attribute in the manifest of the jar it was launched with via + * {@code java -jar}, never for a jar that is merely a classpath dependency. + * + *

{@link #allocateMemory}/{@link #freeMemory} are provided for standalone callers of {@link + * org.apache.arrow.memory.util.MemoryUtil#allocateMemory}/{@code #freeMemory}; each call gets its + * own {@link Arena}, tracked by address so {@link #freeMemory} can close the right one. + * Allocation-manager-owned memory instead goes through {@link FfmAllocationManager}, which holds + * its {@link Arena} directly rather than round-tripping through this map. + * + *

Note. Because freeing is arena-based rather than address-based, {@link #freeMemory} can + * only release addresses that came from this accessor's own {@link #allocateMemory}. An address + * from any other source (JNI, a foreign {@code malloc}, another accessor) is a silent no-op. This + * differs from the {@code sun.misc.Unsafe}-backed accessor, which frees any valid native address + * unconditionally. + */ +public final class FfmMemoryAccessor implements MemoryUtilAccessor { + + public static final MemoryUtilAccessor INSTANCE = new FfmMemoryAccessor(); + + private static final ConcurrentHashMap STANDALONE_ARENAS = new ConcurrentHashMap<>(); + + private FfmMemoryAccessor() {} + + private static MemorySegment segment(long address, long byteSize) { + return MemorySegment.ofAddress(address).reinterpret(byteSize); + } + + private static int checkedInt(long value) { + if (value < 0 || value > Integer.MAX_VALUE) { + throw new IllegalArgumentException("value out of int range: " + value); + } + return (int) value; + } + + /** + * Allocates {@code bytes} of native memory in a dedicated shared {@link Arena}, kept alive until + * {@link #freeMemory} is called with the returned address. + */ + @Override + public long allocateMemory(long bytes) { + Arena arena = Arena.ofShared(); + long address = arena.allocate(bytes).address(); + STANDALONE_ARENAS.put(address, arena); + return address; + } + + /** + * Frees memory previously returned by {@link #allocateMemory}. + * + * @implNote Only addresses obtained from this accessor's {@link #allocateMemory} are actually + * freed; the address is looked up in a map of owning arenas. An address from any other source + * (JNI, a foreign {@code malloc}, another accessor) is not tracked here and the call is a + * silent no-op. The {@code sun.misc.Unsafe}-backed accessor instead frees any valid native + * address unconditionally. + */ + @Override + public void freeMemory(long address) { + Arena arena = STANDALONE_ARENAS.remove(address); + if (arena != null) { + arena.close(); + } + } + + @Override + public byte getByte(long address) { + return segment(address, Byte.BYTES).get(ValueLayout.JAVA_BYTE, 0); + } + + @Override + public void putByte(long address, byte value) { + segment(address, Byte.BYTES).set(ValueLayout.JAVA_BYTE, 0, value); + } + + @Override + public short getShort(long address) { + return segment(address, Short.BYTES).get(ValueLayout.JAVA_SHORT_UNALIGNED, 0); + } + + @Override + public void putShort(long address, short value) { + segment(address, Short.BYTES).set(ValueLayout.JAVA_SHORT_UNALIGNED, 0, value); + } + + @Override + public int getInt(long address) { + return segment(address, Integer.BYTES).get(ValueLayout.JAVA_INT_UNALIGNED, 0); + } + + @Override + public void putInt(long address, int value) { + segment(address, Integer.BYTES).set(ValueLayout.JAVA_INT_UNALIGNED, 0, value); + } + + @Override + public long getLong(long address) { + return segment(address, Long.BYTES).get(ValueLayout.JAVA_LONG_UNALIGNED, 0); + } + + @Override + public void putLong(long address, long value) { + segment(address, Long.BYTES).set(ValueLayout.JAVA_LONG_UNALIGNED, 0, value); + } + + @Override + public void setMemory(long address, long bytes, byte value) { + segment(address, bytes).fill(value); + } + + @Override + public void copyMemory(long srcAddress, long destAddress, long bytes) { + MemorySegment.copy(segment(srcAddress, bytes), 0, segment(destAddress, bytes), 0, bytes); + } + + @Override + public void copyToMemory(byte[] src, long srcIndex, long destAddress, long bytes) { + MemorySegment.copy( + src, + checkedInt(srcIndex), + segment(destAddress, bytes), + ValueLayout.JAVA_BYTE, + 0, + checkedInt(bytes)); + } + + @Override + public void copyFromMemory(long srcAddress, byte[] dest, long destIndex, long bytes) { + MemorySegment.copy( + segment(srcAddress, bytes), + ValueLayout.JAVA_BYTE, + 0, + dest, + checkedInt(destIndex), + checkedInt(bytes)); + } + + @Override + public int getInt(byte[] bytes, int index) { + return MemorySegment.ofArray(bytes).get(ValueLayout.JAVA_INT_UNALIGNED, index); + } + + @Override + public long getLong(byte[] bytes, int index) { + return MemorySegment.ofArray(bytes).get(ValueLayout.JAVA_LONG_UNALIGNED, index); + } + + /** + * Returns the address of byte 0 of {@code buf}'s backing memory, independent of {@code buf}'s + * current position. + * + * @implNote {@code MemorySegment.ofBuffer(buf)} only spans {@code [position, limit)}, so its + * address shifts with the position. Callers (notably {@link + * org.apache.arrow.memory.ArrowBuf}) add {@code position()} or an explicit index on top of + * the returned address themselves, matching the {@code sun.misc.Unsafe}-backed accessor, + * which reads the raw {@code java.nio.Buffer.address} field. Clearing a duplicate (rather + * than {@code buf} itself, whose state must not change) resets position to 0 and limit to + * capacity so the segment spans the whole backing buffer from byte 0. + */ + @Override + public long getByteBufferAddress(ByteBuffer buf) { + return MemorySegment.ofBuffer(buf.duplicate().clear()).address(); + } + + @Override + public ByteBuffer directBuffer(long address, int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("Capacity is negative, has to be positive or 0"); + } + return segment(address, capacity).asByteBuffer(); + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerFfm.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerFfm.java new file mode 100644 index 0000000000..f70bca8bf8 --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerFfm.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.arrow.memory.AllocationManager; +import org.apache.arrow.memory.DefaultAllocationManagerOption; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** Test cases for {@link AllocationManager} type resolution. */ +public class TestAllocationManagerFfm { + + @AfterEach + public void after() { + // Avoid leaking this JVM-wide property into other tests sharing this Surefire fork. + System.clearProperty(DefaultAllocationManagerOption.ALLOCATION_MANAGER_TYPE_PROPERTY_NAME); + } + + @Test + public void testAllocationManagerType() { + System.setProperty(DefaultAllocationManagerOption.ALLOCATION_MANAGER_TYPE_PROPERTY_NAME, "FFM"); + DefaultAllocationManagerOption.AllocationManagerType mgrType = + DefaultAllocationManagerOption.getDefaultAllocationManagerType(); + + assertEquals(DefaultAllocationManagerOption.AllocationManagerType.FFM, mgrType); + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerTypeImpliesAccessor.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerTypeImpliesAccessor.java new file mode 100644 index 0000000000..9f03502e48 --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestAllocationManagerTypeImpliesAccessor.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.arrow.memory.util.MemoryUtil; +import org.junit.jupiter.api.Test; + +/** + * Verifies that setting only {@code arrow.allocation.manager.type=FFM} (with {@code + * arrow.memory.accessor.type} left unset) is enough for {@link MemoryUtil} to resolve to {@code + * FfmMemoryAccessor} on its own, so choosing the FFM allocation manager avoids {@code + * sun.misc.Unsafe} entirely without a second property. Runs in a dedicated Surefire execution (see + * memory-ffm/pom.xml) so the property is set before MemoryUtil's static init, and doesn't leak into + * this module's other tests. + */ +public class TestAllocationManagerTypeImpliesAccessor { + + @Test + public void allocationManagerTypeAloneSelectsFfmAccessor() { + assertEquals( + "org.apache.arrow.memory.ffm.FfmMemoryAccessor", MemoryUtil.getAccessorClassName()); + + long address = MemoryUtil.allocateMemory(8); + try { + MemoryUtil.putLong(address, 123456789L); + assertEquals(123456789L, MemoryUtil.getLong(address)); + } finally { + MemoryUtil.freeMemory(address); + } + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java new file mode 100644 index 0000000000..d82de7b275 --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import org.apache.arrow.memory.AllocationManager; +import org.apache.arrow.memory.ArrowBuf; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.BufferLedger; +import org.apache.arrow.memory.RootAllocator; +import org.junit.jupiter.api.Test; + +/** Test cases for {@link FfmAllocationManager}. */ +public class TestFfmAllocationManager { + + private BufferAllocator createFfmAllocator() { + return new RootAllocator( + RootAllocator.configBuilder() + .allocationManagerFactory(FfmAllocationManager.FACTORY) + .build()); + } + + private void readWriteArrowBuf(ArrowBuf buffer) { + for (long i = 0; i < buffer.capacity() / 8; i++) { + buffer.setLong(i * 8, i); + } + for (long i = 0; i < buffer.capacity() / 8; i++) { + assertEquals(i, buffer.getLong(i * 8)); + } + } + + @Test + public void testBufferAllocation() { + final long bufSize = 4096L; + try (BufferAllocator allocator = createFfmAllocator(); + ArrowBuf buffer = allocator.buffer(bufSize)) { + assertInstanceOf(BufferLedger.class, buffer.getReferenceManager()); + BufferLedger bufferLedger = (BufferLedger) buffer.getReferenceManager(); + + AllocationManager allocMgr = bufferLedger.getAllocationManager(); + assertInstanceOf(FfmAllocationManager.class, allocMgr); + FfmAllocationManager ffmMgr = (FfmAllocationManager) allocMgr; + + assertEquals(bufSize, ffmMgr.getSize()); + readWriteArrowBuf(buffer); + } + } + + @Test + public void testBufferIsFreedOnClose() { + try (BufferAllocator allocator = createFfmAllocator()) { + assertEquals(0, allocator.getAllocatedMemory()); + try (ArrowBuf buffer = allocator.buffer(1024)) { + assertEquals(1024, allocator.getAllocatedMemory()); + } + assertEquals(0, allocator.getAllocatedMemory()); + } + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmArrowBufByteBuffer.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmArrowBufByteBuffer.java new file mode 100644 index 0000000000..0584355d51 --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmArrowBufByteBuffer.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.ByteBuffer; +import org.apache.arrow.memory.ArrowBuf; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.memory.util.MemoryUtil; +import org.junit.jupiter.api.Test; + +/** + * Regression tests for {@link ArrowBuf}'s direct-{@link ByteBuffer} paths under the FFM accessor. + * + *

{@link ArrowBuf#getBytes(long, ByteBuffer)} and {@link ArrowBuf#setBytes(long, ByteBuffer)} + * add {@code position()} (or an explicit index) on top of what {@link + * MemoryUtil#getByteBufferAddress} returns, so that address must be position-independent. An + * earlier version of {@link FfmMemoryAccessor#getByteBufferAddress} returned {@code + * MemorySegment.ofBuffer(buf).address()}, which spans {@code [position, limit)} and therefore + * double-applied the offset: reads and writes silently landed at the wrong native address for any + * buffer with a non-zero position. Every buffer here is direct and positioned past 0 on purpose. + * + *

Runs in a Surefire execution with {@code arrow.memory.accessor.type=FFM} (see + * memory-ffm/pom.xml), since {@link ArrowBuf} reaches the accessor through {@link MemoryUtil}, + * whose choice of accessor is fixed at class-initialization time. + */ +public class TestFfmArrowBufByteBuffer { + + private static BufferAllocator createFfmAllocator() { + return new RootAllocator( + RootAllocator.configBuilder() + .allocationManagerFactory(FfmAllocationManager.FACTORY) + .build()); + } + + private static ByteBuffer directBufferOf(byte... values) { + ByteBuffer buf = ByteBuffer.allocateDirect(values.length); + buf.put(values); + buf.clear(); + return buf; + } + + @Test + public void accessorIsFfm() { + // Guards against this test silently exercising the Unsafe accessor instead. + assertEquals( + "FFM", System.getProperty(MemoryUtil.MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, "Unsafe")); + } + + @Test + public void setBytesFromPositionedDirectBuffer() { + ByteBuffer src = directBufferOf((byte) 10, (byte) 11, (byte) 12, (byte) 13, (byte) 14); + src.position(2); + + try (BufferAllocator allocator = createFfmAllocator(); + ArrowBuf buffer = allocator.buffer(8)) { + buffer.setZero(0, 8); + buffer.setBytes(0, src); + + assertEquals((byte) 12, buffer.getByte(0)); + assertEquals((byte) 13, buffer.getByte(1)); + assertEquals((byte) 14, buffer.getByte(2)); + assertEquals((byte) 0, buffer.getByte(3)); + assertEquals(5, src.position()); + } + } + + @Test + public void setBytesFromPositionedDirectBufferWithSrcIndex() { + ByteBuffer src = directBufferOf((byte) 20, (byte) 21, (byte) 22, (byte) 23, (byte) 24); + src.position(4); + + try (BufferAllocator allocator = createFfmAllocator(); + ArrowBuf buffer = allocator.buffer(8)) { + buffer.setZero(0, 8); + // srcIndex is absolute, so position() must not shift the source address. + buffer.setBytes(0, src, 1, 2); + + assertEquals((byte) 21, buffer.getByte(0)); + assertEquals((byte) 22, buffer.getByte(1)); + assertEquals((byte) 0, buffer.getByte(2)); + } + } + + @Test + public void getBytesIntoPositionedDirectBuffer() { + ByteBuffer dst = ByteBuffer.allocateDirect(8); + dst.position(5); + + try (BufferAllocator allocator = createFfmAllocator(); + ArrowBuf buffer = allocator.buffer(8)) { + for (int i = 0; i < 8; i++) { + buffer.setByte(i, (byte) (30 + i)); + } + buffer.getBytes(0, dst); + + assertEquals(8, dst.position()); + assertEquals((byte) 30, dst.get(5)); + assertEquals((byte) 31, dst.get(6)); + assertEquals((byte) 32, dst.get(7)); + // Bytes before the destination position must be untouched. + for (int i = 0; i < 5; i++) { + assertEquals((byte) 0, dst.get(i)); + } + } + } + + @Test + public void roundTripThroughPositionedDirectBuffers() { + ByteBuffer src = directBufferOf((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, (byte) 6); + src.position(3); + ByteBuffer dst = ByteBuffer.allocateDirect(6); + dst.position(3); + + try (BufferAllocator allocator = createFfmAllocator(); + ArrowBuf buffer = allocator.buffer(8)) { + buffer.setBytes(0, src); + buffer.getBytes(0, dst); + + assertEquals((byte) 4, dst.get(3)); + assertEquals((byte) 5, dst.get(4)); + assertEquals((byte) 6, dst.get(5)); + } + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java new file mode 100644 index 0000000000..e5ee8bc604 --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class TestFfmMemoryAccessor { + + private long address = 0; + + @AfterEach + public void after() { + if (address != 0) { + FfmMemoryAccessor.INSTANCE.freeMemory(address); + address = 0; + } + } + + @Test + public void putAndGetPrimitives() { + address = FfmMemoryAccessor.INSTANCE.allocateMemory(16); + + FfmMemoryAccessor.INSTANCE.putByte(address, (byte) 42); + assertEquals((byte) 42, FfmMemoryAccessor.INSTANCE.getByte(address)); + + FfmMemoryAccessor.INSTANCE.putShort(address + 2, (short) 1234); + assertEquals((short) 1234, FfmMemoryAccessor.INSTANCE.getShort(address + 2)); + + FfmMemoryAccessor.INSTANCE.putInt(address + 4, 123456789); + assertEquals(123456789, FfmMemoryAccessor.INSTANCE.getInt(address + 4)); + + FfmMemoryAccessor.INSTANCE.putLong(address + 8, 9876543210123L); + assertEquals(9876543210123L, FfmMemoryAccessor.INSTANCE.getLong(address + 8)); + } + + @Test + public void copyToAndFromMemoryRoundTrips() { + address = FfmMemoryAccessor.INSTANCE.allocateMemory(8); + byte[] src = {1, 2, 3, 4, 5, 6, 7, 8}; + FfmMemoryAccessor.INSTANCE.copyToMemory(src, 0, address, 8); + + byte[] dest = new byte[8]; + FfmMemoryAccessor.INSTANCE.copyFromMemory(address, dest, 0, 8); + assertArrayEquals(src, dest); + } + + @Test + public void copyMemoryBetweenAddresses() { + long src = FfmMemoryAccessor.INSTANCE.allocateMemory(8); + long dst = FfmMemoryAccessor.INSTANCE.allocateMemory(8); + try { + FfmMemoryAccessor.INSTANCE.putLong(src, 555L); + FfmMemoryAccessor.INSTANCE.copyMemory(src, dst, 8); + assertEquals(555L, FfmMemoryAccessor.INSTANCE.getLong(dst)); + } finally { + FfmMemoryAccessor.INSTANCE.freeMemory(dst); + FfmMemoryAccessor.INSTANCE.freeMemory(src); + } + } + + @Test + public void setMemoryFillsBytes() { + address = FfmMemoryAccessor.INSTANCE.allocateMemory(4); + FfmMemoryAccessor.INSTANCE.setMemory(address, 4, (byte) 0xAB); + for (int i = 0; i < 4; i++) { + assertEquals((byte) 0xAB, FfmMemoryAccessor.INSTANCE.getByte(address + i)); + } + } + + @Test + public void byteArrayIndexedAccessors() { + byte[] bytes = new byte[Long.BYTES]; + ByteBuffer.wrap(bytes).order(ByteOrder.nativeOrder()).putLong(0, 42L); + assertEquals(42L, FfmMemoryAccessor.INSTANCE.getLong(bytes, 0)); + + byte[] intBytes = new byte[Integer.BYTES * 2]; + ByteBuffer.wrap(intBytes).order(ByteOrder.nativeOrder()).putInt(0, 123456789); + ByteBuffer.wrap(intBytes).order(ByteOrder.nativeOrder()).putInt(Integer.BYTES, -987654321); + assertEquals(123456789, FfmMemoryAccessor.INSTANCE.getInt(intBytes, 0)); + assertEquals(-987654321, FfmMemoryAccessor.INSTANCE.getInt(intBytes, Integer.BYTES)); + } + + /** + * {@link FfmMemoryAccessor#getByteBufferAddress} must return the address of byte 0 of the backing + * memory regardless of the buffer's position, matching {@code UnsafeMemoryAccessor}: callers add + * {@code position()} themselves. It must also not disturb the caller's buffer state. + */ + @Test + public void getByteBufferAddressIsPositionIndependent() { + address = FfmMemoryAccessor.INSTANCE.allocateMemory(16); + ByteBuffer buf = FfmMemoryAccessor.INSTANCE.directBuffer(address, 16); + assertEquals(address, FfmMemoryAccessor.INSTANCE.getByteBufferAddress(buf)); + + buf.position(5); + buf.limit(12); + assertEquals(address, FfmMemoryAccessor.INSTANCE.getByteBufferAddress(buf)); + assertEquals(5, buf.position()); + assertEquals(12, buf.limit()); + + ByteBuffer slice = buf.slice(); + assertEquals(address + 5, FfmMemoryAccessor.INSTANCE.getByteBufferAddress(slice)); + slice.position(3); + assertEquals(address + 5, FfmMemoryAccessor.INSTANCE.getByteBufferAddress(slice)); + + ByteBuffer readOnly = buf.asReadOnlyBuffer(); + readOnly.position(7); + assertEquals(address, FfmMemoryAccessor.INSTANCE.getByteBufferAddress(readOnly)); + } + + @Test + public void directBufferRejectsNegativeCapacity() { + assertThrows( + IllegalArgumentException.class, () -> FfmMemoryAccessor.INSTANCE.directBuffer(1, -1)); + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessorWithoutAddOpens.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessorWithoutAddOpens.java new file mode 100644 index 0000000000..af47f53e77 --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessorWithoutAddOpens.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.junit.jupiter.api.Test; + +/** + * Verifies the direct-buffer paths of {@link FfmMemoryAccessor} work with no {@code --add-opens} + * JVM directive at all, i.e. without reflective access into {@code java.nio} internals. + * + *

This only proves anything when the JVM really is started without {@code + * --add-opens=java.base/java.nio=...}, so it runs in a dedicated Surefire execution (see + * memory-ffm/pom.xml) that overrides the inherited argLine to be empty. It is excluded from the + * default execution, which does inherit that flag. + */ +public class TestFfmMemoryAccessorWithoutAddOpens { + + @Test + public void directBufferRoundTripsWithoutReflection() { + long address = FfmMemoryAccessor.INSTANCE.allocateMemory(8); + try { + FfmMemoryAccessor.INSTANCE.putLong(address, 42L); + + ByteBuffer buf = FfmMemoryAccessor.INSTANCE.directBuffer(address, 8); + assertTrue(buf.isDirect()); + assertEquals(42L, buf.order(ByteOrder.nativeOrder()).getLong(0)); + assertEquals(address, FfmMemoryAccessor.INSTANCE.getByteBufferAddress(buf)); + + buf.position(4); + assertEquals(address, FfmMemoryAccessor.INSTANCE.getByteBufferAddress(buf)); + } finally { + FfmMemoryAccessor.INSTANCE.freeMemory(address); + } + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestMemoryUtilFfmWiring.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestMemoryUtilFfmWiring.java new file mode 100644 index 0000000000..b119576913 --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestMemoryUtilFfmWiring.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.arrow.memory.util.MemoryUtil; +import org.junit.jupiter.api.Test; + +/** + * Verifies MemoryUtil actually resolves to FfmMemoryAccessor when arrow.memory.accessor.type=FFM is + * set. Runs in a dedicated Surefire execution (see memory-ffm/pom.xml) so the property is set + * before MemoryUtil's static init, and doesn't leak into this module's other tests, which rely on + * calling FfmMemoryAccessor directly rather than through MemoryUtil. + */ +public class TestMemoryUtilFfmWiring { + + @Test + public void memoryUtilDelegatesToFfmAccessor() { + long address = MemoryUtil.allocateMemory(8); + try { + MemoryUtil.putLong(address, 123456789L); + assertEquals(123456789L, MemoryUtil.getLong(address)); + } finally { + MemoryUtil.freeMemory(address); + } + } +} diff --git a/memory/pom.xml b/memory/pom.xml index 4ea3d1f9ca..e3562f5d6b 100644 --- a/memory/pom.xml +++ b/memory/pom.xml @@ -34,4 +34,17 @@ under the License. memory-netty-buffer-patch memory-netty + + + + + arrow-memory-ffm + + [22,) + + + memory-ffm + + + From bd99d213170e28a51dba7e3f4175020540a84f7b Mon Sep 17 00:00:00 2001 From: Florian Bernard Date: Tue, 6 Oct 2026 10:08:13 +0200 Subject: [PATCH 3/7] fixup! [feat] add arrow-memory-ffm module [fix] fall back to unsafe on ffm linkage errors - An inferred FFM accessor (arrow.allocation.manager.type=FFM) now also falls back to Unsafe on LinkageError: UnsupportedClassVersionError on JDK 21 or earlier, or a failing FfmMemoryAccessor static initializer - resolveAccessor takes its inputs as parameters, so tests can simulate these errors without a pre-22 JDK - Addresses review comment r4192588927 --- .../apache/arrow/memory/util/MemoryUtil.java | 30 ++++++---- ...MemoryUtilInferredFfmAccessorFallback.java | 59 +++++++++++++++++++ 2 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilInferredFfmAccessorFallback.java diff --git a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java index f70418fe2e..b288c73030 100644 --- a/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java +++ b/memory/memory-core/src/main/java/org/apache/arrow/memory/util/MemoryUtil.java @@ -19,6 +19,7 @@ import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.function.Supplier; import org.apache.arrow.memory.DefaultAllocationManagerOption; import org.apache.arrow.util.VisibleForTesting; @@ -40,7 +41,11 @@ public class MemoryUtil { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MemoryUtil.class); - private static final MemoryUtilAccessor ACCESSOR = resolveAccessor(); + private static final MemoryUtilAccessor ACCESSOR = + resolveAccessor( + System.getProperty(MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, ""), + DefaultAllocationManagerOption.getDefaultAllocationManagerType(), + MemoryUtil::loadFfmAccessor); private MemoryUtil() {} @@ -50,33 +55,38 @@ public static String getAccessorClassName() { return ACCESSOR.getClass().getName(); } - private static MemoryUtilAccessor resolveAccessor() { - String type = System.getProperty(MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, ""); + @VisibleForTesting + static MemoryUtilAccessor resolveAccessor( + String type, + DefaultAllocationManagerOption.AllocationManagerType allocationManagerType, + Supplier ffmAccessorLoader) { if ("FFM".equals(type)) { logger.info( "{}=FFM, loading org.apache.arrow.memory.ffm.FfmMemoryAccessor", MEMORY_ACCESSOR_TYPE_PROPERTY_NAME); - return loadFfmAccessor(); + return ffmAccessorLoader.get(); } if (type.isEmpty() - && DefaultAllocationManagerOption.getDefaultAllocationManagerType() - == DefaultAllocationManagerOption.AllocationManagerType.FFM) { + && allocationManagerType == DefaultAllocationManagerOption.AllocationManagerType.FFM) { try { - MemoryUtilAccessor accessor = loadFfmAccessor(); + MemoryUtilAccessor accessor = ffmAccessorLoader.get(); logger.info( "{}=FFM, also loading org.apache.arrow.memory.ffm.FfmMemoryAccessor to avoid" + " sun.misc.Unsafe (override with {}=Unsafe if this is not wanted)", DefaultAllocationManagerOption.ALLOCATION_MANAGER_TYPE_PROPERTY_NAME, MEMORY_ACCESSOR_TYPE_PROPERTY_NAME); return accessor; - } catch (RuntimeException e) { + } catch (RuntimeException | LinkageError e) { // Unlike an explicit arrow.memory.accessor.type=FFM request, this preference is only // inferred from a different property, which may not even be load-bearing (e.g. a // caller-supplied custom AllocationManager.Factory that never reads it). Fall back // instead of poisoning MemoryUtil's for the rest of the JVM's life (JLS 12.4.2). + // LinkageError covers arrow-memory-ffm on a JDK older than 22 + // (UnsupportedClassVersionError) and a failing FfmMemoryAccessor static initializer. logger.warn( - "{}=FFM but arrow-memory-ffm is not on the classpath; falling back to Unsafe for {}" - + " (set {}=Unsafe to silence this warning)", + "{}=FFM but the FFM accessor could not be loaded (arrow-memory-ffm is missing or needs" + + " JDK 22+); falling back to Unsafe for {} (set {}=Unsafe to silence this" + + " warning)", DefaultAllocationManagerOption.ALLOCATION_MANAGER_TYPE_PROPERTY_NAME, MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, MEMORY_ACCESSOR_TYPE_PROPERTY_NAME, diff --git a/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilInferredFfmAccessorFallback.java b/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilInferredFfmAccessorFallback.java new file mode 100644 index 0000000000..09c693fb95 --- /dev/null +++ b/memory/memory-core/src/test/java/org/apache/arrow/memory/util/TestMemoryUtilInferredFfmAccessorFallback.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.util; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.apache.arrow.memory.DefaultAllocationManagerOption.AllocationManagerType; +import org.junit.jupiter.api.Test; + +/** + * When FFM is only inferred from arrow.allocation.manager.type=FFM, a failure to load the FFM + * accessor must fall back to Unsafe, including the {@link LinkageError}s a JVM throws for it. + */ +public class TestMemoryUtilInferredFfmAccessorFallback { + + @Test + public void fallsBackToUnsafeWhenFfmAccessorTargetsANewerJdk() { + // What JDK 21 and earlier throw when loading arrow-memory-ffm, which is compiled for release 22 + MemoryUtilAccessor accessor = + MemoryUtil.resolveAccessor( + "", + AllocationManagerType.FFM, + () -> { + throw new UnsupportedClassVersionError( + "org/apache/arrow/memory/ffm/FfmMemoryAccessor has been compiled by a more" + + " recent version of the Java Runtime (class file version 66.0)"); + }); + + assertSame(UnsafeMemoryAccessor.INSTANCE, accessor); + } + + @Test + public void fallsBackToUnsafeWhenFfmAccessorInitializationFails() { + MemoryUtilAccessor accessor = + MemoryUtil.resolveAccessor( + "", + AllocationManagerType.FFM, + () -> { + throw new ExceptionInInitializerError( + new IllegalCallerException("Illegal native access")); + }); + + assertSame(UnsafeMemoryAccessor.INSTANCE, accessor); + } +} From 9cdfe60f4f6e19f0137ad261a156748eb3508b30 Mon Sep 17 00:00:00 2001 From: Florian Bernard Date: Tue, 6 Oct 2026 10:08:13 +0200 Subject: [PATCH 4/7] fixup! [feat] add arrow-memory-ffm module [fix] init ffm allocation factory before empty buffer - Declare FfmAllocationManager.FACTORY before EMPTY: creating EMPTY can initialize BaseAllocator, whose default config reads FACTORY back while the class is still initializing, and got null - With only arrow-memory-ffm on the classpath, accessing FACTORY first failed with an NPE in BaseAllocator's static initializer - Add an isolated Surefire execution, since the test needs a fresh JVM --- memory/memory-ffm/pom.xml | 17 +++++++ .../memory/ffm/FfmAllocationManager.java | 10 +++-- ...fmAllocationManagerFactoryFirstAccess.java | 44 +++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManagerFactoryFirstAccess.java diff --git a/memory/memory-ffm/pom.xml b/memory/memory-ffm/pom.xml index c4eea71940..b4b821add5 100644 --- a/memory/memory-ffm/pom.xml +++ b/memory/memory-ffm/pom.xml @@ -74,6 +74,8 @@ under the License. **/TestAllocationManagerTypeImpliesAccessor.java **/TestFfmMemoryAccessorWithoutAddOpens.java + + **/TestFfmAllocationManagerFactoryFirstAccess.java @@ -131,6 +133,21 @@ under the License. + + + factory-first-access-test + + test + + test + + + + **/TestFfmAllocationManagerFactoryFirstAccess.java + + + diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java index a5ea333391..b042b688ea 100644 --- a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java @@ -26,10 +26,8 @@ /** Allocation manager based on the Foreign Function & Memory API. */ public final class FfmAllocationManager extends AllocationManager { - private static final Arena EMPTY_ARENA = Arena.ofShared(); - private static final ArrowBuf EMPTY = - new ArrowBuf(ReferenceManager.NO_OP, null, 0, EMPTY_ARENA.allocate(0).address()); - + // Must be initialized before EMPTY: creating an ArrowBuf may initialize BaseAllocator, whose + // default config can read FACTORY back while this class is still initializing. public static final AllocationManager.Factory FACTORY = new Factory() { @Override @@ -43,6 +41,10 @@ public ArrowBuf empty() { } }; + private static final Arena EMPTY_ARENA = Arena.ofShared(); + private static final ArrowBuf EMPTY = + new ArrowBuf(ReferenceManager.NO_OP, null, 0, EMPTY_ARENA.allocate(0).address()); + private final Arena arena; private final long allocatedSize; private final long allocatedAddress; diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManagerFactoryFirstAccess.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManagerFactoryFirstAccess.java new file mode 100644 index 0000000000..709e7e9d5c --- /dev/null +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManagerFactoryFirstAccess.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.arrow.memory.AllocationManager; +import org.apache.arrow.memory.ArrowBuf; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.junit.jupiter.api.Test; + +public class TestFfmAllocationManagerFactoryFirstAccess { + + @Test + public void factoryIsUsableWhenAccessedBeforeAnyAllocator() { + // This test runs in its own JVM (see memory-ffm/pom.xml Surefire execution) so that + // FfmAllocationManager is the first Arrow memory class initialized. Its static init creates an + // ArrowBuf, which initializes BaseAllocator, whose default config resolves the default factory: + // with only arrow-memory-ffm on the classpath, that is FfmAllocationManager.FACTORY itself. + AllocationManager.Factory factory = FfmAllocationManager.FACTORY; + + try (BufferAllocator allocator = + new RootAllocator( + RootAllocator.configBuilder().allocationManagerFactory(factory).build()); + ArrowBuf buf = allocator.buffer(64)) { + assertEquals(64, buf.capacity()); + } + } +} From c6800b90d8dfb840f237dd2d9b6c6b12babe7770 Mon Sep 17 00:00:00 2001 From: Florian Bernard Date: Tue, 6 Oct 2026 12:32:04 +0200 Subject: [PATCH 5/7] fixup! [feat] add arrow-memory-ffm module [fix] allocate ffm buffers with malloc and free - Replace one shared Arena per buffer with malloc/free downcalls through the foreign linker, in FfmAllocationManager and FfmMemoryAccessor - Closing a shared Arena handshakes with every JVM thread: allocating and releasing 4 KiB drops from 8.8 us to 97 ns (Unsafe: 90 ns), and from 58.8 us to 99 ns with 256 idle threads - Memory is no longer zeroed on allocation, like the Unsafe and Netty managers - FfmMemoryAccessor.freeMemory now frees any malloc address, which removes the address-to-arena map and its silent no-op for foreign addresses - A failed malloc throws OutOfMemoryError, as Arena.allocate and Unsafe do - Addresses review comment r4192653585 --- .../memory/ffm/FfmAllocationManager.java | 17 ++-- .../arrow/memory/ffm/FfmMemoryAccessor.java | 55 ++++-------- .../apache/arrow/memory/ffm/NativeMemory.java | 83 +++++++++++++++++++ .../memory/ffm/TestFfmAllocationManager.java | 10 +++ 4 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java index b042b688ea..20f44d3590 100644 --- a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmAllocationManager.java @@ -16,14 +16,15 @@ */ package org.apache.arrow.memory.ffm; -import java.lang.foreign.Arena; -import java.lang.foreign.MemorySegment; import org.apache.arrow.memory.AllocationManager; import org.apache.arrow.memory.ArrowBuf; import org.apache.arrow.memory.BufferAllocator; import org.apache.arrow.memory.ReferenceManager; -/** Allocation manager based on the Foreign Function & Memory API. */ +/** + * Allocation manager based on the Foreign Function & Memory API: buffers are allocated with the + * C library's {@code malloc} and freed with {@code free}, both called through the foreign linker. + */ public final class FfmAllocationManager extends AllocationManager { // Must be initialized before EMPTY: creating an ArrowBuf may initialize BaseAllocator, whose @@ -41,19 +42,15 @@ public ArrowBuf empty() { } }; - private static final Arena EMPTY_ARENA = Arena.ofShared(); private static final ArrowBuf EMPTY = - new ArrowBuf(ReferenceManager.NO_OP, null, 0, EMPTY_ARENA.allocate(0).address()); + new ArrowBuf(ReferenceManager.NO_OP, null, 0, NativeMemory.allocate(0)); - private final Arena arena; private final long allocatedSize; private final long allocatedAddress; FfmAllocationManager(BufferAllocator accountingAllocator, long requestedSize) { super(accountingAllocator); - this.arena = Arena.ofShared(); - MemorySegment segment = arena.allocate(requestedSize); - this.allocatedAddress = segment.address(); + this.allocatedAddress = NativeMemory.allocate(requestedSize); this.allocatedSize = requestedSize; } @@ -69,6 +66,6 @@ protected long memoryAddress() { @Override protected void release0() { - arena.close(); + NativeMemory.free(allocatedAddress); } } diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java index 5fbdff8186..6092209bdc 100644 --- a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java @@ -16,20 +16,20 @@ */ package org.apache.arrow.memory.ffm; -import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.nio.ByteBuffer; -import java.util.concurrent.ConcurrentHashMap; import org.apache.arrow.memory.util.MemoryUtilAccessor; /** - * {@link MemoryUtilAccessor} backed by {@code java.lang.foreign} ({@link MemorySegment}/{@link - * Arena}). Does not use {@code sun.misc.Unsafe} or reflection into {@code java.nio} internals, so - * it requires neither {@code --add-opens} nor {@code sun.misc.Unsafe} availability. + * {@link MemoryUtilAccessor} backed by {@code java.lang.foreign} ({@link MemorySegment} and the + * foreign {@link java.lang.foreign.Linker}). Does not use {@code sun.misc.Unsafe} or reflection + * into {@code java.nio} internals, so it requires neither {@code --add-opens} nor {@code + * sun.misc.Unsafe} availability. * - *

Required JVM flag. This accessor calls the restricted method {@link - * MemorySegment#reinterpret(long)}. That is a real, current requirement, not a caveat that this + *

Required JVM flag. This accessor calls the restricted methods {@link + * MemorySegment#reinterpret(long)} and {@link java.lang.foreign.Linker#downcallHandle}, the latter + * for {@code malloc} and {@code free}. That is a real, current requirement, not a caveat that this * module already handles for you: classpath (unnamed-module) consumers must pass {@code * --enable-native-access=ALL-UNNAMED} and module-path consumers must pass {@code * --enable-native-access=org.apache.arrow.memory.ffm} on the JVM command line. Without it the JVM @@ -39,24 +39,14 @@ * this: the JVM only honours that attribute in the manifest of the jar it was launched with via * {@code java -jar}, never for a jar that is merely a classpath dependency. * - *

{@link #allocateMemory}/{@link #freeMemory} are provided for standalone callers of {@link - * org.apache.arrow.memory.util.MemoryUtil#allocateMemory}/{@code #freeMemory}; each call gets its - * own {@link Arena}, tracked by address so {@link #freeMemory} can close the right one. - * Allocation-manager-owned memory instead goes through {@link FfmAllocationManager}, which holds - * its {@link Arena} directly rather than round-tripping through this map. - * - *

Note. Because freeing is arena-based rather than address-based, {@link #freeMemory} can - * only release addresses that came from this accessor's own {@link #allocateMemory}. An address - * from any other source (JNI, a foreign {@code malloc}, another accessor) is a silent no-op. This - * differs from the {@code sun.misc.Unsafe}-backed accessor, which frees any valid native address - * unconditionally. + *

{@link #allocateMemory}/{@link #freeMemory} call {@code malloc}/{@code free} directly, like + * the {@code sun.misc.Unsafe}-backed accessor, so {@link #freeMemory} accepts any address returned + * by {@code malloc}. */ public final class FfmMemoryAccessor implements MemoryUtilAccessor { public static final MemoryUtilAccessor INSTANCE = new FfmMemoryAccessor(); - private static final ConcurrentHashMap STANDALONE_ARENAS = new ConcurrentHashMap<>(); - private FfmMemoryAccessor() {} private static MemorySegment segment(long address, long byteSize) { @@ -70,33 +60,16 @@ private static int checkedInt(long value) { return (int) value; } - /** - * Allocates {@code bytes} of native memory in a dedicated shared {@link Arena}, kept alive until - * {@link #freeMemory} is called with the returned address. - */ + /** Allocates {@code bytes} of uninitialized native memory with the C library's {@code malloc}. */ @Override public long allocateMemory(long bytes) { - Arena arena = Arena.ofShared(); - long address = arena.allocate(bytes).address(); - STANDALONE_ARENAS.put(address, arena); - return address; + return NativeMemory.allocate(bytes); } - /** - * Frees memory previously returned by {@link #allocateMemory}. - * - * @implNote Only addresses obtained from this accessor's {@link #allocateMemory} are actually - * freed; the address is looked up in a map of owning arenas. An address from any other source - * (JNI, a foreign {@code malloc}, another accessor) is not tracked here and the call is a - * silent no-op. The {@code sun.misc.Unsafe}-backed accessor instead frees any valid native - * address unconditionally. - */ + /** Frees native memory with the C library's {@code free}. */ @Override public void freeMemory(long address) { - Arena arena = STANDALONE_ARENAS.remove(address); - if (arena != null) { - arena.close(); - } + NativeMemory.free(address); } @Override diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java new file mode 100644 index 0000000000..cc493846a3 --- /dev/null +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.memory.ffm; + +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; + +/** + * Native memory from the C library's {@code malloc} and {@code free}, called through the foreign + * {@link Linker}. + * + *

An {@link java.lang.foreign.Arena} per buffer is avoided on purpose: closing a shared arena + * handshakes with every JVM thread, which makes freeing orders of magnitude slower than {@code + * sun.misc.Unsafe}, and its allocations are always zeroed. + */ +final class NativeMemory { + + private static final Linker LINKER = Linker.nativeLinker(); + private static final MethodHandle MALLOC = + LINKER.downcallHandle( + find("malloc"), FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)); + private static final MethodHandle FREE = + LINKER.downcallHandle(find("free"), FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + + private NativeMemory() {} + + private static MemorySegment find(String name) { + return LINKER + .defaultLookup() + .find(name) + .orElseThrow(() -> new IllegalStateException("C library function not found: " + name)); + } + + /** + * Returns the address of {@code bytes} of uninitialized native memory. + * + * @throws OutOfMemoryError if {@code malloc} cannot allocate them, like {@code + * sun.misc.Unsafe#allocateMemory} + */ + static long allocate(long bytes) { + long address; + try { + address = ((MemorySegment) MALLOC.invokeExact(bytes)).address(); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new IllegalStateException(t); + } + // malloc(0) may legitimately return NULL + if (address == 0 && bytes != 0) { + throw new OutOfMemoryError("Unable to allocate " + bytes + " bytes of native memory"); + } + return address; + } + + /** Frees memory returned by {@link #allocate}, or by any other call to {@code malloc}. */ + static void free(long address) { + try { + FREE.invokeExact(MemorySegment.ofAddress(address)); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new IllegalStateException(t); + } + } +} diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java index d82de7b275..59c7b53072 100644 --- a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmAllocationManager.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.apache.arrow.memory.AllocationManager; import org.apache.arrow.memory.ArrowBuf; @@ -72,4 +73,13 @@ public void testBufferIsFreedOnClose() { assertEquals(0, allocator.getAllocatedMemory()); } } + + @Test + public void failedNativeAllocationThrowsOutOfMemoryError() { + // 4 EiB is beyond any platform's address space, so the native allocation must fail + try (BufferAllocator allocator = createFfmAllocator()) { + assertThrows( + OutOfMemoryError.class, () -> FfmAllocationManager.FACTORY.create(allocator, 1L << 62)); + } + } } From d341ba900b5781477221b3db9d3f91ea10b47556 Mon Sep 17 00:00:00 2001 From: Florian Bernard Date: Wed, 7 Oct 2026 16:57:53 +0200 Subject: [PATCH 6/7] fixup! [feat] add arrow-memory-ffm module [docs] explain int checks in byte array copies - Note why checkedInt narrows byte[] indexes and lengths to int: MemorySegment.copy takes ints there, so a valid call always fits - Addresses review comment r4207204760 --- .../main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java index 6092209bdc..3f96363046 100644 --- a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/FfmMemoryAccessor.java @@ -53,6 +53,7 @@ private static MemorySegment segment(long address, long byteSize) { return MemorySegment.ofAddress(address).reinterpret(byteSize); } + // byte[] indexes and lengths are ints in MemorySegment.copy, so a valid call always fits. private static int checkedInt(long value) { if (value < 0 || value > Integer.MAX_VALUE) { throw new IllegalArgumentException("value out of int range: " + value); From 3a03de6e8dc8d22b37ada44611232b8c7a44810c Mon Sep 17 00:00:00 2001 From: Florian Bernard Date: Wed, 7 Oct 2026 17:02:11 +0200 Subject: [PATCH 7/7] fixup! [feat] add arrow-memory-ffm module [fix] reject negative ffm allocation sizes - NativeMemory.allocate throws IllegalArgumentException for a negative size, like Unsafe.allocateMemory, instead of passing it to malloc as a huge size_t and throwing OutOfMemoryError - Addresses review comment r4207224636 --- .../main/java/org/apache/arrow/memory/ffm/NativeMemory.java | 5 +++++ .../org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java index cc493846a3..6e4563e49c 100644 --- a/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java +++ b/memory/memory-ffm/src/main/java/org/apache/arrow/memory/ffm/NativeMemory.java @@ -51,10 +51,15 @@ private static MemorySegment find(String name) { /** * Returns the address of {@code bytes} of uninitialized native memory. * + * @throws IllegalArgumentException if {@code bytes} is negative, like {@code + * sun.misc.Unsafe#allocateMemory} * @throws OutOfMemoryError if {@code malloc} cannot allocate them, like {@code * sun.misc.Unsafe#allocateMemory} */ static long allocate(long bytes) { + if (bytes < 0) { + throw new IllegalArgumentException("Negative allocation size: " + bytes); + } long address; try { address = ((MemorySegment) MALLOC.invokeExact(bytes)).address(); diff --git a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java index e5ee8bc604..04ded0712a 100644 --- a/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java +++ b/memory/memory-ffm/src/test/java/org/apache/arrow/memory/ffm/TestFfmMemoryAccessor.java @@ -133,4 +133,10 @@ public void directBufferRejectsNegativeCapacity() { assertThrows( IllegalArgumentException.class, () -> FfmMemoryAccessor.INSTANCE.directBuffer(1, -1)); } + + @Test + public void allocateMemoryRejectsNegativeSize() { + assertThrows( + IllegalArgumentException.class, () -> FfmMemoryAccessor.INSTANCE.allocateMemory(-1)); + } }