NAME
ExtUtils::XSOne - Combine multiple XS files into a single shared library
VERSION
Version 0.01
SYNOPSIS
# In Makefile.PL
use ExtUtils::MakeMaker;
use ExtUtils::XSOne;
# Combine XS files before WriteMakefile
ExtUtils::XSOne->combine(
src_dir => 'lib/MyModule/xs',
output => 'lib/MyModule.xs',
);
WriteMakefile(
NAME => 'MyModule',
# ... other options
);
Or use the command-line tool:
xsone --src lib/MyModule/xs --out lib/MyModule.xs
DESCRIPTION
ExtUtils::XSOne solves a fundamental limitation of Perl's XSMULTI feature: when using XSMULTI => 1 in ExtUtils::MakeMaker, each .xs file compiles into a separate shared library (.so/.bundle/.dll), which means they cannot share C static variables, registries, or internal state.
This module allows you to organize your XS code into multiple files for maintainability while still producing a single shared library that can share all C-level state.
The Problem
With XSMULTI, this structure:
lib/
├── Foo.xs → blib/arch/auto/Foo/Foo.bundle
└── Foo/
└── Bar.xs → blib/arch/auto/Foo/Bar/Bar.bundle
Creates two separate shared libraries. If Foo.xs has:
static int my_registry[100];
Then Foo/Bar.xs cannot access my_registry - each bundle has its own copy of static variables.
The Solution
With ExtUtils::XSOne, you organize code like this:
lib/
└── Foo/
└── xs/
├── _header.xs # Common includes, types, static vars
├── context.xs # MODULE = Foo PACKAGE = Foo::Context
├── tensor.xs # MODULE = Foo PACKAGE = Foo::Tensor
└── _footer.xs # BOOT section
These are combined at build time into a single lib/Foo.xs, which compiles to one shared library where all modules share the same C state.
FILE NAMING CONVENTION
Files in the source directory are processed in this order:
- 1.
_header.xs- Always first (if present) -
Contains
#includedirectives, type definitions, static variables, and helper functions shared by all modules. - 2. Other
.xsfiles - Alphabetically sorted -
Each file typically contains one
MODULE = ... PACKAGE = ...section with XS function definitions. -
Contains the
BOOT:section and any final initialization code.
Files starting with _ (other than _header.xs and _footer.xs) are processed after regular files but before _footer.xs.
METHODS
combine
ExtUtils::XSOne->combine(
src_dir => 'lib/MyModule/xs',
output => 'lib/MyModule.xs',
order => [qw(_header context tensor model _footer)], # optional
verbose => 1, # optional
);
Combines multiple XS files into a single output file.
Options:
src_dir(required)-
Directory containing the source
.xsfiles. output(required)-
Path to the output combined
.xsfile. order(optional)-
Array reference specifying the order of files (without
.xsextension). If not provided, files are sorted alphabetically with_headerfirst and_footerlast. verbose(optional)-
If true, prints progress messages to STDERR.
deduplicate(optional, default: true)-
If true (the default), automatically deduplicates
#includeand#definedirectives across all files. This allows each XS file to have its own includes for standalone development while producing a clean combined file.The deduplication process:
- 1. Extracts all
#includeand#definedirectives from the C preamble (code before the firstMODULE =declaration) - 2. Removes duplicate includes (based on the included file path)
- 3. Removes duplicate defines (based on the macro name)
- 4. Collects remaining C code (structs, functions, etc.) with source markers
- 5. Outputs the deduplicated preamble followed by the XS sections
Set to false to disable deduplication and combine files verbatim.
- 1. Extracts all
Returns the number of files combined.
files_in_order
my @files = ExtUtils::XSOne->files_in_order($src_dir);
my @files = ExtUtils::XSOne->files_in_order($src_dir, \@order);
Returns the list of .xs files in the order they would be combined. Useful for debugging or generating dependency lists.
INTEGRATION WITH EXTUTILS::MAKEMAKER
For seamless integration, add a MY::postamble section to regenerate the combined XS file when source files change:
# In Makefile.PL
use ExtUtils::MakeMaker;
use ExtUtils::XSOne;
# Generate initially
ExtUtils::XSOne->combine(
src_dir => 'lib/MyModule/xs',
output => 'lib/MyModule.xs',
);
WriteMakefile(
NAME => 'MyModule',
# ...
);
sub MY::postamble {
my @src_files = ExtUtils::XSOne->files_in_order('lib/MyModule/xs');
my $deps = join(' ', map { "lib/MyModule/xs/$_" } @src_files);
return <<"MAKE_FRAG";
lib/MyModule.xs : $deps
\t\$(PERLRUN) -MExtUtils::XSOne -e 'ExtUtils::XSOne->combine(src_dir => "lib/MyModule/xs", output => "lib/MyModule.xs")'
MAKE_FRAG
}
EXAMPLE DIRECTORY STRUCTURE
lib/
└── MyModule/
├── xs/
│ ├── _header.xs # Includes, types, static vars
│ ├── context.xs # Lugh::Context methods
│ ├── tensor.xs # Lugh::Tensor methods
│ ├── inference.xs # Lugh::Inference methods
│ └── _footer.xs # BOOT section
└── MyModule.pm # Perl module
_header.xs example
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
/* Shared static registry - accessible from all modules */
static void *registry[1024];
static int registry_count = 0;
context.xs example
MODULE = MyModule PACKAGE = MyModule::Context
SV *
new(class, ...)
char *class
CODE:
/* Can access registry from _header.xs */
registry[registry_count++] = create_context();
/* ... */
OUTPUT:
RETVAL
_footer.xs example
MODULE = MyModule PACKAGE = MyModule
BOOT:
/* Initialize shared state */
memset(registry, 0, sizeof(registry));
WHY NOT JUST USE XSMULTI?
XSMULTI is great when your XS modules are truly independent. Use XSMULTI when:
Each module has no shared C state with other modules
Modules only depend on external libraries (like ggml, OpenSSL, etc.)
You want separate compilation for faster incremental builds
Use ExtUtils::XSOne when:
Modules need to share C registries, caches, or static variables
You have a monolithic XS file that's grown too large to maintain
You want modular source organization with single-library deployment
You can also combine both approaches: use XSMULTI for truly independent modules while using XSOne for modules that need to share state.
DEBUGGING
The combined file includes #line preprocessor directives that point back to the original source files. This means:
Compiler errors show the original file and line number
Debuggers (gdb, lldb) can step through original source files
Stack traces reference the original files
AUTHOR
LNATION email@lnation.org
BUGS
Please report any bugs or feature requests to bug-extutils-xsone at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=ExtUtils-XSOne.
SEE ALSO
ExtUtils::MakeMaker, perlxs, perlxstut
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)
1 POD Error
The following errors were encountered while parsing the POD:
- Around line 353:
Non-ASCII character seen before =encoding in '├──'. Assuming UTF-8