From 045c663ca2134e145ed15661da20c0c4b9165e2e Mon Sep 17 00:00:00 2001 From: Mark Kittisopikul Date: Thu, 8 Oct 2026 21:10:17 -0400 Subject: [PATCH] Load the HDF5 JNI library before enum constants read HDF5Constants The public enums FileFormatVersion, HDF5DataClass, HDF5StorageLayout, HDF5EnumerationType.EnumStorageForm and CharacterEncoding initialize their constants from HDF5Constants. Touching any of them, or anything that does (FileFormatVersionBounds, the *StorageFeatures classes, switch statements over them), before HDF5Factory, HDF5FactoryProvider or HDFHelper initialized HDF5Constants before HDF5NativeLibrary.load() ran. H5's static initializer then failed for good, and every later HDF5 call failed with UnsatisfiedLinkError: 'int hdf.hdf5lib.H5.H5dont_atexit()'. This is what failed 306 tests on macOS once NativeLibraryBootstrap was removed (HDF5ArchiverTest touches FileFormatVersionBounds first); Linux only passed because of a different test order. Enum constants are created before any static initializer of their enum and their arguments are evaluated before their constructor, so they now read HDF5 constants through HDF5NativeLibrary.intConstant/longConstant, which call load() first. Also, in HDF5NativeLibrary.load(): - only mark the library as loaded once that succeeded, so a failure (e.g. no natives for the platform) is reported to the next caller again instead of surfacing as the UnsatisfiedLinkError above; - don't set the property to null when Loader.load() returns null (JavaCPP's library loading disabled), which threw. Checked by initializing each of the 1104 classes in the jar first, in a fresh JVM, and then writing a file through HDF5Factory: 15 classes failed before this change, none after. Co-Authored-By: Claude Opus 5.5 --- .../systemsx/cisd/hdf5/CharacterEncoding.java | 5 ++- .../ch/systemsx/cisd/hdf5/HDF5DataClass.java | 30 +++++++------ .../cisd/hdf5/HDF5EnumerationType.java | 7 ++-- .../systemsx/cisd/hdf5/HDF5StorageLayout.java | 5 ++- .../cisd/hdf5/IHDF5WriterConfigurator.java | 9 ++-- .../cisd/hdf5/hdf5lib/HDF5NativeLibrary.java | 42 +++++++++++++++++-- 6 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/main/java/ch/systemsx/cisd/hdf5/CharacterEncoding.java b/src/main/java/ch/systemsx/cisd/hdf5/CharacterEncoding.java index 4430ee4..4405bdb 100644 --- a/src/main/java/ch/systemsx/cisd/hdf5/CharacterEncoding.java +++ b/src/main/java/ch/systemsx/cisd/hdf5/CharacterEncoding.java @@ -16,6 +16,7 @@ package ch.systemsx.cisd.hdf5; +import static ch.systemsx.cisd.hdf5.hdf5lib.HDF5NativeLibrary.intConstant; import static hdf.hdf5lib.HDF5Constants.H5T_CSET_ASCII; import static hdf.hdf5lib.HDF5Constants.H5T_CSET_UTF8; @@ -26,8 +27,8 @@ */ public enum CharacterEncoding { - ASCII("ASCII", H5T_CSET_ASCII, 1), - UTF8("UTF8", H5T_CSET_UTF8, 4); + ASCII("ASCII", intConstant(() -> H5T_CSET_ASCII), 1), + UTF8("UTF8", intConstant(() -> H5T_CSET_UTF8), 4); private final String charSetName; diff --git a/src/main/java/ch/systemsx/cisd/hdf5/HDF5DataClass.java b/src/main/java/ch/systemsx/cisd/hdf5/HDF5DataClass.java index f4ed798..58117ff 100644 --- a/src/main/java/ch/systemsx/cisd/hdf5/HDF5DataClass.java +++ b/src/main/java/ch/systemsx/cisd/hdf5/HDF5DataClass.java @@ -16,6 +16,7 @@ package ch.systemsx.cisd.hdf5; +import static ch.systemsx.cisd.hdf5.hdf5lib.HDF5NativeLibrary.intConstant; import static hdf.hdf5lib.HDF5Constants.H5T_BITFIELD; import static hdf.hdf5lib.HDF5Constants.H5T_COMPOUND; import static hdf.hdf5lib.HDF5Constants.H5T_ENUM; @@ -47,18 +48,23 @@ public enum HDF5DataClass { // Implementation note: The order matters! ENUM needs to be before INTEGER, as H5Tdetect_class // will return TRUE for ENUM arrays when trying to detect an INTEGER class. - BITFIELD(H5T_BITFIELD, new BasicJavaTypeProvider(BitSet.class, null, null, null)), ENUM( - H5T_ENUM, new BasicJavaTypeProvider(HDF5EnumerationValue.class, - HDF5EnumerationValueArray.class, null, null)), INTEGER(H5T_INTEGER, - new IntJavaTypeProvider()), FLOAT(H5T_FLOAT, new FloatJavaTypeProvider()), STRING( - H5T_STRING, new BasicJavaTypeProvider(String.class, String[].class, String[][].class, - MDArray.class)), OPAQUE(H5T_OPAQUE, new BasicJavaTypeProvider(byte.class, - byte[].class, byte[][].class, MDByteArray.class)), BOOLEAN(-1, - new BasicJavaTypeProvider(boolean.class, BitSet.class, null, null)), COMPOUND( - H5T_COMPOUND, new BasicJavaTypeProvider(Map.class, Map[].class, Map[][].class, - MDArray.class)), REFERENCE(H5T_REFERENCE, new BasicJavaTypeProvider( - String.class, String[].class, String[][].class, MDArray.class)), OTHER(-1, - new BasicJavaTypeProvider(null, null, null, null)); + BITFIELD(intConstant(() -> H5T_BITFIELD), + new BasicJavaTypeProvider(BitSet.class, null, null, null)), + ENUM(intConstant(() -> H5T_ENUM), + new BasicJavaTypeProvider(HDF5EnumerationValue.class, HDF5EnumerationValueArray.class, + null, null)), + INTEGER(intConstant(() -> H5T_INTEGER), new IntJavaTypeProvider()), + FLOAT(intConstant(() -> H5T_FLOAT), new FloatJavaTypeProvider()), + STRING(intConstant(() -> H5T_STRING), + new BasicJavaTypeProvider(String.class, String[].class, String[][].class, MDArray.class)), + OPAQUE(intConstant(() -> H5T_OPAQUE), + new BasicJavaTypeProvider(byte.class, byte[].class, byte[][].class, MDByteArray.class)), + BOOLEAN(-1, new BasicJavaTypeProvider(boolean.class, BitSet.class, null, null)), + COMPOUND(intConstant(() -> H5T_COMPOUND), + new BasicJavaTypeProvider(Map.class, Map[].class, Map[][].class, MDArray.class)), + REFERENCE(intConstant(() -> H5T_REFERENCE), + new BasicJavaTypeProvider(String.class, String[].class, String[][].class, MDArray.class)), + OTHER(-1, new BasicJavaTypeProvider(null, null, null, null)); /** * A role that can provide a java type for a data class, rank and element size. diff --git a/src/main/java/ch/systemsx/cisd/hdf5/HDF5EnumerationType.java b/src/main/java/ch/systemsx/cisd/hdf5/HDF5EnumerationType.java index 8549396..17d595e 100644 --- a/src/main/java/ch/systemsx/cisd/hdf5/HDF5EnumerationType.java +++ b/src/main/java/ch/systemsx/cisd/hdf5/HDF5EnumerationType.java @@ -16,6 +16,7 @@ package ch.systemsx.cisd.hdf5; +import static ch.systemsx.cisd.hdf5.hdf5lib.HDF5NativeLibrary.longConstant; import static hdf.hdf5lib.HDF5Constants.H5T_NATIVE_INT16; import static hdf.hdf5lib.HDF5Constants.H5T_NATIVE_INT32; import static hdf.hdf5lib.HDF5Constants.H5T_NATIVE_INT8; @@ -43,15 +44,15 @@ public enum EnumStorageForm /** * One byte, for up to 255 alternatives. */ - BYTE(1, H5T_NATIVE_INT8, H5T_STD_U8LE), + BYTE(1, longConstant(() -> H5T_NATIVE_INT8), longConstant(() -> H5T_STD_U8LE)), /** * Two bytes, for up to 65535 alternatives. */ - SHORT(2, H5T_NATIVE_INT16, H5T_STD_U16LE), + SHORT(2, longConstant(() -> H5T_NATIVE_INT16), longConstant(() -> H5T_STD_U16LE)), /** * Four bytes, for more than 65535 alternatives. */ - INT(4, H5T_NATIVE_INT32, H5T_STD_U32LE); + INT(4, longConstant(() -> H5T_NATIVE_INT32), longConstant(() -> H5T_STD_U32LE)); private final byte storageSize; diff --git a/src/main/java/ch/systemsx/cisd/hdf5/HDF5StorageLayout.java b/src/main/java/ch/systemsx/cisd/hdf5/HDF5StorageLayout.java index 80d385d..dabc121 100644 --- a/src/main/java/ch/systemsx/cisd/hdf5/HDF5StorageLayout.java +++ b/src/main/java/ch/systemsx/cisd/hdf5/HDF5StorageLayout.java @@ -1,5 +1,6 @@ package ch.systemsx.cisd.hdf5; +import static ch.systemsx.cisd.hdf5.hdf5lib.HDF5NativeLibrary.intConstant; import hdf.hdf5lib.HDF5Constants; /** @@ -9,8 +10,8 @@ */ public enum HDF5StorageLayout { - COMPACT(HDF5Constants.H5D_COMPACT), CONTIGUOUS(HDF5Constants.H5D_CONTIGUOUS), CHUNKED( - HDF5Constants.H5D_CHUNKED), NOT_APPLICABLE(-1); + COMPACT(intConstant(() -> HDF5Constants.H5D_COMPACT)), CONTIGUOUS(intConstant(() -> HDF5Constants.H5D_CONTIGUOUS)), CHUNKED( + intConstant(() -> HDF5Constants.H5D_CHUNKED)), NOT_APPLICABLE(-1); private int id; diff --git a/src/main/java/ch/systemsx/cisd/hdf5/IHDF5WriterConfigurator.java b/src/main/java/ch/systemsx/cisd/hdf5/IHDF5WriterConfigurator.java index d7452bc..69eb31c 100644 --- a/src/main/java/ch/systemsx/cisd/hdf5/IHDF5WriterConfigurator.java +++ b/src/main/java/ch/systemsx/cisd/hdf5/IHDF5WriterConfigurator.java @@ -16,6 +16,7 @@ package ch.systemsx.cisd.hdf5; +import static ch.systemsx.cisd.hdf5.hdf5lib.HDF5NativeLibrary.intConstant; import hdf.hdf5lib.HDF5Constants; /** @@ -71,22 +72,22 @@ public enum FileFormatVersion /** * The library will create objects with the earliest possible format versions. */ - EARLIEST(HDF5Constants.H5F_LIBVER_EARLIEST, false), + EARLIEST(intConstant(() -> HDF5Constants.H5F_LIBVER_EARLIEST), false), /** * The library will allow objects to be created with the latest format versions available to the current library release version. */ - LATEST(HDF5Constants.H5F_LIBVER_LATEST, true), + LATEST(intConstant(() -> HDF5Constants.H5F_LIBVER_LATEST), true), /** * The library will allow objects to be created with the latest format versions available to HDF5 v1.8. */ - V1_8(HDF5Constants.H5F_LIBVER_V18, false), + V1_8(intConstant(() -> HDF5Constants.H5F_LIBVER_V18), false), /** * The library will allow objects to be created with the latest format versions available to HDF5 v1.10. */ - V1_10(HDF5Constants.H5F_LIBVER_V110, true); + V1_10(intConstant(() -> HDF5Constants.H5F_LIBVER_V110), true); private final int hdf5Constant; diff --git a/src/main/java/ch/systemsx/cisd/hdf5/hdf5lib/HDF5NativeLibrary.java b/src/main/java/ch/systemsx/cisd/hdf5/hdf5lib/HDF5NativeLibrary.java index b07d1a9..14e6e49 100644 --- a/src/main/java/ch/systemsx/cisd/hdf5/hdf5lib/HDF5NativeLibrary.java +++ b/src/main/java/ch/systemsx/cisd/hdf5/hdf5lib/HDF5NativeLibrary.java @@ -1,5 +1,8 @@ package ch.systemsx.cisd.hdf5.hdf5lib; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; + import org.bytedeco.hdf5.hdf5_java; import org.bytedeco.javacpp.Loader; @@ -18,6 +21,11 @@ * public entry points ({@code HDF5Factory}, {@code HDF5FactoryProvider}, {@link HDFHelper}) call it * from their static initializers. Code that touches {@code hdf.hdf5lib.H5} directly before any of * those should call {@link #load()} itself first. + *

+ * Enum constants that are initialized from {@code HDF5Constants} need the same guarantee, but they + * are created before any static initializer of their enum runs, and their constructor arguments + * are evaluated before their constructor. They read the constant through {@link #intConstant} or + * {@link #longConstant} instead, which load the library first. */ public final class HDF5NativeLibrary { @@ -43,12 +51,38 @@ public static synchronized void load() { return; } - loaded = true; - if (isSet(H5_PATH_PROPERTY) || isSet(H5_NAME_PROPERTY)) + if (isSet(H5_PATH_PROPERTY) == false && isSet(H5_NAME_PROPERTY) == false) { - return; + // Loader.load() returns null when JavaCPP's library loading is disabled. + final String path = Loader.load(hdf5_java.class); + if (path != null) + { + System.setProperty(H5_PATH_PROPERTY, path); + } } - System.setProperty(H5_PATH_PROPERTY, Loader.load(hdf5_java.class)); + // Only now, so that a failed attempt (e.g. no natives for this platform) is retried and + // reported again by the next caller, instead of surfacing later as an obscure + // UnsatisfiedLinkError from H5's static initializer. + loaded = true; + } + + /** + * Returns {@code constant.getAsInt()}, after {@link #load()}. For initializing enum constants + * from {@code HDF5Constants}, e.g. {@code COMPACT(intConstant(() -> H5D_COMPACT))}. + */ + public static int intConstant(IntSupplier constant) + { + load(); + return constant.getAsInt(); + } + + /** + * Returns {@code constant.getAsLong()}, after {@link #load()}. See {@link #intConstant}. + */ + public static long longConstant(LongSupplier constant) + { + load(); + return constant.getAsLong(); } private static boolean isSet(String key)