The no-XS rule, and the shapes you will meet#

pperl cannot load a compiled extension. This is structural, not a setting: XS binds to perl5’s C-level ABI, and pperl is a Rust reimplementation of Perl 5 that does not have one to bind to. An XSLoader::load under pperl fails with the message such modules are written to expect:

dynamic loading not available in this perl

What that means for an install depends entirely on how the distribution is put together. There are five shapes in the wild and they behave very differently, so it is worth knowing which one you are looking at.

1. Pure Perl#

Nothing to say. It installs and runs exactly as it does anywhere else. This is the large majority of CPAN.

2. A Perl module with an optional XS accelerator#

The module is Perl; a separate distribution speeds it up if present. Readonly and ReadonlyXS are the classic pair.

Install the Perl one and ignore the accelerator. The module probes for its companion, does not find it, and uses its own implementation - which is the documented, supported path, not a degraded mode.

pperl --cpan Readonly       # yes
pperl --cpan ReadonlyXS     # no, and not needed

This is the shape to prefer when you have a choice of dependency. The Perl module is the module, and XS is an optimisation.

3. One distribution, both halves, XS optional at build time#

The distribution ships C and Perl, and its Makefile.PL treats the compile as optional: if the compiler step fails or is declined, it installs the Perl half and carries on. At runtime the module notices the XS is absent and uses its own fallback.

Cookie::Baker and WWW::Form::UrlEncoded are of this shape. They install cleanly under pperl.

4. One distribution, both halves, XS mandatory at build time#

Same idea, but the build compiles C unconditionally. The compile fails, the installer aborts, and with an ordinary installer the Perl half - which would have worked - never reaches disk.

HTTP::Parser::XS is the worked example. pperl implements it natively now, so you will not hit it in practice - an installer sees 0.17 already present and never fetches the distribution at all. It is still the clearest illustration of the shape, because the failure is not where you would guess. Its runtime logic is impeccable:

eval {
    require XSLoader;
    XSLoader::load(__PACKAGE__, $VERSION);
    $BACKEND = 'xs';
};
die $@ if $@ && $BACKEND =~ /\bxs\b/;    # only if you FORCED xs
if (not __PACKAGE__->can('parse_http_response')) {
    require HTTP::Parser::XS::PP;        # automatic fallback
    $BACKEND = 'pp';
}

Given the files on disk, that falls back by itself, with no intervention. The problem is purely that Makefile.PL builds picohttpparser unconditionally, so the files never get there. Its PERL_ONLY=1 switch selects the runtime backend and drives a test target; it does not make the build skip the C.

Note

A native implementation is the escape hatch, and it is the one that was used here. But it only holds while pperl’s version satisfies what dependents ask for: pperl reports the native version, so a dependency on HTTP::Parser::XS >= 0.18 would send the installer after the distribution again and the abort would come back.

A shape-4 distribution pperl does not implement natively installs all the same, because pperl’s ExtUtils::MakeMaker never compiles. Under pperl a Makefile.PL produces a Makefile for the Perl half only: no compiler, no perl header files and no xsubpp are looked for, so the build cannot fail at the C. Two things then decide whether the distribution is installed:

  • every module in it that names an XS loader is loaded once. One that falls back to Perl, as above, loads. One that is only the front of its compiled part does not: it is left out and named, and the rest of the distribution goes on;

  • the distribution’s own tests run, as for any other module.

pperl: left out, because without the compiled part, which pperl cannot build, it does not load: Some/Dist/Fast.pm

A distribution built with Module::Build still attempts its compile and fails. For those, --cpan installs the Perl files the build had already copied into its blib directory, after the same load check, and says so:

pperl: Some-Dist-1.23-0: XS build failed; installed its 4 pure-Perl file(s)

That run ends with a failure status, because the distribution’s tests never ran: test what was installed before you rely on it.

5. XS only#

No Perl implementation exists anywhere in the distribution. There is nothing to fall back to and nothing pperl can do with it. The load check leaves no module over, and the build stops:

pperl: left out, because without the compiled part, which pperl cannot build, it does not load: String/CRC32.pm
pperl: not built: no module of this distribution loads without its compiled part

Nothing of the distribution is installed.

When you actually need the speed#

The reason XS exists is performance, so it is fair to ask what replaces it. pperl’s answer has three parts, and none of them is “write it in Perl and hope”.

  • Native modules. About a hundred of the modules people reach for XS to get are already Rust inside the binary, at Rust speed, with no build step. See what pperl already has.

  • The JIT. Hot loops are compiled, so the pure-Perl version of an algorithm is not automatically the slow version. See the performance guide.

  • Peta::FFI. When you genuinely need a C library, call it directly rather than wrapping it in XS. See the FFI primer.

If you hit a module in shape 4 or 5 that you cannot work around, that is worth reporting: petamem/pperl-bugtracker#issues