@@ -33,6 +33,30 @@ public class XSLoader extends PerlModuleBase {
3333 "XSLoader"
3434 );
3535
36+ /**
37+ * Modules that are pure-XS in real Perl with no PerlOnJava-side implementation.
38+ * When XSLoader::load is called for one of these, we die cleanly so that
39+ * {@code eval { require SomeModule }} in CPAN code (and test suites that
40+ * probe for optional backends like DBM engines) correctly falls through
41+ * to alternatives instead of silently "succeeding" and then crashing
42+ * later when methods are actually called.
43+ * <p>
44+ * Rule of thumb: the module's whole functionality lives in a shared
45+ * library shipped with CPAN, and there is no pure-Perl or Java-backed
46+ * replacement in PerlOnJava. Pre-registered Java modules (File::Glob,
47+ * Encode, Time::HiRes, etc.) must NOT appear here.
48+ * <p>
49+ * Kept in sync with the Perl-side copy in {@code lib/XSLoader.pm}.
50+ */
51+ private static final Set <String > XS_ONLY_NOT_SUPPORTED = Set .of (
52+ "DB_File" ,
53+ "BerkeleyDB" ,
54+ "GDBM_File" ,
55+ "NDBM_File" ,
56+ "ODBM_File" ,
57+ "SDBM_File"
58+ );
59+
3660 /**
3761 * Constructor for XSLoader.
3862 * Initializes the module with the name "XSLoader".
@@ -57,6 +81,41 @@ public static void initialize() {
5781 }
5882 }
5983
84+ /**
85+ * Installs no-op Perl subroutines for XS symbols that a failed-to-load
86+ * module's END block is known to call. Without these, the END queue
87+ * aborts on interpreter shutdown with a non-zero exit status, which
88+ * prove/TAP::Harness counts as a failed test program even when the
89+ * program's actual assertions all passed or were SKIPped.
90+ *
91+ * Keyed by the module name passed to {@code XSLoader::load}.
92+ */
93+ private static void installEndBlockStubs (String moduleName ) {
94+ String [] symbols = switch (moduleName ) {
95+ case "BerkeleyDB" -> new String [] { "BerkeleyDB::Term::close_everything" };
96+ default -> null ;
97+ };
98+ if (symbols == null ) return ;
99+ try {
100+ java .lang .invoke .MethodHandle mh = RuntimeCode .lookup .findStatic (
101+ XSLoader .class , "noopStub" , RuntimeCode .methodType );
102+ for (String sym : symbols ) {
103+ if (GlobalVariable .existsGlobalCodeRef (sym )) continue ;
104+ RuntimeCode code = new RuntimeCode (mh , null , null );
105+ code .isStatic = true ;
106+ GlobalVariable .getGlobalCodeRef (sym ).set (new RuntimeScalar (code ));
107+ }
108+ } catch (Exception e ) {
109+ // Non-fatal: the test program may still report a spurious non-zero
110+ // exit, but the module-load failure path is unaffected.
111+ }
112+ }
113+
114+ /** No-op Perl sub used by {@link #installEndBlockStubs}. */
115+ public static RuntimeList noopStub (RuntimeArray args , int ctx ) {
116+ return new RuntimeList ();
117+ }
118+
60119 /**
61120 * Loads a PerlOnJava module.
62121 * <p>
@@ -90,6 +149,29 @@ public static RuntimeList load(RuntimeArray args, int ctx) {
90149 moduleName = args .getFirst ().toString ();
91150 }
92151
152+ // Bail out cleanly for pure-XS modules PerlOnJava can't back.
153+ // Without this, modules like DB_File load but their XS helpers
154+ // (constant, etc.) are undefined, leading to infinite AUTOLOAD
155+ // recursion (StackOverflowError) the first time the module is
156+ // actually used. CPAN test suites commonly probe optional backends
157+ // with `eval { require SomeDBM }` and rely on require FAILING to
158+ // fall through to alternatives; silent success breaks them.
159+ if (XS_ONLY_NOT_SUPPORTED .contains (moduleName )) {
160+ // Install no-op stubs for any functions the module registers in an
161+ // END block — the `.pm` file was already compiled end-to-end before
162+ // we reach this `load`, so its END queue entries will fire at
163+ // interpreter shutdown regardless of whether `require` succeeds.
164+ // Without these, CPAN prove-style runners report the (otherwise-
165+ // passing / SKIPped) test program as "exited 1" from the END die.
166+ installEndBlockStubs (moduleName );
167+
168+ return WarnDie .die (
169+ new RuntimeScalar ("Can't load '" + moduleName + "' for module " + moduleName
170+ + ": XS module not supported on PerlOnJava" ),
171+ new RuntimeScalar ("\n " )
172+ ).getList ();
173+ }
174+
93175 // Convert Perl::Module::Name to org.perlonjava.runtime.perlmodule.PerlModuleName
94176 String [] parts = moduleName .split ("::" );
95177 StringBuilder className1 = new StringBuilder ("org.perlonjava.runtime.perlmodule." );
0 commit comments