Introduction
Since I last wrote a XS tutorial my knowledge within C has increased this has come from improvements in LLM software that has assisted in improving my knowledge where previously I would be stuck. This knowledge and software has since enabled me to craft more elegant and efficient XS implementations.
Today I will share with you my technique for writing reusable C/XS code.
One of the most powerful patterns in XS development is writing your core logic in pure C header files. This gives you:
-
Zero-cost reuse - no runtime linking, no shared libraries, just a
#includeline. - No Perl dependency in the C layer - your headers work in any C project
- Compile-time inlining - the compiler sees everything, optimises aggressively
-
Simple distribution - headers are installed alongside the Perl module via
PM
This tutorial walks through the complete pattern step by step, using a minimal working example you can build and run yourself.
The Example
We will create two distributions:
-
Abacus - a provider distribution that ships a reusable pure-C
abacus_math.hheader containing simple arithmetic functions -
Tally - a consumer distribution that
#includes the Abacus header to build its own XS module, without duplicating any C code
As always lets start by creating the distributions that we will need for this tutorial. Open your terminal and run module-starter. If you are using a modern version of Module::Starter then the command has changed slightly since my last posts.
module-starter --module=Abacus --author="LNATION <email@lnation.org>"
module-starter --module=Tally --author="LNATION <email@lnation.org>"
Part 1: The Provider Distribution (Abacus)
Write the pure-C header
This is the reusable part. It has zero Perl dependencies - just standard C.
Now enter the Abacus and create the include directory:
cd Abucus
mkdir include
Then create a new file abacus_math.h:
touch abacus_math.h
vim abacus_math.h
Paste the following code into the file:
#ifndef ABACUS_MATH_H
#define ABACUS_MATH_H
/*
* abacus_math.h - Pure C arithmetic library (no Perl dependencies)
*
* This header is the reusable entry point for any C or XS project
* that needs basic arithmetic operations. It has ZERO Perl/XS
* dependencies.
*
* Usage from another XS module:
*
* #include "abacus_math.h"
*
* Build: add -I/path/to/Abacus/include to your compiler flags.
*/
#include <stdint.h>
/* ── Error handling hook ─────────────────────────────────────────
*
* Consumers can #define ABACUS_FATAL(msg) before including this
* header to route errors through their own mechanism.
*
* In an XS module you would typically do:
*
* #define ABACUS_FATAL(msg) croak("%s", (msg))
* #include "abacus_math.h"
*
* In plain C the default behaviour is fprintf + abort.
*/
#ifndef ABACUS_FATAL
# include <stdio.h>
# include <stdlib.h>
# define ABACUS_FATAL(msg) do { \
fprintf(stderr, "abacus fatal: %s\n", (msg)); \
abort(); \
} while (0)
#endif
/* ── Arithmetic operations ───────────────────────────────────── */
static inline int32_t
abacus_add(int32_t a, int32_t b) {
return a + b;
}
static inline int32_t
abacus_subtract(int32_t a, int32_t b) {
return a - b;
}
static inline int32_t
abacus_multiply(int32_t a, int32_t b) {
return a * b;
}
static inline int32_t
abacus_divide(int32_t a, int32_t b) {
if (b == 0) {
ABACUS_FATAL("division by zero");
}
return a / b;
}
static inline int32_t
abacus_factorial(int32_t n) {
int32_t result = 1;
int32_t i;
if (n < 0) {
ABACUS_FATAL("factorial of negative number");
}
for (i = 2; i <= n; i++) {
result *= i;
}
return result;
}
#endif /* ABACUS_MATH_H */
The code above demonstrates three critical design patterns for reusable C headers:
static inline functions eliminate linker complications by giving each translation unit its own copy of the function. The compiler can then inline these small arithmetic operations directly into the call site, producing zero-overhead abstractions. This is key to the "zero-cost reuse" principle—there is no shared library dependency, no function call overhead, just pure generated code.
The ABACUS_FATAL macro hook provides a customization point for error handling. By default, it calls fprintf() and abort() in standalone C programs; but consumers can #define ABACUS_FATAL(msg) croak("%s", (msg)) before including the header to integrate seamlessly with Perl's exception system. This single mechanism allows the same C header to work across Perl XS, plain C, and other environments without code duplication.
The use of only stdint.h integers and no Perl types ensures the header remains truly portable. There are no SV* pointers, no pTHX context variables, no XSUB.h includes—just standard C99 types. This purity is what allows the header to be #included into any C or XS project without creating hidden Perl dependencies at the C layer.
Write the Perl-facing XS header
Next we will add another header which will hold the Perl/XS specific logic. Inside the include directory create a new file called abacus.h. The rational behind this thin wrapper is to pull in Perl's headers and sets up the ABACUS_FATAL macro to use croak(). To reiterate only a XS distribution should include this header, whereas abacus_math.h is generic and could be used by other languages which bind C.
touch abacus.h
vim abacus.h
Paste the following code into the file
#ifndef ABACUS_H
#define ABACUS_H
/*
* abacus.h - Perl XS wrapper header for the Abacus library
*
* This header sets up Perl-specific error handling and includes
* the pure C core library.
*
* For reuse from OTHER XS modules without Perl overhead, include
* abacus_math.h directly instead (see that header for usage).
*/
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
/* Route fatal errors through Perl's core croak() */
#define ABACUS_FATAL(msg) croak("%s", (msg))
/* Pull in the pure-C library */
#include "abacus_math.h"
#endif /* ABACUS_H */
Now any .xs file can #include "abacus.h" and get the full Perl/XS environment plus all the pure-C functions, with errors properly integrated.
Write the XS file
Next we will create the XS file, return to the root directory and then enter the lib directoy where you should see the Abacus.pm file already. Create a new XS file called Abacus.xs. This will be the glue that exposes the C functions to Perl.
cd ../lib
touch Abacus.xs
vim Abacus.xs
#include "abacus.h"
MODULE = Abacus PACKAGE = Abacus
PROTOTYPES: DISABLE
int
add(a, b)
int a
int b
CODE:
RETVAL = abacus_add(a, b);
OUTPUT:
RETVAL
int
subtract(a, b)
int a
int b
CODE:
RETVAL = abacus_subtract(a, b);
OUTPUT:
RETVAL
int
multiply(a, b)
int a
int b
CODE:
RETVAL = abacus_multiply(a, b);
OUTPUT:
RETVAL
int
divide(a, b)
int a
int b
CODE:
RETVAL = abacus_divide(a, b);
OUTPUT:
RETVAL
int
factorial(n)
int n
CODE:
RETVAL = abacus_factorial(n);
OUTPUT:
RETVAL
As you can see we include abacus.h which pulls in all the C that we need to create our XS module. We then define add, subtract, multiply, divide and factorial as XSUBs. As you should know by now XSUBs can be called directly from your perl code.
Write the Perl module with include_dir()
Next open the pm file and update to add an exporter for the XSUBS we have just created.
package Abacus;
use 5.008003;
use strict;
use warnings;
our $VERSION = '0.01';
use Exporter 'import';
our @EXPORT_OK = qw(add subtract multiply divide factorial);
require XSLoader;
XSLoader::load('Abacus', $VERSION);
Now the critical piece that makes header sharing work. A include_dir() method which returns the path to the installed headers so that consumer distributions can find them at build time.
sub include_dir {
my $dir = $INC{'Abacus.pm'};
$dir =~ s{Abacus\.pm$}{Abacus/include};
return $dir;
}
1;
How include_dir() works:
- When Perl loads
Abacus.pm, it records the full path in%INC(e.g./usr/lib/perl5/site_perl/Abacus.pm) -
include_dir()replacesAbacus.pmwithAbacus/include - That directory exists because
Makefile.PLinstalls the headers there (see next step)
Write the Makefile.PL that installs headers
The PM hash is what makes headers available to other distributions after install. It maps source files to their installation destinations.
Abacus/Makefile.PL
use 5.008003;
use strict;
use warnings;
use ExtUtils::MakeMaker;
WriteMakefile(
NAME => 'Abacus',
AUTHOR => 'Your Name <you@example.com>',
VERSION_FROM => 'lib/Abacus.pm',
ABSTRACT_FROM => 'lib/Abacus.pm',
LICENSE => 'artistic_2',
MIN_PERL_VERSION => '5.008003',
CONFIGURE_REQUIRES => {
'ExtUtils::MakeMaker' => '0',
},
TEST_REQUIRES => {
'Test::More' => '0',
},
PREREQ_PM => {},
XSMULTI => 1,
# XS configuration
INC => '-I. -Iinclude',
OBJECT => '$(O_FILES)',
# *** THIS IS THE KEY PART ***
# Install headers alongside the module so dependent
# distributions can find them via Abacus->include_dir()
PM => {
'lib/Abacus.pm' => '$(INST_LIB)/Abacus.pm',
'include/abacus.h' => '$(INST_LIB)/Abacus/include/abacus.h',
'include/abacus_math.h' => '$(INST_LIB)/Abacus/include/abacus_math.h',
},
dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz' },
clean => { FILES => 'Abacus-*' },
);
The PM hash does two things:
- Installs
Abacus.pmas normal -
Copies the header files into
Abacus/include/alongside the module
After make install, the filesystem looks like:
site_perl/
Abacus.pm
Abacus/
include/
abacus.h
abacus_math.h
Write a test
Abacus/t/01-basic.t
use strict;
use warnings;
use Test::More;
use Abacus qw(add subtract multiply divide factorial);
is(add(2, 3), 5, 'add');
is(subtract(10, 4), 6, 'subtract');
is(multiply(3, 7), 21, 'multiply');
is(divide(20, 4), 5, 'divide');
is(factorial(5), 120, 'factorial');
eval { divide(1, 0) };
like($@, qr/division by zero/, 'divide by zero croaks');
eval { factorial(-1) };
like($@, qr/negative/, 'negative factorial croaks');
done_testing;
Build and install Abacus
cd Abacus
perl Makefile.PL
make
make test
make install # installs headers into site_perl
Part 2: The Consumer Distribution (Tally)
Tally is a separate distribution that reuses Abacus's C arithmetic without duplicating any code. It adds its own "running total" functionality on top.
Write the Makefile.PL that finds Abacus headers
This is where the consumer locates the provider's headers. The two-step resolution strategy supports both installed (CPAN) and development (sibling
directory) scenarios.
Tally/Makefile.PL
use 5.008003;
use strict;
use warnings;
use ExtUtils::MakeMaker;
# Resolve Abacus include directory:
# 1. Try installed Abacus module (CPAN / system)
# 2. Fall back to sibling directory (development)
my $abacus_inc;
eval {
no warnings 'redefine';
local *XSLoader::load = sub {}; # skip XS bootstrap
require Abacus;
my $dir = Abacus->include_dir();
$abacus_inc = $dir if $dir && -d $dir;
};
if (!$abacus_inc && -d '../Abacus/include') {
$abacus_inc = '../Abacus/include';
}
die "Cannot find Abacus include directory.\n"
. "Install Abacus or place it as a sibling directory.\n"
unless $abacus_inc;
WriteMakefile(
NAME => 'Tally',
AUTHOR => 'Your Name <you@example.com>',
VERSION_FROM => 'lib/Tally.pm',
ABSTRACT_FROM => 'lib/Tally.pm',
LICENSE => 'artistic_2',
MIN_PERL_VERSION => '5.008003',
CONFIGURE_REQUIRES => {
'ExtUtils::MakeMaker' => '0',
'Abacus' => '0.01',
},
TEST_REQUIRES => {
'Test::More' => '0',
},
PREREQ_PM => {
'Abacus' => '0.01',
},
# Point the compiler at Abacus's installed headers
INC => "-I$abacus_inc",
OBJECT => '$(O_FILES)',
dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz' },
clean => { FILES => 'Tally-*' },
);
Let's walk through the header resolution:
-
Try the installed path first -
require Abacusloads the module, thenAbacus->include_dir()returns the path where the headers were installed. We stub outXSLoader::loadbecause we only need the pure-Perlinclude_dir()method, not the XS functions. -
Fall back to sibling directory - during development, Abacus and Tally
often live side by side.
../Abacus/includehandles this case. - Die with a clear message if neither path works.
The resolved path is passed to INC, which adds it to the C compiler's include search path (-I/path/to/Abacus/include).
Abacus is listed in both CONFIGURE_REQUIRES and PREREQ_PM:
-
CONFIGURE_REQUIRESensures Abacus is installed beforeMakefile.PLruns (needed because werequire Abacusat configure time) -
PREREQ_PMensures it is available at runtime too
Write the XS file
This is where the reuse happens. Tally includes abacus_math.h directly -
no Perl coupling, just pure C function calls.
Tally/Tally.xs
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
/* Hook Abacus errors into Perl's croak() */
#define ABACUS_FATAL(msg) croak("%s", (msg))
/* Include the pure-C header from Abacus - no Perl deps in the header */
#include "abacus_math.h"
/* ── Tally's own C logic, built on top of Abacus ─────────────── */
typedef struct {
int32_t total;
} tally_state_t;
static inline void
tally_init(tally_state_t *state) {
state->total = 0;
}
static inline int32_t
tally_add(tally_state_t *state, int32_t value) {
state->total = abacus_add(state->total, value);
return state->total;
}
static inline int32_t
tally_subtract(tally_state_t *state, int32_t value) {
state->total = abacus_subtract(state->total, value);
return state->total;
}
static inline int32_t
tally_multiply_total(tally_state_t *state, int32_t value) {
state->total = abacus_multiply(state->total, value);
return state->total;
}
static inline int32_t
tally_get(tally_state_t *state) {
return state->total;
}
static inline void
tally_reset(tally_state_t *state) {
state->total = 0;
}
/* ── XS bindings ─────────────────────────────────────────────── */
MODULE = Tally PACKAGE = Tally
PROTOTYPES: DISABLE
SV *
new(class)
const char *class
CODE:
tally_state_t *state;
Newxz(state, 1, tally_state_t);
tally_init(state);
RETVAL = newSV(0);
sv_setref_pv(RETVAL, class, (void *)state);
OUTPUT:
RETVAL
int
add(self, value)
SV *self
int value
CODE:
tally_state_t *state = INT2PTR(tally_state_t *, SvIV(SvRV(self)));
RETVAL = tally_add(state, value);
OUTPUT:
RETVAL
int
subtract(self, value)
SV *self
int value
CODE:
tally_state_t *state = INT2PTR(tally_state_t *, SvIV(SvRV(self)));
RETVAL = tally_subtract(state, value);
OUTPUT:
RETVAL
int
multiply_total(self, value)
SV *self
int value
CODE:
tally_state_t *state = INT2PTR(tally_state_t *, SvIV(SvRV(self)));
RETVAL = tally_multiply_total(state, value);
OUTPUT:
RETVAL
int
total(self)
SV *self
CODE:
tally_state_t *state = INT2PTR(tally_state_t *, SvIV(SvRV(self)));
RETVAL = tally_get(state);
OUTPUT:
RETVAL
void
reset(self)
SV *self
CODE:
tally_state_t *state = INT2PTR(tally_state_t *, SvIV(SvRV(self)));
tally_reset(state);
void
DESTROY(self)
SV *self
CODE:
tally_state_t *state = INT2PTR(tally_state_t *, SvIV(SvRV(self)));
Safefree(state);
Notice that Tally includes abacus_math.h (the pure C header), not abacus.h (the Perl-facing wrapper). This is intentional - Tally has its own Perl/XS setup and only needs the C functions.
Write the Perl module
Tally/lib/Tally.pm
package Tally;
use 5.008003;
use strict;
use warnings;
our $VERSION = '0.01';
require XSLoader;
XSLoader::load('Tally', $VERSION);
1;
__END__
=head1 NAME
Tally - Running total calculator using Abacus C headers
=head1 SYNOPSIS
use Tally;
my $t = Tally->new;
$t->add(10); # total is now 10
$t->add(5); # total is now 15
$t->subtract(3); # total is now 12
$t->multiply_total(2); # total is now 24
say $t->total; # 24
$t->reset; # back to 0
=cut
Write a test
Tally/t/01-basic.t
use strict;
use warnings;
use Test::More;
use_ok('Tally');
my $t = Tally->new;
isa_ok($t, 'Tally');
is($t->total, 0, 'starts at zero');
is($t->add(10), 10, 'add 10');
is($t->add(5), 15, 'add 5');
is($t->subtract(3), 12, 'subtract 3');
is($t->multiply_total(2), 24, 'multiply by 2');
is($t->total, 24, 'total is 24');
$t->reset;
is($t->total, 0, 'reset to zero');
done_testing;
Build Tally (development mode)
cd Tally
perl Makefile.PL # finds ../Abacus/include automatically
make
make test
I hope you found this tutorial useful! If you have questions about XS, C header reuse, or building modular Perl/C libraries, please leave a message.
Top comments (0)