XSLoader in PerlOnJava
March 27, 2026 · View on GitHub
Perl XSLoader became the preferred choice for most modules, with DynaLoader reserved for complex cases that need its full feature set.
In Perl, there are two ways to load compiled (XS) extensions:
- DynaLoader - The original, full-featured dynamic loader
- XSLoader - A lightweight, faster alternative introduced later
The Difference in Perl
# Old way with DynaLoader
package MyModule;
use DynaLoader;
our @ISA = qw(DynaLoader);
bootstrap MyModule $VERSION;
# New way with XSLoader
package MyModule;
use XSLoader;
XSLoader::load('MyModule', $VERSION);
Why XSLoader is Preferred
- Faster - Doesn't need inheritance, less overhead
- Simpler - Just a single function call
- Smaller memory footprint - Fewer features means less code
- Backward compatible - Falls back to DynaLoader if needed
For PerlOnJava: JavaXSLoader
We could implement a similar pattern with a lightweight loader for common cases:
package org.perlonjava.runtime.perlmodule;
import org.perlonjava.runtime.runtimetypes.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Lightweight Java class loader for PerlOnJava modules.
* Similar to XSLoader in Perl - simpler and faster than JavaDynaLoader.
*/
public class JavaXSLoader extends PerlModuleBase {
private static final Map<String, Boolean> loaded = new ConcurrentHashMap<>();
@Override
public void initialize() {
exportSub("load");
exportSub("load_file");
}
/**
* Load a Java-based Perl module by name
* This is the simplified, fast path for built-in modules
*/
public static RuntimeList load(RuntimeArray args, RuntimeContext ctx) {
String moduleName = args.get(0).toString();
String version = args.size() > 1 ? args.get(1).toString() : null;
// Check if already loaded
if (loaded.containsKey(moduleName)) {
return new RuntimeList(scalarTrue);
}
try {
// Fast path for known modules
String className = getJavaClassName(moduleName);
if (className != null) {
// Direct load without full DynaLoader features
Class<?> clazz = Class.forName(className);
Object module = clazz.getDeclaredConstructor().newInstance();
if (module instanceof InitializableModule) {
((InitializableModule) module).initialize();
}
loaded.put(moduleName, true);
return new RuntimeList(scalarTrue);
}
// Fall back to JavaDynaLoader for unknown modules
return JavaDynaLoader.load_class(args, ctx);
} catch (Exception e) {
throw new RuntimeException("Can't load module " + moduleName + ": " + e.getMessage());
}
}
/**
* Load a specific .class or .jar file
* Simplified version without full JAR inspection
*/
public static RuntimeList load_file(RuntimeArray args, RuntimeContext ctx) {
String filename = args.get(0).toString();
try {
if (filename.endsWith(".jar")) {
// Simple JAR loading
DynamicClassLoader.loadJar(filename);
} else if (filename.endsWith(".class")) {
// Direct class file loading would go here
throw new RuntimeException("Direct .class loading not yet implemented");
}
return new RuntimeList(scalarTrue);
} catch (Exception e) {
throw new RuntimeException("Can't load file " + filename + ": " + e.getMessage());
}
}
/**
* Fast mapping of Perl module names to Java classes
* This could be generated at build time for speed
*/
private static String getJavaClassName(String perlModule) {
// This mapping could be loaded from a properties file generated at build time
switch (perlModule) {
case "Cwd": return "org.perlonjava.runtime.perlmodule.Cwd";
case "File::Basename": return "org.perlonjava.runtime.perlmodule.FileBasename";
case "Data::Dumper": return "org.perlonjava.runtime.perlmodule.DataDumper";
case "DBI": return "org.perlonjava.runtime.perlmodule.DBI";
// ... etc
default: return null;
}
}
}
Usage in Perl Modules
package File::Basename;
use strict;
use warnings;
our $VERSION = '2.85';
# Modern, lightweight approach
use JavaXSLoader;
JavaXSLoader::load('File::Basename', $VERSION);
1;
Compare to the heavier JavaDynaLoader approach:
package Some::Complex::Module;
use JavaDynaLoader;
our @ISA = qw(JavaDynaLoader);
# Load multiple JARs
JavaDynaLoader::load_jar('lib/dependency1.jar');
JavaDynaLoader::load_jar('lib/dependency2.jar');
# Import specific methods
JavaDynaLoader::import_methods('com.example.SomeClass', 'Some::Complex::Module');
1;
Build-Time Optimization
Generate the module mappings at build time:
public class GenerateXSLoaderMappings {
public static void main(String[] args) throws IOException {
Properties mappings = new Properties();
// Scan for modules
Reflections reflections = new Reflections("org.perlonjava.runtime.perlmodule");
Set<Class<? extends InitializableModule>> modules =
reflections.getSubTypesOf(InitializableModule.class);
for (Class<?> moduleClass : modules) {
String perlName = extractPerlModuleName(moduleClass);
mappings.put(perlName, moduleClass.getName());
}
// Write to resources
try (FileWriter writer = new FileWriter("src/main/resources/xsloader-mappings.properties")) {
mappings.store(writer, "Generated module mappings for JavaXSLoader");
}
}
}
Benefits of This Approach
- Performance: Direct module loading without scanning
- Simplicity: Most modules just need simple loading
- Compatibility: Falls back to full JavaDynaLoader when needed
- Memory: Smaller footprint for simple cases
- Build-time optimization: Module discovery happens at build time
Known XS Module Dependency Issues (2026-03-19)
DateTime Dependency Chain
When installing DateTime via jcpan, the module has several XS-dependent dependencies:
DateTime.pm
├── namespace::autoclean 0.19
│ └── B::Hooks::EndOfScope ✓ (implemented using defer mechanism)
├── Params::ValidationCompiler 0.26 ✓
├── Specio::Subs ✓
│ └── Specio ✓
│ └── Module::Implementation ✓ (fixed: symbol table dereference bug)
├── Try::Tiny ✓
├── DateTime::Locale 1.06
├── DateTime::TimeZone 2.44
└── POSIX (built-in) ✓
B::Hooks::EndOfScope - IMPLEMENTED
This module provides on_scope_end which registers a callback to execute when the current scope exits.
PerlOnJava Implementation:
Instead of using Perl's compile-time B:: hooks, we leverage PerlOnJava's existing defer mechanism:
DeferBlockclass wraps a code reference for scope-exit executionDynamicVariableManagermanages the stack of deferred blocks- Callbacks execute in LIFO order when scope exits (same as Perl)
How it works:
use B::Hooks::EndOfScope;
{
on_scope_end { print "cleanup\n" };
print "in scope\n";
}
# Output: "in scope" then "cleanup"
The Java XS implementation (BHooksEndOfScope.java) simply creates a DeferBlock and pushes it onto DynamicVariableManager, reusing the same infrastructure that powers Perl's defer feature. The & prototype allows bare block syntax.
Progress Tracking
| Issue | Status | Notes |
|---|---|---|
${ $stash{NAME} } dereference | FIXED | RuntimeScalar.scalarDeref() now handles GLOB type |
| Module::Implementation | FIXED | Works after symbol table fix |
| Specio | FIXED | Works after symbol table fix |
| B::Hooks::EndOfScope | IMPLEMENTED | Uses defer mechanism (DeferBlock + DynamicVariableManager) |
| namespace::autoclean | NEEDS TESTING | Should work now with B::Hooks::EndOfScope |
| DateTime full install | NEEDS TESTING | Should work now - test with jcpan |
Files Changed
Symbol Table Fix:
src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java- Added
GLOBcase toscalarDeref()- returns scalar slot of glob - Added
GLOBcase toscalarDerefNonStrict()- same behavior
- Added
B::Hooks::EndOfScope Implementation:
src/main/java/org/perlonjava/runtime/perlmodule/BHooksEndOfScope.java- Java XS modulesrc/main/perl/lib/B/Hooks/EndOfScope.pm- Perl wrapper with XSLoader
Next Steps
- Test
namespace::autoclean- should work now - Test DateTime full installation via
jcpan install DateTime - If issues remain, check DateTime::Locale and DateTime::TimeZone dependencies