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. + *
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. + * + *
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.