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 #include directives, type definitions, static variables, and helper functions shared by all modules.

2. Other .xs files - Alphabetically sorted

Each file typically contains one MODULE = ... PACKAGE = ... section with XS function definitions.

3. _footer.xs - Always last (if present)

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 .xs files. In recursive mode, this is the base directory to scan recursively.

output (required)

Path to the output combined .xs file.

recursive (optional, default: false)

If true, recursively scans subdirectories for .xs files. This allows XS files to be placed alongside their corresponding .pm files rather than in a single xs/ subdirectory.

In recursive mode:

  • _header.xs files are processed first, ordered by directory depth (shallowest first)

  • Package .xs files are then processed alphabetically by full path

  • _footer.xs files 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 .xs extension). If not provided, files are sorted alphabetically with _header first and _footer last.

verbose (optional)

If true, prints progress messages to STDERR.

deduplicate (optional, default: true)

If true (the default), automatically deduplicates #include and #define directives 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 #include and #define directives from the C preamble (code before the first MODULE = 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.xs is 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.xs because the header's C code appears first in the combined output.

Set to false to disable deduplication and combine files verbatim.

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