Home / Alt manpages / perlmroapi(1)

  • perlmroapi(1)
  • User command
  • linux

Build a Perl MRO Plugin Without Losing the Reference Count

You will finish with the shape of a Perl method resolution order (MRO) plugin: a struct mro_alg, a resolver callback, registration, and an optional per-stash cache. The examples match the installed Perl 5.38.2 and its perl-doc 5.38.2-3.2ubuntu0.6 documentation. Allow about 30 minutes to adapt the interface to an existing XS or core-facing C module.

This is an embedded C interface, not a command-line utility. You need Perl development headers and a C build for a real plugin. Nothing in this guide needs root, changes installed Perl files, or changes a running service. Do not test experimental resolver code in a long-lived process: a bad callback can corrupt Perl's internal state or crash the interpreter.

1. Confirm the Perl version and interface

Start with read-only checks. The version matters because this interface was introduced in Perl 5.10.1, while the installed documentation describes the current local headers and ABI:

$ perl -v
This is perl 5, version 38, subversion 2 (v5.38.2)

$ dpkg-query -W -f='${Package} ${Version}\n' perl perl-doc
perl 5.38.2-3.2ubuntu0.6
perl-doc 5.38.2-3.2ubuntu0.6

The MRO plugin interface lets code provide an ordering other than Perl's default linear depth-first search. C3 is also implemented through this plugin mechanism, while keeping its Perl-level interface. The manual page is the contract for the interface used here; it does not describe a complete module build system.

Checkpoint

If your target interpreter is not Perl 5.10.1 or newer, stop and read that interpreter's own headers and documentation before copying these declarations.

2. Define the algorithm descriptor

Each plugin supplies one descriptor. The resolver is a function pointer. The remaining fields identify the MRO name and let Perl look it up efficiently:

static AV *my_resolve(pTHX_ HV *stash, U32 level);

static struct mro_alg my_mro_alg = {
    my_resolve,
    "example",
    7,
    0,
    0
};

The name may be ISO-8859-1 or UTF-8. The length is the name length. Set kflags to HVhek_UTF8 when the name is UTF-8; otherwise zero is suitable for the ASCII name above. The hash is a precomputed name hash or zero, allowing Perl to calculate it.

Keep the descriptor's storage alive for as long as Perl can call the plugin. A file-scope static descriptor is a straightforward choice. Do not build this structure on a callback stack and then register a pointer to it.

3. Register the descriptor

Register it from the module's initialisation path, using Perl's context argument:

Perl_mro_register(aTHX_ &my_mro_alg);

aTHX_ is part of Perl's thread-aware C calling convention. Keep it exactly where the API declaration expects it. Registration makes the named algorithm available to the interpreter; it does not select that MRO for every class and it does not alter existing class declarations by itself.

Checkpoint

Inspect the installed prototype before compiling against a different Perl build:

$ perl -MConfig -e 'print "$Config{archlib}/CORE\n"'
/usr/lib/x86_64-linux-gnu/perl/5.38/CORE
$ rg -n 'Perl_mro_register' /usr/lib/x86_64-linux-gnu/perl/5.38.2/CORE/proto.h
2541:Perl_mro_register(pTHX_ const struct mro_alg *mro);

The exact path varies with architecture and distribution. Use the path printed by your own Perl, and use headers from the same interpreter you will load.

4. Return the linearised ISA

Perl calls resolve with the stash and a level. The core passes level zero for its call; the parameter exists so a resolver can track depth when it recurses. Return an array reference containing string SVs for the parent class names, in the required order:

static AV *
my_resolve(pTHX_ HV *stash, U32 level)
{
    AV *isa = newAV();

    (void)level;
    av_push(isa, newSVpv(HvENAME(stash), 0));
    return isa;
}

This is only a shape example, not a complete MRO. A real resolver must calculate a consistent linearisation and handle the stash names it encounters. The names should come from HvENAME(). If that returns null, use HvNAME() instead. Do not assume every stash has a usable UTF-8 name.

The example also highlights a safety boundary: check a name before passing it to a string constructor. The abbreviated callback is useful for showing the return type, but it is not ready to ship until it handles null names, inheritance errors, and the reference ownership rules below.

5. Make ownership explicit

The caller is responsible for incrementing the returned array's reference count if it wants to retain it. If the resolver creates a temporary array and keeps no pointer, make it mortal instead:

AV *isa = build_linearised_isa(stash);
return sv_2mortal((SV *)isa);

If the resolver caches the array, return the cached pointer without changing its reference count. Adding an extra increment on every cached return leaks memory. Returning a temporary array without making it mortal can also leak it, while making a cached array mortal can leave the cache pointing at released data.

These are lifetime rules, not optional performance details. Test a plugin with repeated method lookups and inheritance changes under a memory checker before loading it into an application that stays up for days.

6. Cache expensive MRO work per stash

Computing a linearisation can be expensive. Perl provides one private value for an algorithm in the stash's MRO metadata. Read it with MRO_GET_PRIVATE_DATA():

struct mro_meta *meta = HvMROMETA(stash);
SV *private_sv = MRO_GET_PRIVATE_DATA(meta, &my_mro_alg);

The value is an SV *, but an AV * or another value that can be cast to SV * can be stored there. On a cache miss, compute the result and store it against the same descriptor:

AV *isa = build_linearised_isa(stash);
SV *private_sv = (SV *)isa;
Perl_mro_set_private_data(aTHX_ meta, &my_mro_alg, private_sv);
return isa;

The private-data cache takes ownership of a reference to the value, in the same broad ownership style as hv_store(). Decide which reference belongs to the cache before returning the value. Keep cache invalidation in view: a cached linearisation must not survive a class hierarchy change unless the surrounding implementation guarantees that it is refreshed.

7. Check the result without changing the machine

There is no useful shell command that can validate an arbitrary resolver's ordering. Check the source-level contract, compile against matching Perl headers, and run a small isolated Perl process that exercises a hierarchy representative of your algorithm. Keep the test process separate from the service that will eventually use the module.

For examples from Perl itself, inspect the C3 implementation in ext/mro/mro.xs and the default depth-first implementation in mro_core.c, as named by the manual page. These are implementation examples, not permission to copy internal code without checking the licence and version.

If a test fails, remove the module from that test's load path or stop the test process. No undo operation is needed for the examples in this guide because they only inspect files and show C fragments. Do not delete a working module or edit the system Perl installation while diagnosing a resolver bug.

Done means

  • The plugin targets a confirmed Perl version and uses matching CORE headers.
  • A long-lived struct mro_alg supplies a correctly measured name, flags, hash and resolver.
  • Registration uses Perl_mro_register(aTHX_ ...) from initialisation code.
  • The resolver returns parent names in a consistent order and handles missing stash names.
  • Temporary and cached arrays have deliberate reference-count ownership.
  • Private data is keyed by the plugin descriptor and invalidated when its assumptions no longer hold.
  • Tests run in an isolated process, with no system Perl files or services changed.