NAME
Sub::Protected - Enforce protected subroutine access (Java/C++ semantics)
VERSION
0.03
SYNOPSIS
package Foo;
use Sub::Protected; # enables the :Protected attribute
sub new { bless {}, shift }
# Attribute form (preferred: protection lives next to the definition)
sub _helper :Protected {
...
}
sub public_method {
my $self = shift;
$self->_helper; # OK -- same package
}
# ----------------------------------------------------------------
package Bar;
use Sub::Protected qw(_other _private); # declarative form
sub _other { 'other' }
sub _private { 'private' }
DESCRIPTION
Enforces Java/C++-style "protected" access at runtime: a subroutine decorated with :Protected (or named in use Sub::Protected qw(...)) may only be called from within its defining package or from a subclass of that package. Any other caller causes a Carp::croak with a descriptive message.
Two usage forms
- Attribute form (preferred)
-
sub _helper :Protected { ... }The
:Protectedattribute is registered inUNIVERSALvia Attribute::Handlers whenSub::Protectedis loaded, so every package has access to it without any furtheruseor inheritance. The sub is wrapped atCHECKtime, or, if the module is loaded afterCHECK(Perl 5.14 or later), at the end of the file being compiled. This form is preferred because the protection declaration sits next to the definition and wrapping happens at compile time (making pre-wrap raw-coderef captures impossible). - Declarative form
-
use Sub::Protected qw(_helper _other);Each named sub is looked up in the caller's stash and wrapped at
CHECKtime (or immediately if the module is loaded at runtime viarequire). All named subs must be defined beforeCHECKfires -- i.e. they must be compile-time named subs in the same file, not generated at runtime.
Bypass for testing
Either condition alone (OR logic) disables all access checks:
$Sub::Protected::BYPASSset to a true value. Uselocalin tests.$ENV{HARNESS_ACTIVE}set (the convention used by Test::Harness/prove).
$Sub::Protected::BYPASS is the recommended form for new test code; it is explicit and does not depend on the test runner. HARNESS_ACTIVE is a zero-config convenience.
The HARNESS_ACTIVE bypass can be disabled by setting:
$Sub::Protected::config{harness_bypass} = 0;
Configuration
The module exposes %Sub::Protected::config for runtime configuration:
harness_bypass(default: 1)-
When true, access checks are skipped whenever
$ENV{HARNESS_ACTIVE}is set. Set to 0 to test protection behaviour from within a test harness.
The hash is compatible with Object::Configure for dependency-injection scenarios.
Error message format
_helper() is a protected method of Foo and cannot be called from Bar
The error is raised with "croak" in Carp, so it reports the file and line of the offending call, not a line inside Sub::Protected.
How access is decided
Only the immediate caller counts
The decision is made on the package of the code that called the protected sub. If
Bar::runcallsFoo::public, which callsFoo::_helper, the call is allowed: the immediate caller isFoo. IfFoo::publicinstead calls aBarmethod that callsFoo::_helper, it is blocked, however deep the chain.Subclasses are allowed
A caller whose package
isathe owner is allowed, so calls throughSUPER::,$self->can('_helper')and ordinary inherited method calls all work from a subclass.The wrapper is invisible
The wrapper hands over with
goto &sub, so inside the protected subcaller()reports the real caller, not Sub::Protected. Arguments (including aliasing of@_), calling context (list, scalar or void) and return values, including false ones, pass through unchanged.
PUBLIC INTERFACE
import
use Sub::Protected; # attribute form -- no arguments
use Sub::Protected qw(_a _b _c); # declarative form
Purpose
Called automatically by use Sub::Protected.
With no arguments: does nothing beyond making the :Protected attribute globally available (which happens when the module is first loaded).
With one or more sub names: registers those subs in the calling package for wrapping at CHECK time. If CHECK has already fired (e.g. the module was loaded via runtime require, a plugin loader or a string eval), wrapping is applied immediately. Each named sub must be defined before CHECK fires (for pre-CHECK loads) or before import is called (for post-CHECK loads), so a module loaded at run time should call Sub::Protected->import(...) at the end of the file, after its subs are defined.
Run-time loading needs Perl 5.14 or later. On older Perls the module must be loaded at compile time (use, or require inside BEGIN).
Arguments
$class(positional, required)-
The name of the importing class. Set automatically by the
usemechanism. Must be a non-empty string. @subs(positional, optional)-
Zero or more sub names to protect in the calling package. Each must be a valid Perl identifier: matching
/\A[_a-zA-Z]\w*\z/.These calls are equivalent:
Sub::Protected->import(qw(_a _b)); Sub::Protected->import([ qw(_a _b) ]); Sub::Protected->import({ subs => [ qw(_a _b) ] });A plain list is always a list of names, so
qw(subs _x)protects bothsubsand_x. A single arrayref or hashref is normalised by Params::Get.
Returns
$class (the importing class name). The return value is ignored by the use mechanism; it is provided for optional method chaining at the class level.
Side effects
Each supplied sub name is appended to an internal pending list (if pre-CHECK) or wrapped immediately (if post-CHECK).
The pending list is consumed and cleared when the CHECK block fires.
$@,$!and$_are left unchanged, as are any pendingalarm()timers.
Example
package Foo;
use Sub::Protected qw(_helper _init);
sub _helper { ... } # will be protected
sub _init { ... } # will be protected
API SPECIFICATION
Input
# Params::Get::get_params / Params::Validate::Strict schema
{
# class is the implicit first argument, set by Perl's 'use' mechanism
subs => {
type => 'array',
required => 0,
each => {
type => 'string',
regex => qr/\A[_a-zA-Z]\w*\z/,
},
},
}
Output
# Return::Set schema
{
type => 'string',
desc => 'The importing class name ($class), for optional chaining.',
}
MESSAGES
The following table lists every error or warning this method can produce.
Message Meaning
---------------------------------------- -------------------------------------
"Sub::Protected->import: 'NAME' is not a A sub name passed to import() failed
valid Perl identifier" the identifier regex. Use a name
matching /\A[_a-zA-Z]\w*\z/.
NAME is shown as '' when the
name was undef or a reference.
"Sub::Protected: PKG::NAME is not defined" The named sub was not found in the
package stash at wrap time. For
pre-CHECK loads, ensure the sub is
a compile-time named sub. For
post-CHECK/runtime loads, ensure
the sub is defined before import().
Calling a protected sub from outside its package or subclasses croaks with the message shown in "Error message format".
FORMAL SPECIFICATION
ValidName ≙ { n : seq CHAR | n matches /\A[_a-zA-Z]\w*\z/ }
┌─ Import ──────────────────────────────────────────
│ ΔRegistry
│ class? : Package ; caller? : Package
│ names? : seq SubName ; postCheck : 𝔹
│ result! : Package
├───────────────────────────────────────────────────
│ ∀ n ∈ ran names? • n ∈ ValidName
│ postCheck ⇒ (∀ n ∈ ran names? • defined(caller?, n))
│ postCheck ⇒ protected′ = protected ∪ { n : ran names? • (caller?, n) }
│ ¬postCheck ⇒ pending′ = pending ⁀ ⟨ n : names? • (caller?, n) ⟩
│ result! = class?
└───────────────────────────────────────────────────
-- Failure cases (croak, Registry unchanged):
-- ∃ n ∈ ran names? • n ¬in; ValidName
-- ⇒ "Sub::Protected->import: 'n' is not a valid Perl identifier"
-- postCheck ∧ ∃ n ∈ ran names? • ¬defined(caller?, n)
-- ⇒ "Sub::Protected: caller?::n is not defined"
KNOWN LIMITATIONS
- Runtime-only
-
Checks are runtime only; there is no compile-time enforcement.
- Raw coderef bypass
-
A raw code reference obtained before wrapping (via
can()or direct\&Foo::_helper) bypasses the check. The attribute form prevents this because wrapping happens at compile time. - Moo/Moose method modifiers
-
Method modifiers applied after Sub::Protected has wrapped a sub will wrap the wrapper. Apply Sub::Protected last, or use the declarative form in a
CHECKblock after the class is fully built. - Run-time loading on Perl < 5.14
-
On Perls older than 5.14, loading this module (or a package that uses it) at run time -- via
require, a plugin loader or a stringeval-- leaves the subs unprotected, becauseCHECKhas already fired. Load it at compile time instead. - UNIVERSAL namespace pollution
-
The
:Protectedattribute is installed inUNIVERSAL, which is intentional (any package can use it after a singleuse), but it does introduceUNIVERSAL::Protectedinto the global namespace. If another module has already definedUNIVERSAL::Protected, loading Sub::Protected warns that it is being replaced. - Thread safety
-
@_pendingand$BYPASSare unguarded package globals. Subs are normally wrapped before any thread is started, which is safe. CallingSub::Protected->import(...)or loading a package that uses Sub::Protected from more than one thread at once is not supported.$BYPASSis per-thread, as with anyourvariable under ithreads.
DEPENDENCIES
Carp (core), Attribute::Handlers (core since 5.8), B::Hooks::EndOfScope, Readonly, Sub::Identify, Params::Get, Params::Validate::Strict, Return::Set.
SEE ALSO
-
Sister module enforcing abstract (virtual) methods
-
Sister module enforcing strictly private (owner-only) access.
Attribute::Handlers, Carp, Readonly, Params::Get, Params::Validate::Strict, Return::Set.
FORMAL SPECIFICATION
import
The following Z-notation schemas formally specify the state and operations of Sub::Protected. Unicode mathematical symbols are used in this section only.
-- Type abbreviations
Package == seq CHAR -- a non-empty Perl package name string
SubName == seq CHAR -- a Perl identifier string
Proc == seq CHAR -- abstract: a callable code reference
-- Ancestry relation (derived dynamically from @ISA chains)
anc : Package -> P Package
forall p : Package .
anc p = {p} union bigcup { anc r | r in @ISA_of(p) }
-- Protected-access predicate
permitted : Package x Package -> BOOL
forall caller, owner : Package .
permitted(caller, owner) <=> owner in anc(caller)
-- System state
+-Registry-------------------------------------------+
| protected : P (Package x SubName) |
| bypass : BOOL |
| config : { harness_bypass : BOOL } |
+----------------------------------------------------+
-- Initial state
+-InitRegistry---------------------------------------+
| Registry |
|----------------------------------------------------|
| protected = {} |
| bypass = false |
| config = { harness_bypass |-> true } |
+----------------------------------------------------+
-- Wrap: add a sub to the protected registry
+-Wrap-----------------------------------------------+
| Delta-Registry |
| pkg? : Package ; name? : SubName |
|----------------------------------------------------|
| protected' = protected union { (pkg?, name?) } |
| bypass' = bypass |
| config' = config |
+----------------------------------------------------+
-- Bypass predicate
bypass_active(R) <=>
R.bypass or (R.config.harness_bypass and HARNESS_ACTIVE)
-- Access check: no state change
+-CheckAccess----------------------------------------+
| Xi-Registry |
| caller? : Package |
| owner? : Package |
| name? : SubName |
| ok! : BOOL |
|----------------------------------------------------|
| (owner?, name?) in protected |
| ok! <=> bypass_active or permitted(caller?, owner?)|
+----------------------------------------------------+
-- Violation (croak case):
-- not ok! =>
-- croak("name?()" ++ " is a protected method of " ++ owner?
-- ++ " and cannot be called from " ++ caller?)
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.