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 #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
);

Combines multiple XS files into a single output file.

Options:

src_dir (required)

Directory containing the source .xs files.

output (required)

Path to the output combined .xs file.

order (optional)

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. Outputs the deduplicated preamble followed by the XS sections

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  => '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