diff --git a/build.gradle.kts b/build.gradle.kts index f382283..9c389dd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ plugins { allprojects { group = "org.grimmory" - version = "0.14.0" + version = "0.15.0" repositories { mavenCentral() diff --git a/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java b/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java index 87532e4..22596fe 100644 --- a/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java +++ b/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java @@ -13,53 +13,143 @@ *

PDFium requires a single call to {@code FPDF_InitLibraryWithConfig} before any document * operations, and {@code FPDF_DestroyLibrary} at shutdown. This class handles both automatically. * - *

Thread safety: initialization is synchronized. After init, independent {@link PdfDocument} - * instances on separate threads are safe. A single document/page handle must not be accessed - * concurrently. + *

Concurrency model

+ * + *

Initialization is driven by the Initialization-on-demand holder idiom (JLS ยง12.4.2): + * the JVM class-loader lock guarantees {@code FPDF_InitLibraryWithConfig} is invoked exactly once + * per JVM, under the class-loader initialization lock, before any thread can observe a successful + * state. No explicit synchronized block, volatile read, or double-checked locking is needed on the + * hot path. + * + *

This makes {@link #initialize()} and {@link #isAvailable()} safe to call from any thread at + * any time, including concurrent {@code @BeforeAll} hooks in a parallel JUnit test run. + * + *

After initialization, independent {@link PdfDocument} instances on separate threads are safe. + * A single document or page handle must not be accessed concurrently. + * + *

System properties

+ * + * */ public final class PdfiumLibrary { - private static volatile boolean initialized = false; + /** See class javadoc for usage. */ + public static final String PROP_LIBRARY_PATH = "pdfium4j.library.path"; + + /** See class javadoc for usage. */ + public static final String PROP_AUTOINIT = "pdfium4j.autoinit"; private PdfiumLibrary() {} /** * Ensure PDFium is initialized. Safe to call multiple times. Called automatically by {@link - * PdfDocument#open} methods. + * PdfDocument#open} methods (unless {@link #PROP_AUTOINIT} is set to {@code "false"}). + * + * @throws PdfiumException if the native library cannot be loaded or initialized */ - public static synchronized void initialize() { - if (initialized) return; + public static void initialize() { + Holder.STATE.require(); + } - NativeLoader.ensureLoaded(); + /** + * Non-throwing availability probe. + * + *

Returns {@code true} if the native library has been successfully loaded and {@code + * FPDF_InitLibraryWithConfig} has been invoked. Returns {@code false} if initialization has + * failed, the cause is suppressed, never re-thrown from this method. Safe to call from any + * thread. + * + *

This is the preferred way to gate optional functionality: + * + *

{@code
+   * if (PdfiumLibrary.isAvailable()) {
+   *   try (PdfDocument doc = PdfDocument.open(path)) { ... }
+   * }
+   * }
+ * + * @return {@code true} iff PDFium is fully initialized in this JVM + */ + public static boolean isAvailable() { + return Holder.STATE.loaded; + } - try (Arena arena = Arena.ofConfined()) { - MemorySegment config = arena.allocate(ViewBindings.LIBRARY_CONFIG_LAYOUT); - config.set(ValueLayout.JAVA_INT, 0, 2); + /** + * Returns the Throwable that caused initialization to fail, or {@code null} if initialization + * succeeded or has not yet been attempted for reasons other than failure. Useful for + * diagnostics/logging without forcing a throw. + */ + public static Throwable loadError() { + return Holder.STATE.loadError; + } - ViewBindings.FPDF_InitLibraryWithConfig.invokeExact(config); - } catch (PdfiumException e) { - throw e; - } catch (Throwable t) { - throw new PdfiumException("Failed to initialize PDFium", t); + static void ensureInitialized() { + if (!autoInitEnabled()) { + // Honour the explicit opt-out; require the caller has already run initialize(). + if (!Holder.STATE.loaded) { + throw new PdfiumException( + "PDFium auto-init is disabled (" + + PROP_AUTOINIT + + "=false); call PdfiumLibrary.initialize() explicitly"); + } + return; } + Holder.STATE.require(); + } - initialized = true; + private static boolean autoInitEnabled() { + String v = System.getProperty(PROP_AUTOINIT); + return v == null || !"false".equalsIgnoreCase(v.trim()); + } - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - try { - ViewBindings.FPDF_DestroyLibrary.invokeExact(); - } catch (Throwable ignored) { - } - }, - "pdfium4j-shutdown")); + private static final class Holder { + static final State STATE = new State(); } - static void ensureInitialized() { - if (!initialized) { - initialize(); + private static final class State { + final boolean loaded; + final Throwable loadError; + + State() { + Throwable err = null; + boolean ok = false; + try { + NativeLoader.ensureLoaded(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment config = arena.allocate(ViewBindings.LIBRARY_CONFIG_LAYOUT); + config.set(ValueLayout.JAVA_INT, 0, 2); + ViewBindings.FPDF_InitLibraryWithConfig.invokeExact(config); + } + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + try { + ViewBindings.FPDF_DestroyLibrary.invokeExact(); + } catch (Throwable ignored) { + } + }, + "pdfium4j-shutdown")); + ok = true; + } catch (Throwable t) { + err = t; + } + this.loaded = ok; + this.loadError = err; + } + + void require() { + if (loaded) return; + if (loadError instanceof PdfiumException pe) { + throw pe; + } + throw new PdfiumException("Failed to initialize PDFium", loadError); } } } diff --git a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java index 657abff..5762a64 100644 --- a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java +++ b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java @@ -16,6 +16,13 @@ public final class NativeLoader { + /** + * System property: absolute filesystem path to a pdfium native library. When set, {@link + * #ensureLoaded()} loads this path directly via {@code System.load} and skips both classpath + * extraction and {@code System.loadLibrary("pdfium")} lookup. + */ + public static final String PROP_LIBRARY_PATH = "pdfium4j.library.path"; + private static volatile boolean loaded = false; private static volatile Throwable loadError = null; @@ -32,28 +39,52 @@ public static void ensureLoaded() { throw new NativeLoadException("Native library failed to load previously", loadError); } try { - tryLoadFromClasspath(); + performLoad(); loaded = true; - } catch (NativeLoadException classpathMiss) { + } catch (NativeLoadException e) { + loadError = e; + throw e; + } catch (Throwable t) { + loadError = t; + throw new NativeLoadException("Failed to load native library", t); + } + } + } + + private static void performLoad() { + try { + String overridePath = System.getProperty(PROP_LIBRARY_PATH); + if (overridePath != null && !overridePath.isBlank()) { try { - System.loadLibrary("pdfium"); - loaded = true; + System.load(overridePath); + return; } catch (UnsatisfiedLinkError e) { - NativeLoadException ex = - new NativeLoadException( - "PDFium native library not found for " - + detectPlatform() - + ". Also tried System.loadLibrary(\"pdfium\") and failed.", - classpathMiss); - ex.addSuppressed(e); - loadError = ex; - throw ex; + throw new NativeLoadException( + "PDFium override path set via -D" + + PROP_LIBRARY_PATH + + "=" + + overridePath + + " but loading that file failed", + e); } - } catch (Throwable t) { - loadError = t; - throw (t instanceof NativeLoadException nativeLoadException) - ? nativeLoadException - : new NativeLoadException("Failed to load native library", t); + } + + tryLoadFromClasspath(); + } catch (NativeLoadException classpathMiss) { + try { + System.loadLibrary("pdfium"); + } catch (UnsatisfiedLinkError e) { + NativeLoadException ex = + new NativeLoadException( + "PDFium native library not found for " + + detectPlatform() + + ". Also tried System.loadLibrary(\"pdfium\") and failed. " + + "Set -D" + + PROP_LIBRARY_PATH + + "=/path/to/libpdfium. to load a system copy explicitly.", + classpathMiss); + ex.addSuppressed(e); + throw ex; } } }