NAME
ExtUtils::XSOne - Combine multiple XS files into a single shared library
VERSION
Version 0.03
SYNOPSIS
# In Makefile.PL
use ExtUtils::MakeMaker;
use ExtUtils::XSOne;
# Combine XS files before WriteMakefile
ExtUtils::XSOne->combine(
src_dir => 'lib/MyModule/xs',
output => '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 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 can organize code in two ways:
Traditional layout - all XS in a single xs/ subdirectory:
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
Hierarchical layout - XS files alongside their .pm files:
lib/
└── Foo/
├── _header.xs # Common includes, types, static vars
├── _footer.xs # BOOT section
├── Context.pm
├── Context.xs # MODULE = Foo PACKAGE = Foo::Context
├── Tensor.pm
└── Tensor.xs # MODULE = Foo PACKAGE = Foo::Tensor
Both are combined at build time into a single 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
);
Or with recursive mode for hierarchical layouts:
ExtUtils::XSOne->combine(
src_dir => 'lib/MyModule',
output => 'MyModule.xs',
recursive => 1,
verbose => 1,
);
Combines multiple XS files into a single output file.
Options:
src_dir(required)-
Directory containing the source
.xsfiles. In recursive mode, this is the base directory to scan recursively. output(required)-
Path to the output combined
.xsfile. recursive(optional, default: false)-
If true, recursively scans subdirectories for
.xsfiles. This allows XS files to be placed alongside their corresponding.pmfiles rather than in a singlexs/subdirectory.In recursive mode:
_header.xsfiles are processed first, ordered by directory depth (shallowest first)Package
.xsfiles are then processed alphabetically by full path_footer.xsfiles are processed last, ordered by directory depth (deepest first, reverse of headers)
This enables hierarchical layouts like:
lib/ └── MyModule/ ├── _header.xs # Top-level shared state ├── Context.pm ├── Context.xs # Context package ├── Tensor.pm ├── Tensor.xs # Tensor package └── _footer.xs # BOOT section order(optional, ignored in recursive mode)-
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. C code from
_header.xsis placed first in the combined preamble, ensuring that types, macros, and static variables defined in the header are available to C code in other XS files - 6. Outputs the deduplicated preamble followed by the XS sections
This ordering is important: helper functions in package XS files (e.g.,
Memory.xs) can access definitions from_header.xsbecause the header's C code appears first in the combined output.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 => '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 STRUCTURES
Traditional Layout (non-recursive)
All XS files in a single xs/ subdirectory:
lib/
└── MyModule/
├── xs/
│ ├── _header.xs # Includes, types, static vars
│ ├── context.xs # MyModule::Context methods
│ ├── tensor.xs # MyModule::Tensor methods
│ ├── inference.xs # MyModule::Inference methods
│ └── _footer.xs # BOOT section
└── MyModule.pm # Perl module
Hierarchical Layout (recursive mode)
XS files alongside their corresponding .pm files:
lib/
└── MyModule/
├── _header.xs # Shared includes, types, static vars
├── _footer.xs # BOOT section
├── MyModule.pm # Main module
├── Context.pm # Context module
├── Context.xs # Context XS code
├── Tensor.pm # Tensor module
├── Tensor.xs # Tensor XS code
└── Inference.pm # Inference module
└── Inference.xs # Inference XS code
Use recursive => 1 in combine() for this layout.
_header.xs example
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
/* Shared constants and static variables - accessible from all modules */
#define MAX_SLOTS 10
static double slots[MAX_SLOTS];
static int slot_count = 0;
/* Shared helper function */
static int is_valid_slot(int slot) {
return (slot >= 0 && slot < MAX_SLOTS);
}
Note: _header.xs typically has no MODULE = line - its entire content is treated as C preamble code that gets placed first in the combined output.
context.xs example
Package XS files can define their own C helper functions that use definitions from _header.xs:
/*
* Context-specific helpers using shared state from _header.xs
*/
/* This function can use MAX_SLOTS, slots[], and is_valid_slot()
because _header.xs C code is placed first in the combined preamble */
static int ctx_count_used(void) {
int count = 0;
for (int i = 0; i < MAX_SLOTS; i++) {
if (slots[i] != 0.0) count++;
}
return count;
}
MODULE = MyModule PACKAGE = MyModule::Context
int
used_slots()
CODE:
RETVAL = ctx_count_used();
OUTPUT:
RETVAL
_footer.xs example
MODULE = MyModule PACKAGE = MyModule
BOOT:
/* Initialize shared state */
memset(registry, 0, sizeof(registry));
You can also combine both approaches: use XSMULTI for truly independent modules while using XSOne for modules that need to share state.
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 440:
Non-ASCII character seen before =encoding in '├──'. Assuming UTF-8