diff --git a/src/main/docs/architecture-overview.adoc b/src/main/docs/architecture-overview.adoc index 44dce67..54b3eb0 100644 --- a/src/main/docs/architecture-overview.adoc +++ b/src/main/docs/architecture-overview.adoc @@ -43,3 +43,7 @@ When the VM cannot be recognised, `UnknownJvmStringHash` provides a defensive fa * `net.openhft.hashing.Primitives` houses byte-order normalisation helpers and unsigned conversions so algorithms can expect canonical little-endian operands even on big-endian hardware (`Primitives.java`). * `net.openhft.hashing.Maths` provides low-level arithmetic helpers such as `unsignedLongMulXorFold` used by wyHash and XXH3 for 128-bit cross-products (`Maths.java`). * Tests under `src/test/java/net/openhft/hashing` validate the API contract across arrays, primitives, buffers, and custom access strategies, and serve as reference snippets for typical `Access` usage. + +== Backend selection + +`RuntimeSupport` caches the JVM feature version; `HeapAccess` then chooses `UnsafeAccess` (JDK < 25) or `SafeHeapAccess` (JDK 25+) once at class initialisation. See ADR-007 in the decision log. diff --git a/src/main/docs/decision-log.adoc b/src/main/docs/decision-log.adoc index c47c412..e3fef4e 100644 --- a/src/main/docs/decision-log.adoc +++ b/src/main/docs/decision-log.adoc @@ -82,3 +82,22 @@ Chronicle Software * **Positive:** Supports >64-bit hashes without allocating an Object wrapper (like `BigInteger` or a custom class). * **Negative:** The `long[]` result array must be managed/reused by the caller to maintain the "zero-allocation" promise. |=== + +== ADR-007: Unsafe-free heap access on JDK 25+ + +[cols="1,3"] +|=== +| Status | Accepted +| Date | 2025-11-01 +| Context | `sun.misc.Unsafe` memory access is being phased out. JDK 25+ should never instantiate it, while JDK 8-24 (HotSpot, IBM Semeru/OpenJ9, AIX ppc64, Linux ppc64le) keep today's fast path. The feature version is constant for the lifetime of a JVM. +| Decision | +* `RuntimeSupport` caches the feature version once (static final); `useUnsafeAccess()` is true below 25. The system property `net.openhft.hashing.useUnsafe=false` forces the safe backend on older JDKs for testing. +* `HeapAccess` selects the array backend once: `UnsafeAccess` (real base offsets) or `SafeHeapAccess` (base offset 0, native-order byte view built on array indexing, no allocation). +* `UnsafeAccess` refuses to initialise when the safe backend is selected, so Unsafe is never instantiated on JDK 25+. +* On JDK 25+, strings use `UnknownJvmStringHash` (`CharSequence` reads) and `ByteBuffer` uses `ByteBufferAccess`. +* `hashMemory` uses `ForeignRawAccess`, a direct translation of `Unsafe.getX(null, address)` to the FFM API (JDK 22+, via method handles so the Java 8 build is unchanged). If FFM is unavailable it throws `UnsupportedOperationException`. +* `Access.unsafe()` throws on the safe backend: object plus field-offset access cannot be expressed without Unsafe. +| Consequences | +* **Positive:** Hash values are identical on every backend; zero allocation is retained; no behaviour change below JDK 25. +* **Negative:** The safe backend composes wide reads from bytes and is slower; raw-memory hashing is unavailable on JDK 25+. +|=== diff --git a/src/main/docs/unsafe-and-platform-notes.adoc b/src/main/docs/unsafe-and-platform-notes.adoc index 492a6eb..9c0ab33 100644 --- a/src/main/docs/unsafe-and-platform-notes.adoc +++ b/src/main/docs/unsafe-and-platform-notes.adoc @@ -32,3 +32,11 @@ Hash outputs therefore match across architectures, but big-endian CPUs incur add This ensures correctness at the cost of potential performance; review the decision log before altering the detection logic (`Util.java:29-63`). * Passing invalid pointers to `hashMemory` or misreporting `Access` byte order is undefined and can crash the JVM. Always validate foreign memory addresses and keep access strategies consistent with the underlying storage layout. + +=== JDK 25+ and the Unsafe-free backend + +* `RuntimeSupport.useUnsafeAccess()` is false on JDK 25+ (and when `-Dnet.openhft.hashing.useUnsafe=false` is set); `UnsafeAccess` then throws `IllegalStateException` if touched, so `sun.misc.Unsafe` is never instantiated. +* `HeapAccess` supplies array base offsets and the `Access` used by `LongHashFunction`, `LongTupleHashFunction` and `XXH3`; the safe variant (`SafeHeapAccess`) uses base offset 0 and native byte order, so big-endian hosts (AIX ppc64) still normalise through `Access.byteOrder`. +* `hashMemory` uses `ForeignRawAccess` (FFM, JDK 22+). It calls the restricted `MemorySegment.reinterpret`; JDK 24+ warns once unless `--enable-native-access=ALL-UNNAMED` is set. Without FFM it throws `UnsupportedOperationException`. +* `Access.unsafe()` throws `UnsupportedOperationException` on this backend; direct `ByteBuffer`s are hashed through `ByteBufferAccess`. +* Strings use `UnknownJvmStringHash`; this applies equally to HotSpot and IBM Semeru/OpenJ9. diff --git a/src/main/java/net/openhft/hashing/Access.java b/src/main/java/net/openhft/hashing/Access.java index dbe0f05..6bff514 100644 --- a/src/main/java/net/openhft/hashing/Access.java +++ b/src/main/java/net/openhft/hashing/Access.java @@ -66,9 +66,12 @@ public abstract class Access { * * @param the type of objects to access * @return the unsafe memory {@code Access} + * @throws UnsupportedOperationException on JDK 25+ (or with + * {@code -Dnet.openhft.hashing.useUnsafe=false}), where Unsafe is never instantiated */ @SuppressWarnings("unchecked") public static Access unsafe() { + HeapAccess.requireUnsafe(); return (Access) UnsafeAccess.INSTANCE; } diff --git a/src/main/java/net/openhft/hashing/ForeignRawAccess.java b/src/main/java/net/openhft/hashing/ForeignRawAccess.java new file mode 100644 index 0000000..1c005a0 --- /dev/null +++ b/src/main/java/net/openhft/hashing/ForeignRawAccess.java @@ -0,0 +1,151 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.hashing; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.nio.ByteOrder; + +/** + * Raw native-address {@link Access} built on the Foreign Function and Memory API (final in + * JDK 22), used instead of {@code sun.misc.Unsafe} where Unsafe is not used. + * + *

The API is reached through method handles so that the library still compiles for Java 8 and + * ships as a single artifact. The handles are bound to one process-wide segment spanning the whole + * address space ({@code MemorySegment.NULL.reinterpret(Long.MAX_VALUE)}), so the "offset" of an + * access is the absolute address, exactly like {@code Unsafe.getLong(null, address)}. The handles + * are static finals and so are constant-folded by the JIT; reads allocate nothing. + * + *

{@code reinterpret} is a restricted method: JDK 24+ prints a one-off warning unless + * {@code --enable-native-access=ALL-UNNAMED} is given. If the API is missing or refused, + * {@link #INSTANCE} is {@code null} and raw memory stays unsupported. + * + *

The {@code input} argument is ignored and must be {@code null}; this class is not exposed + * publicly because object plus field-offset access (see {@link Access#unsafe()}) cannot be + * expressed with it. Addresses at or above {@code 2^63} are not supported. + */ +final class ForeignRawAccess extends Access { + static final ForeignRawAccess INSTANCE; + + private static final MethodHandle GET_LONG; + private static final MethodHandle GET_INT; + private static final MethodHandle GET_SHORT; + private static final MethodHandle GET_BYTE; + private static final Access NON_NATIVE; + + static { + MethodHandle l = null; + MethodHandle i = null; + MethodHandle s = null; + MethodHandle b = null; + if (RuntimeSupport.isAtLeast(22)) { + try { + final Class segmentClass = Class.forName("java.lang.foreign.MemorySegment"); + final Class layouts = Class.forName("java.lang.foreign.ValueLayout"); + final Object global = segmentClass.getMethod("reinterpret", long.class) + .invoke(segmentClass.getField("NULL").get(null), Long.MAX_VALUE); + l = bind(segmentClass, layouts, global, "JAVA_LONG_UNALIGNED", "OfLong", long.class); + i = bind(segmentClass, layouts, global, "JAVA_INT_UNALIGNED", "OfInt", int.class); + s = bind(segmentClass, layouts, global, "JAVA_SHORT_UNALIGNED", "OfShort", short.class); + b = bind(segmentClass, layouts, global, "JAVA_BYTE", "OfByte", byte.class); + } catch (final Throwable ignore) { + l = null; + } + } + final boolean ok = l != null && i != null && s != null && b != null; + GET_LONG = ok ? l : null; + GET_INT = ok ? i : null; + GET_SHORT = ok ? s : null; + GET_BYTE = ok ? b : null; + INSTANCE = ok ? new ForeignRawAccess() : null; + NON_NATIVE = ok ? Access.newDefaultReverseAccess(INSTANCE) : null; + } + + private ForeignRawAccess() {} + + /** Returns a {@code (long address) -> value} handle reading with the given native layout. */ + private static MethodHandle bind(final Class segmentClass, final Class layouts, + final Object global, final String layoutField, + final String layoutType, final Class valueType) + throws ReflectiveOperationException { + final Object layout = layouts.getField(layoutField).get(null); + final Class layoutClass = Class.forName("java.lang.foreign.ValueLayout$" + layoutType); + final Method get = segmentClass.getMethod("get", layoutClass, long.class); + final MethodHandle handle = MethodHandles.lookup().unreflect(get); + return MethodHandles.insertArguments(handle, 0, global, layout) + .asType(MethodType.methodType(valueType, long.class)); + } + + @Override + public long getLong(final Object input, final long offset) { + try { + return (long) GET_LONG.invokeExact(offset); + } catch (final Throwable t) { + throw rethrow(t); + } + } + + @Override + public long getUnsignedInt(final Object input, final long offset) { + return Primitives.unsignedInt(getInt(input, offset)); + } + + @Override + public int getInt(final Object input, final long offset) { + try { + return (int) GET_INT.invokeExact(offset); + } catch (final Throwable t) { + throw rethrow(t); + } + } + + @Override + public int getUnsignedShort(final Object input, final long offset) { + return Primitives.unsignedShort(getShort(input, offset)); + } + + @Override + public int getShort(final Object input, final long offset) { + try { + return (short) GET_SHORT.invokeExact(offset); + } catch (final Throwable t) { + throw rethrow(t); + } + } + + @Override + public int getUnsignedByte(final Object input, final long offset) { + return Primitives.unsignedByte(getByte(input, offset)); + } + + @Override + public int getByte(final Object input, final long offset) { + try { + return (byte) GET_BYTE.invokeExact(offset); + } catch (final Throwable t) { + throw rethrow(t); + } + } + + private static RuntimeException rethrow(final Throwable t) { + if (t instanceof RuntimeException) { + return (RuntimeException) t; + } else if (t instanceof Error) { + throw (Error) t; + } + return new IllegalStateException(t); + } + + @Override + public ByteOrder byteOrder(final Object input) { + return ByteOrder.nativeOrder(); + } + + @Override + protected Access reverseAccess() { + return NON_NATIVE; + } +} diff --git a/src/main/java/net/openhft/hashing/HeapAccess.java b/src/main/java/net/openhft/hashing/HeapAccess.java new file mode 100644 index 0000000..de4098b --- /dev/null +++ b/src/main/java/net/openhft/hashing/HeapAccess.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.hashing; + +/** + * One-time selection of the array access backend. + * + *

When {@link RuntimeSupport#useUnsafeAccess()} is false (JDK 25+ or forced by property) this + * class never touches {@link UnsafeAccess}, so {@code sun.misc.Unsafe} is never instantiated. + * Raw-memory and {@code DirectBuffer} fast paths are only available with Unsafe. + */ +final class HeapAccess { + static final boolean UNSAFE_ENABLED = RuntimeSupport.useUnsafeAccess(); + + static final Access ACCESS; + + /** Access for absolute native addresses, or {@code null} if raw memory is unsupported. */ + static final Access RAW_MEMORY_ACCESS; + static final boolean RAW_MEMORY_AVAILABLE; + + static final long BOOLEAN_BASE; + static final long BYTE_BASE; + static final long CHAR_BASE; + static final long SHORT_BASE; + static final long INT_BASE; + static final long LONG_BASE; + + static final byte TRUE_BYTE_VALUE; + static final byte FALSE_BYTE_VALUE; + + static { + if (UNSAFE_ENABLED) { + ACCESS = UnsafeAccess.INSTANCE; + RAW_MEMORY_ACCESS = UnsafeAccess.INSTANCE; + BOOLEAN_BASE = UnsafeAccess.BOOLEAN_BASE; + BYTE_BASE = UnsafeAccess.BYTE_BASE; + CHAR_BASE = UnsafeAccess.CHAR_BASE; + SHORT_BASE = UnsafeAccess.SHORT_BASE; + INT_BASE = UnsafeAccess.INT_BASE; + LONG_BASE = UnsafeAccess.LONG_BASE; + TRUE_BYTE_VALUE = UnsafeAccess.TRUE_BYTE_VALUE; + FALSE_BYTE_VALUE = UnsafeAccess.FALSE_BYTE_VALUE; + } else { + ACCESS = SafeHeapAccess.INSTANCE; + RAW_MEMORY_ACCESS = ForeignRawAccess.INSTANCE; + BOOLEAN_BASE = 0L; + BYTE_BASE = 0L; + CHAR_BASE = 0L; + SHORT_BASE = 0L; + INT_BASE = 0L; + LONG_BASE = 0L; + TRUE_BYTE_VALUE = 1; + FALSE_BYTE_VALUE = 0; + } + } + + static { + RAW_MEMORY_AVAILABLE = RAW_MEMORY_ACCESS != null; + } + + private HeapAccess() {} + + /** Returns the access for {@code hashMemory}; fails fast if no raw memory backend exists. */ + static Access rawMemoryAccess() { + if (RAW_MEMORY_ACCESS == null) { + throw new UnsupportedOperationException( + "Raw memory access needs sun.misc.Unsafe (JDK < 25) or the FFM API (JDK 22+), " + + "neither is usable on this JVM"); + } + return RAW_MEMORY_ACCESS; + } + + /** Guards {@link Access#unsafe()}: object plus field-offset access needs real Unsafe. */ + static void requireUnsafe() { + if (!UNSAFE_ENABLED) { + throw new UnsupportedOperationException( + "Access.unsafe() requires sun.misc.Unsafe, which is not used on this JVM"); + } + } +} diff --git a/src/main/java/net/openhft/hashing/LongHashFunction.java b/src/main/java/net/openhft/hashing/LongHashFunction.java index 85565de..6982ba9 100644 --- a/src/main/java/net/openhft/hashing/LongHashFunction.java +++ b/src/main/java/net/openhft/hashing/LongHashFunction.java @@ -11,7 +11,7 @@ import java.nio.ByteOrder; import static net.openhft.hashing.CharSequenceAccess.nativeCharSequenceAccess; -import static net.openhft.hashing.UnsafeAccess.*; +import static net.openhft.hashing.HeapAccess.*; import static net.openhft.hashing.Util.VALID_STRING_HASH; import static net.openhft.hashing.Util.checkArrayOffs; @@ -499,8 +499,13 @@ protected LongHashFunction() { */ public abstract long hash(T input, Access access, long off, long len); + /* + * Historical name: this hashes through HeapAccess.ACCESS, which is Unsafe based only below + * JDK 25. On JDK 25+ (or useUnsafe=false) it is the Unsafe-free SafeHeapAccess; only the + * null-input (raw address) case needs real Unsafe and then fails fast. + */ private long unsafeHash(Object input, long off, long len) { - return hash(input, UnsafeAccess.INSTANCE, off, len); + return hash(input, input == null ? HeapAccess.rawMemoryAccess() : HeapAccess.ACCESS, off, len); } /** @@ -606,7 +611,7 @@ public long hashBytes(@NotNull ByteBuffer input, int off, int len) { private long hashByteBuffer(@NotNull ByteBuffer input, int off, int len) { if (input.hasArray()) { return unsafeHash(input.array(), BYTE_BASE + input.arrayOffset() + off, len); - } else if (input instanceof DirectBuffer) { + } else if (UNSAFE_ENABLED && input instanceof DirectBuffer) { return unsafeHash(null, ((DirectBuffer) input).address() + off, len); } else { return hash(input, ByteBufferAccess.INSTANCE, off, len); @@ -622,6 +627,9 @@ private long hashByteBuffer(@NotNull ByteBuffer input, int off, int len) { * @param address the address of the first byte to hash * @param len length of the byte sequence to hash * @return hash code for the specified byte sequence + * @throws UnsupportedOperationException on JDK 25+ (or with + * {@code -Dnet.openhft.hashing.useUnsafe=false}); hash a direct {@link ByteBuffer} + * via {@link #hashBytes(ByteBuffer)} instead */ public long hashMemory(long address, long len) { return unsafeHash(null, address, len); diff --git a/src/main/java/net/openhft/hashing/LongTupleHashFunction.java b/src/main/java/net/openhft/hashing/LongTupleHashFunction.java index c01a3b0..1c925ba 100644 --- a/src/main/java/net/openhft/hashing/LongTupleHashFunction.java +++ b/src/main/java/net/openhft/hashing/LongTupleHashFunction.java @@ -13,7 +13,7 @@ import java.nio.ByteOrder; import static net.openhft.hashing.CharSequenceAccess.nativeCharSequenceAccess; -import static net.openhft.hashing.UnsafeAccess.*; +import static net.openhft.hashing.HeapAccess.*; import static net.openhft.hashing.Util.*; /** @@ -601,6 +601,8 @@ public long[] hashBytes(final ByteBuffer input, final int off, final int len) { * @throws IllegalArgumentException if {@code result.length < newResultArray().length} * @throws IllegalArgumentException if {@code off < 0} or {@code off + len > input.length} * or {@code len < 0} + * @throws UnsupportedOperationException on JDK 25+ (or with + * {@code -Dnet.openhft.hashing.useUnsafe=false}), where raw memory is unavailable */ public void hashMemory(final long address, final long len, final long[] result) { unsafeHash(this, null, address, len, result); @@ -989,22 +991,24 @@ public long[] hashLongs(final long[] input, final int off, final int len) { // Internal helper // @NotNull - private static final Access OBJECT_ACCESS = UnsafeAccess.INSTANCE; + // Unsafe based below JDK 25, SafeHeapAccess otherwise. + private static final Access OBJECT_ACCESS = HeapAccess.ACCESS; @NotNull private static final Access CHAR_SEQ_ACCESS = nativeCharSequenceAccess(); @NotNull private static final Access BYTE_BUF_ACCESS = ByteBufferAccess.INSTANCE; + // Historical name, see LongHashFunction#unsafeHash: Unsafe is only used below JDK 25. private static void unsafeHash(final LongTupleHashFunction f, @Nullable final Object input, final long off, final long len, final long[] result) { - f.hash(input, OBJECT_ACCESS, off, len, result); + f.hash(input, input == null ? HeapAccess.rawMemoryAccess() : OBJECT_ACCESS, off, len, result); } private static void hashByteBuffer(final LongTupleHashFunction f, final ByteBuffer input, final int off, final int len, final long[] result) { if (input.hasArray()) { unsafeHash(f, input.array(), BYTE_BASE + input.arrayOffset() + off, len, result); - } else if (input instanceof DirectBuffer) { + } else if (UNSAFE_ENABLED && input instanceof DirectBuffer) { unsafeHash(f, null, ((DirectBuffer) input).address() + off, len, result); } else { f.hash(input, BYTE_BUF_ACCESS, off, len, result); diff --git a/src/main/java/net/openhft/hashing/RuntimeSupport.java b/src/main/java/net/openhft/hashing/RuntimeSupport.java new file mode 100644 index 0000000..8c28ddf --- /dev/null +++ b/src/main/java/net/openhft/hashing/RuntimeSupport.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.hashing; + +final class RuntimeSupport { + /** System property; set to {@code false} to force the Unsafe-free backend on JDK < 25. */ + static final String USE_UNSAFE_PROPERTY = "net.openhft.hashing.useUnsafe"; + + private static final int JAVA_FEATURE = detectJavaFeature(); + private static final boolean USE_UNSAFE = + JAVA_FEATURE < 25 && !"false".equalsIgnoreCase(System.getProperty(USE_UNSAFE_PROPERTY)); + + private RuntimeSupport() {} + + static int javaFeature() { + return JAVA_FEATURE; + } + + static boolean useUnsafeAccess() { + return USE_UNSAFE; + } + + static boolean isAtLeast(final int feature) { + return JAVA_FEATURE >= feature; + } + + static int parseJavaFeature(final String specificationVersion) { + final int dot = specificationVersion.indexOf('.'); + if (dot < 0) { + return Integer.parseInt(specificationVersion); + } + return Integer.parseInt(specificationVersion.substring(dot + 1)); + } + + private static int detectJavaFeature() { + try { + final Object version = Runtime.class.getMethod("version").invoke(Runtime.getRuntime()); + return (Integer) version.getClass().getMethod("feature").invoke(version); + } catch (final Throwable ignore) { + return parseJavaFeature(System.getProperty("java.specification.version", "8")); + } + } +} diff --git a/src/main/java/net/openhft/hashing/SafeHeapAccess.java b/src/main/java/net/openhft/hashing/SafeHeapAccess.java new file mode 100644 index 0000000..c598f96 --- /dev/null +++ b/src/main/java/net/openhft/hashing/SafeHeapAccess.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.hashing; + +import java.nio.ByteOrder; + +/** + * Unsafe-free {@link Access} over primitive arrays, used on JDK 25+. + * + *

Offsets are logical byte offsets from the start of the array (base offset 0) and bytes are + * presented in the platform native order, exactly as the Unsafe based access does. Reads allocate + * nothing; wider reads are composed from {@link #getByte(Object, long)} by {@link Access}. + */ +final class SafeHeapAccess extends Access { + static final SafeHeapAccess INSTANCE = new SafeHeapAccess(); + + private static final boolean LE = Primitives.NATIVE_LITTLE_ENDIAN; + private static final Access NON_NATIVE = Access.newDefaultReverseAccess(INSTANCE); + + private SafeHeapAccess() {} + + @Override + public int getByte(final Object input, final long offset) { + if (input instanceof byte[]) { + return ((byte[]) input)[(int) offset]; + } else if (input instanceof long[]) { + return (int) (element(((long[]) input)[(int) (offset >> 3)], offset, 8) & 0xFF); + } else if (input instanceof int[]) { + return (int) (element(((int[]) input)[(int) (offset >> 2)], offset, 4) & 0xFF); + } else if (input instanceof char[]) { + return (int) (element(((char[]) input)[(int) (offset >> 1)], offset, 2) & 0xFF); + } else if (input instanceof short[]) { + return (int) (element(((short[]) input)[(int) (offset >> 1)], offset, 2) & 0xFF); + } else if (input instanceof boolean[]) { + return ((boolean[]) input)[(int) offset] ? 1 : 0; + } + // Defensive guard; fail loudly rather than return a wrong hash. The library's own calls + // only pass the six array types above: + // - null (raw address): only reachable by a direct hash(null, access, addr, len) call + // with this access; the hashMemory entry points are routed to ForeignRawAccess. + // - arbitrary objects (Pair-style, see Access#unsafe()): need Unsafe field offsets, so + // they can only arrive through such direct calls as well. + // - float[]/double[]: no hashFloats/hashDoubles exist today. If they are added, extend + // this method (via Float.floatToRawIntBits/Double.doubleToRawLongBits). + throw new IllegalArgumentException("Unsupported input without Unsafe: " + + (input == null ? "null (raw memory)" : input.getClass().getName())); + } + + private static long element(final long value, final long offset, final int width) { + final int pos = (int) (offset & (width - 1)); + return value >>> ((LE ? pos : width - 1 - pos) << 3); + } + + @Override + public ByteOrder byteOrder(final Object input) { + return ByteOrder.nativeOrder(); + } + + @Override + protected Access reverseAccess() { + return NON_NATIVE; + } +} diff --git a/src/main/java/net/openhft/hashing/UnsafeAccess.java b/src/main/java/net/openhft/hashing/UnsafeAccess.java index 6dfbcac..c6759d6 100644 --- a/src/main/java/net/openhft/hashing/UnsafeAccess.java +++ b/src/main/java/net/openhft/hashing/UnsafeAccess.java @@ -31,6 +31,9 @@ public class UnsafeAccess extends Access { static final byte FALSE_BYTE_VALUE; static { + if (!RuntimeSupport.useUnsafeAccess()) { + throw new IllegalStateException("sun.misc.Unsafe must not be used on this JVM"); + } try { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); diff --git a/src/main/java/net/openhft/hashing/Util.java b/src/main/java/net/openhft/hashing/Util.java index f3a4b95..d4757e3 100644 --- a/src/main/java/net/openhft/hashing/Util.java +++ b/src/main/java/net/openhft/hashing/Util.java @@ -38,6 +38,10 @@ static private boolean isZing(@NotNull final String name) { static { StringHash stringHash = null; try { + if (!RuntimeSupport.useUnsafeAccess()) { + // CharSequence based and allocation free; never touches String internals or Unsafe + stringHash = UnknownJvmStringHash.INSTANCE; + } else { final String vmName = System.getProperty("java.vm.name"); if (isHotSpotVM(vmName) || isJ9VM(vmName) || isZing(vmName)) { final String javaVersion = System.getProperty("java.version"); @@ -57,6 +61,7 @@ static private boolean isZing(@NotNull final String name) { // try to initialize this version anyway stringHash = HotSpotPrior7u6StringHash.INSTANCE; } + } } catch (final Throwable ignore) { } finally { if (null == stringHash) { diff --git a/src/main/java/net/openhft/hashing/XXH3.java b/src/main/java/net/openhft/hashing/XXH3.java index 4ff62a3..bf390c8 100644 --- a/src/main/java/net/openhft/hashing/XXH3.java +++ b/src/main/java/net/openhft/hashing/XXH3.java @@ -7,14 +7,14 @@ import static java.nio.ByteOrder.LITTLE_ENDIAN; import static net.openhft.hashing.Maths.unsignedLongMulXorFold; -import static net.openhft.hashing.UnsafeAccess.*; +import static net.openhft.hashing.HeapAccess.*; /** * Adapted version of XXH3 implementation from https://github.com/Cyan4973/xxHash. * This implementation provides endian-independant hash values, but it's slower on big-endian platforms. */ class XXH3 { - private static final Access unsafeLE = UnsafeAccess.INSTANCE.byteOrder(null, LITTLE_ENDIAN); + private static final Access unsafeLE = HeapAccess.ACCESS.byteOrder(null, LITTLE_ENDIAN); /*! Pseudorandom secret taken directly from FARSH. */ private static final byte[] XXH3_kSecret = { diff --git a/src/test/java/net/openhft/hashing/CompactLatin1CharSequenceAccessTest.java b/src/test/java/net/openhft/hashing/CompactLatin1CharSequenceAccessTest.java index f79ad24..8a74602 100644 --- a/src/test/java/net/openhft/hashing/CompactLatin1CharSequenceAccessTest.java +++ b/src/test/java/net/openhft/hashing/CompactLatin1CharSequenceAccessTest.java @@ -12,7 +12,13 @@ import static org.junit.Assume.assumeTrue; public class CompactLatin1CharSequenceAccessTest { - static private final Access access = CompactLatin1CharSequenceAccess.INSTANCE; + @org.junit.BeforeClass + public static void requireUnsafe() { + assumeTrue(RuntimeSupport.useUnsafeAccess()); + } + + static private final Access access = + RuntimeSupport.useUnsafeAccess() ? CompactLatin1CharSequenceAccess.INSTANCE : null; static private final byte[] b = { (byte)0xF1, (byte)0xE2, (byte)0xD3, (byte)0xC4, (byte)0xB5 }; @Test diff --git a/src/test/java/net/openhft/hashing/ForeignRawAccessTest.java b/src/test/java/net/openhft/hashing/ForeignRawAccessTest.java new file mode 100644 index 0000000..4f99fa8 --- /dev/null +++ b/src/test/java/net/openhft/hashing/ForeignRawAccessTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.hashing; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Random; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; + +public class ForeignRawAccessTest { + @Test + public void matchesDirectBufferReads() { + assumeTrue(RuntimeSupport.isAtLeast(22)); + final ForeignRawAccess access = ForeignRawAccess.INSTANCE; + assertNotNull("FFM raw access should be available on JDK 22+", access); + + final ByteBuffer bb = ByteBuffer.allocateDirect(64).order(ByteOrder.nativeOrder()); + final byte[] data = new byte[64]; + new Random(7).nextBytes(data); + bb.put(data); + final long address = Util.getDirectBufferAddress(bb); + + for (int i = 0; i < 56; i++) { + assertEquals(bb.getLong(i), access.getLong(null, address + i)); + assertEquals(bb.getInt(i), access.getInt(null, address + i)); + assertEquals(bb.getShort(i), access.getShort(null, address + i)); + assertEquals(data[i], access.getByte(null, address + i)); + assertEquals(data[i] & 0xFF, access.getUnsignedByte(null, address + i)); + } + } + + @Test + public void hashMemoryMatchesHashBytes() { + assumeTrue(HeapAccess.RAW_MEMORY_AVAILABLE); + final byte[] data = new byte[200]; + new Random(3).nextBytes(data); + final ByteBuffer bb = ByteBuffer.allocateDirect(data.length); + bb.put(data); + final long address = Util.getDirectBufferAddress(bb); + for (final LongHashFunction f : new LongHashFunction[] { + LongHashFunction.xx(), LongHashFunction.xx3(), LongHashFunction.murmur_3(), LongHashFunction.wy_3()}) { + assertEquals(f.hashBytes(data), f.hashMemory(address, data.length)); + } + final LongTupleHashFunction t = LongTupleHashFunction.xx128(); + org.junit.Assert.assertArrayEquals(t.hashBytes(data), t.hashMemory(address, data.length)); + } +} diff --git a/src/test/java/net/openhft/hashing/LongHashFunctionTest.java b/src/test/java/net/openhft/hashing/LongHashFunctionTest.java index fa4380a..106f842 100644 --- a/src/test/java/net/openhft/hashing/LongHashFunctionTest.java +++ b/src/test/java/net/openhft/hashing/LongHashFunctionTest.java @@ -48,7 +48,7 @@ private static void testBoolean(LongHashFunction f, int len) { long single = f.hashBoolean(b); long array = f.hashBooleans(a); assertEquals(single, array); - assertEquals(single, f.hash(a, UnsafeAccess.unsafe(), UnsafeAccess.BOOLEAN_BASE, 1L)); + assertEquals(single, f.hash(a, HeapAccess.ACCESS, HeapAccess.BOOLEAN_BASE, 1L)); } } @@ -196,7 +196,11 @@ private static void testCharSequences(LongHashFunction f, long eh, int len, Byte private static void testMemory(LongHashFunction f, long eh, int len, ByteBuffer bb) { ByteBuffer directBB = ByteBuffer.allocateDirect(len); directBB.put(bb); - assertEquals("memory", eh, f.hashMemory(Util.getDirectBufferAddress(directBB), len)); + ((Buffer)directBB).clear(); + assertEquals("direct buffer", eh, f.hashBytes(directBB)); + if (HeapAccess.RAW_MEMORY_AVAILABLE) { + assertEquals("memory", eh, f.hashMemory(Util.getDirectBufferAddress(directBB), len)); + } ((Buffer)bb).clear(); } diff --git a/src/test/java/net/openhft/hashing/LongTupleHashFunctionTest.java b/src/test/java/net/openhft/hashing/LongTupleHashFunctionTest.java index 9781aa2..fff0ba0 100644 --- a/src/test/java/net/openhft/hashing/LongTupleHashFunctionTest.java +++ b/src/test/java/net/openhft/hashing/LongTupleHashFunctionTest.java @@ -100,7 +100,7 @@ private static void testBoolean(LongTupleHashFunction f, int len) { long[] single = f.hashBoolean(b); long[] array = f.hashBooleans(a); assertArrayEquals("testBoolean array", single, array); - assertArrayEquals("testBoolean unsafe", single, f.hash(a, UnsafeAccess.unsafe(), UnsafeAccess.BOOLEAN_BASE, 1L)); + assertArrayEquals("testBoolean unsafe", single, f.hash(a, HeapAccess.ACCESS, HeapAccess.BOOLEAN_BASE, 1L)); } } @@ -248,7 +248,11 @@ private static void testCharSequences(LongTupleHashFunction f, long[] eh, int le private static void testMemory(LongTupleHashFunction f, long[] eh, int len, ByteBuffer bb) { ByteBuffer directBB = ByteBuffer.allocateDirect(len); directBB.put(bb); - assertArrayEquals("memory", eh, f.hashMemory(Util.getDirectBufferAddress(directBB), len)); + ((Buffer)directBB).clear(); + assertArrayEquals("direct buffer", eh, f.hashBytes(directBB)); + if (HeapAccess.RAW_MEMORY_AVAILABLE) { + assertArrayEquals("memory", eh, f.hashMemory(Util.getDirectBufferAddress(directBB), len)); + } ((Buffer)bb).clear(); } diff --git a/src/test/java/net/openhft/hashing/ModernCompactStringHashTest.java b/src/test/java/net/openhft/hashing/ModernCompactStringHashTest.java index c2c5877..87b3a63 100644 --- a/src/test/java/net/openhft/hashing/ModernCompactStringHashTest.java +++ b/src/test/java/net/openhft/hashing/ModernCompactStringHashTest.java @@ -10,6 +10,11 @@ public class ModernCompactStringHashTest { + @org.junit.BeforeClass + public static void requireUnsafe() { + org.junit.Assume.assumeTrue(RuntimeSupport.useUnsafeAccess()); + } + private static boolean isCompactStringsVm() { return System.getProperty("java.version").compareTo("1.9") >= 0; } diff --git a/src/test/java/net/openhft/hashing/RuntimeSupportTest.java b/src/test/java/net/openhft/hashing/RuntimeSupportTest.java new file mode 100644 index 0000000..7dceca2 --- /dev/null +++ b/src/test/java/net/openhft/hashing/RuntimeSupportTest.java @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.hashing; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class RuntimeSupportTest { + @Test + public void parsesJavaFeatureVersions() { + assertEquals(8, RuntimeSupport.parseJavaFeature("1.8")); + assertEquals(11, RuntimeSupport.parseJavaFeature("11")); + assertEquals(25, RuntimeSupport.parseJavaFeature("25")); + } +} diff --git a/src/test/java/net/openhft/hashing/SafeHeapAccessTest.java b/src/test/java/net/openhft/hashing/SafeHeapAccessTest.java new file mode 100644 index 0000000..f12791d --- /dev/null +++ b/src/test/java/net/openhft/hashing/SafeHeapAccessTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.hashing; + +import java.util.Random; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; + +public class SafeHeapAccessTest { + private static final LongHashFunction[] FUNCTIONS = { + LongHashFunction.xx(), LongHashFunction.xx3(), LongHashFunction.city_1_1(), + LongHashFunction.murmur_3(), LongHashFunction.wy_3(), LongHashFunction.metro() + }; + + @Test + public void safeBackendMatchesPublicApiForAllArrayTypes() { + final Random random = new Random(42); + for (final LongHashFunction f : FUNCTIONS) { + for (int len = 0; len < 100; len++) { + final byte[] bytes = new byte[len * 8]; + random.nextBytes(bytes); + final java.nio.ByteBuffer bb = java.nio.ByteBuffer.wrap(bytes).order(java.nio.ByteOrder.nativeOrder()); + final long[] longs = new long[len]; + final int[] ints = new int[len * 2]; + final short[] shorts = new short[len * 4]; + final char[] chars = new char[len * 4]; + bb.asLongBuffer().get(longs); + bb.asIntBuffer().get(ints); + bb.asShortBuffer().get(shorts); + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) shorts[i]; + } + final Access a = SafeHeapAccess.INSTANCE; + assertEquals(f.hashBytes(bytes), f.hash(bytes, a, 0, bytes.length)); + assertEquals(f.hashLongs(longs), f.hash(longs, a, 0, longs.length * 8L)); + assertEquals(f.hashInts(ints), f.hash(ints, a, 0, ints.length * 4L)); + assertEquals(f.hashShorts(shorts), f.hash(shorts, a, 0, shorts.length * 2L)); + assertEquals(f.hashChars(chars), f.hash(chars, a, 0, chars.length * 2L)); + } + } + } + + @Test + public void tupleHashMatches() { + final byte[] bytes = new byte[77]; + new Random(1).nextBytes(bytes); + final LongTupleHashFunction f = LongTupleHashFunction.xx128(); + assertArrayEquals(f.hashBytes(bytes), f.hash(bytes, SafeHeapAccess.INSTANCE, 0, bytes.length)); + } +} diff --git a/src/test/java/net/openhft/hashing/UnsafeAccessTest.java b/src/test/java/net/openhft/hashing/UnsafeAccessTest.java index b3e5820..885fdb1 100644 --- a/src/test/java/net/openhft/hashing/UnsafeAccessTest.java +++ b/src/test/java/net/openhft/hashing/UnsafeAccessTest.java @@ -21,9 +21,17 @@ public class UnsafeAccessTest { @Parameters public static Object[] data() { + if (!RuntimeSupport.useUnsafeAccess()) { + return new Object[] { null }; + } return new Object[] { UnsafeAccess.INSTANCE, UnsafeAccess.OLD_INSTANCE }; } + @org.junit.BeforeClass + public static void requireUnsafe() { + assumeTrue(RuntimeSupport.useUnsafeAccess()); + } + @Parameter public Access unsafe; diff --git a/src/test/java/net/openhft/hashing/UtilTest.java b/src/test/java/net/openhft/hashing/UtilTest.java index 11fe9d1..aac4440 100644 --- a/src/test/java/net/openhft/hashing/UtilTest.java +++ b/src/test/java/net/openhft/hashing/UtilTest.java @@ -13,6 +13,7 @@ public class UtilTest { public void testStringHash() { // This is a sentinel test to make sure that in all known VMs it will not fall back to use // native CharSequenceAccess + org.junit.Assume.assumeTrue(RuntimeSupport.useUnsafeAccess()); assertNotSame(Util.VALID_STRING_HASH, UnknownJvmStringHash.INSTANCE); } }