Build a Perl MRO Plugin Without Losing the Reference Count
Get the reference counting wrong in a Perl MRO plugin and you get a slow memory leak or a crash that only shows up days into a long-running process. This walks through the shape of a method resolution order plugin: a struct mro_alg, a resolver callback, registration, and an optional per-stash cache. 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.
The route
Jump straight to the step you need, or tick off Done means at the end.
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 here needs root, changes installed Perl files, or touches a running service.
Warning
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, and 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.
kflagsisHVhek_UTF8when 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 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 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.
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.
Warning
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 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
- Targeted a confirmed Perl version and used matching CORE headers.
- Built a long-lived
struct mro_algwith a correctly measured name, flags, hash and resolver. - Registered it with
Perl_mro_register(aTHX_ ...)from initialisation code. - Made the resolver consistent, returning parent names in order and handling missing stash names.
- Gave temporary and cached arrays deliberate reference-count ownership.
- Keyed private data by the plugin descriptor, invalidated when its assumptions no longer hold.
- Ran tests in an isolated process, with no system Perl files or services changed.