NAME
Sub::Private - Private subroutines and methods
VERSION
Version 0.06
SYNOPSIS
package Foo;
use Sub::Private;
sub foo { return 42 }
sub bar :Private {
return foo() + 1;
}
sub baz {
return bar() + 1;
}
DESCRIPTION
Enforces strictly private access on subroutines. A subroutine decorated with :Private (or named in use Sub::Private qw(...) when in enforce mode) may only be called from within its defining package. Subclasses do not inherit access: private means this package only.
Two enforcement modes
namespacemode (default, backward-compatible)-
Removes the subroutine from the package symbol table using namespace::clean. Direct (non-method) function calls compiled before cleanup still work because Perl optimises them to direct opcode references. OO method dispatch (
$self-name>) does not work for private subs in this mode because method lookup uses the symbol table at runtime.This is the default mode and is backward-compatible with all existing code.
enforcemode (OO-safe, opt-in)-
Replaces the subroutine with a wrapper closure that checks
callerat call time and either delegates (owner package) or croaks (anyone else). Works correctly with OO dispatch ($self-_helper>).Enable before declaring your first private sub:
BEGIN { $Sub::Private::config{mode} = 'enforce' } package MyClass; use Sub::Private; sub _helper :Private { ... }
Behaviour of wrapped subs (enforce mode)
The wrapper hands over with
goto &sub, so it does not appear on the call stack:callerinside a private sub sees its real caller.Arguments,
@_aliasing and calling context (list, scalar or void) are passed through unchanged.$_is left untouched, whether the call is allowed or blocked.A blocked call croaks; it does not fall through to
AUTOLOAD.Works with Moo and Moose classes; use the declarative form after
use Moooruse Moose.
Loading at run time
Packages that use Sub::Private may be loaded at run time (require, a plugin loader, a string eval), and Sub::Private itself may be loaded for the first time that way. The private subs are then wrapped (or removed, in namespace mode) when the enclosing scope, normally the rest of the file, has been compiled.
Bypass for testing
Either condition alone (OR logic) disables all access checks in enforce mode:
$Sub::Private::BYPASSset to a true value. Uselocalin tests.$ENV{HARNESS_ACTIVE}set (the convention used by Test::Harness/prove).
$Sub::Private::BYPASS is the recommended form for new test code. The HARNESS_ACTIVE bypass can be disabled:
$Sub::Private::config{harness_bypass} = 0;
Configuration
$Sub::Private::config{mode} -- 'namespace' (default) or 'enforce'
$Sub::Private::config{harness_bypass} -- 1 (default); set to 0 to test enforcement
Error message format (enforce mode)
bar() is a private subroutine of Foo and cannot be called from Bar
PUBLIC INTERFACE
import
use Sub::Private; # attribute form -- no arguments
use Sub::Private qw(_a _b _c); # declarative form (enforce mode only)
Purpose
Called automatically by use Sub::Private.
With no arguments: makes the :Private attribute globally available via UNIVERSAL. No other action is taken.
With one or more sub names: registers those named subs in the calling package for access-enforcement wrapping at CHECK time. If CHECK has already fired (for example the package is loaded with require at run time), the subs are wrapped as soon as the enclosing scope (normally the rest of the file) has been compiled, so the use line can still come before the subs it names. A direct Sub::Private->import(...) call at run time wraps immediately. Requires $Sub::Private::config{mode} to equal 'enforce'; croaks otherwise.
Arguments
@subs(optional)-
Zero or more Perl sub names. Each must be a defined, non-reference scalar matching
/\A[_a-zA-Z]\w*\z/.undef, references, empty strings, and names starting with a digit or containing hyphens are all rejected.
Returns
The class name ('Sub::Private') as a plain string in all cases.
Side effects
Pre-CHECK: appends
[$owner_pkg, $sub_name]pairs to the internal@_pendinglist.Post-CHECK, during compilation: installs wrapper closures in the calling package's stash when the enclosing scope has been compiled.
Post-CHECK, at run time: installs wrapper closures directly in the calling package's stash.
Example
BEGIN { $Sub::Private::config{mode} = 'enforce' }
package MyClass;
use Sub::Private qw(_helper _init);
sub new { bless {}, shift }
sub _helper { ... } # wrapped at CHECK time
sub _init { ... } # wrapped at CHECK time
sub run { my $s = shift; $s->_helper; $s->_init }
API SPECIFICATION
Input
# No-argument form: always valid.
Sub::Private->import();
# Declarative form (enforce mode only):
{
subs => {
type => 'array',
optional => 1,
element => {
type => 'string',
regex => qr/\A[_a-zA-Z]\w*\z/,
},
}
}
Output
{ type => 'string' } # returns the class name 'Sub::Private'
MESSAGES
Message Meaning / Action
--------------------------------------------------- -----------------------------------------------
"Sub::Private->import: declarative form requires use Sub::Private qw(...) was called while
mode => 'enforce'" $config{mode} is not 'enforce'. Set
$config{mode} = 'enforce' in a BEGIN block
before "use Sub::Private".
"Sub::Private->import: 'NAME' is not a valid The sub name failed the identifier regex.
Perl identifier" Check for typos, hyphens, leading digits,
undef, or reference values in the import list.
"Sub::Private: PKG::NAME is not defined" The named sub was not found in the stash at
wrap time. Define the sub before import()
runs, before CHECK fires, or (after CHECK)
before the end of the enclosing scope.
After CHECK, the location reported is the
"use Sub::Private" line.
PUBLIC VARIABLES
$BYPASS
Set to a true value to disable all access checks (enforce mode only). Use local in tests; see "Bypass for testing".
%config
Module-level configuration hash. Supported keys:
mode-
'namespace'(default) or'enforce'. Must be set in aBEGINblock beforeuse Sub::Privateto take effect atCHECKtime. harness_bypass-
When true (default), access checks are skipped whenever
$ENV{HARNESS_ACTIVE}is set. Set to 0 to test enforcement underprove.
KNOWN LIMITATIONS
namespacemode: OO dispatch fails for private subs-
$self-_helper> from within the owner package fails because method dispatch uses the symbol table at runtime, which no longer contains the entry. Useenforcemode for OO classes. enforcemode: runtime-only-
Checks are runtime only; there is no compile-time enforcement.
enforcemode: raw coderef bypass-
A raw code reference obtained before wrapping (via
can()or\&Foo::_helper) bypasses the check. The attribute form makes this hard because wrapping happens at CHECK time. When the package is loaded after CHECK, wrapping happens at the end of the enclosing scope, so aBEGINblock earlier in the same file could still take a reference to the unwrapped sub. enforcemode:can()leaks private method existence-
In
enforcemode the original sub is replaced by a wrapper closure, so->can('_helper')returns the wrapper (a true value) even to callers outside the owner package. Innamespacemode the stash entry is deleted entirely, so->cancorrectly returnsundef. A future release may inject a caller-awarecan()override into each class that usesenforcemode, returning the coderef only when the caller is the owner package andundeffor everyone else. - UNIVERSAL namespace pollution
-
The
:Privateattribute is installed inUNIVERSAL, which is intentional (any package can use it after a singleuse), but it does introduceUNIVERSAL::Privateinto the global namespace.
DEPENDENCIES
Perl 5.14 or later, Carp (core), Attribute::Handlers (core), B::Hooks::EndOfScope, Readonly, Params::Validate::Strict, Return::Set, namespace::clean, Sub::Identify.
SEE ALSO
-
Sister module enforcing protected (owner + subclass) rather than strictly private access
-
Sister module enforcing abstract (virtual) methods
FORMAL SPECIFICATION
The following Z-notation schemas formally specify the CheckAccess operation and import.
CheckAccess
-- Type abbreviations
Package == seq CHAR -- a non-empty Perl package name string
SubName == seq CHAR -- a Perl identifier string
-- Private-access predicate (strictly owner only -- no isa expansion)
permitted : Package x Package -> BOOL
forall caller, owner : Package .
permitted(caller, owner) <=> caller = owner
-- System state
+-Registry-------------------------------------------+
| private : P (Package x SubName) |
| bypass : BOOL |
| config : { mode : seq CHAR, |
| harness_bypass : BOOL } |
+----------------------------------------------------+
-- Initial state
+-InitRegistry---------------------------------------+
| Registry |
|----------------------------------------------------|
| private = {} |
| bypass = false |
| config = { mode |-> 'namespace', |
| harness_bypass |-> true } |
+----------------------------------------------------+
-- 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 private |
| ok! <=> bypass_active or permitted(caller?, owner?)|
+----------------------------------------------------+
-- Violation (croak case):
-- not ok! =>
-- croak("name?()" ++ " is a private subroutine of " ++ owner?
-- ++ " and cannot be called from " ++ caller?)
-- Key difference from Sub::Protected:
-- permitted(caller, owner) <=> caller = owner (identity only)
-- vs Sub::Protected:
-- permitted(caller, owner) <=> owner in anc(caller) (ISA chain)
import
-- Valid identifier predicate
valid_id : SubName -> BOOL
valid_id(n) <=> n =~ /\A[_a-zA-Z]\w*\z/
-- Compile phase: CHECK has run, and code is still being compiled
post_check : BOOL
compiling : BOOL -- true while $^S is undefined
-- Pre-condition (declarative form)
+-ImportPre-----------------------------------------+
| config.mode = 'enforce' |
| forall n in subs . valid_id(n) |
+---------------------------------------------------+
-- Post-condition (before CHECK): queue for the CHECK block
+-ImportPost_PreCheck-------------------------------+
| not post_check |
|---------------------------------------------------|
| pending' = pending ++ < (caller, n) | n in subs > |
+---------------------------------------------------+
-- Post-condition (after CHECK, at run time): wrap now
+-ImportPost_RunTime--------------------------------+
| post_check and not compiling |
| forall n in subs . defined(caller, n) |
|---------------------------------------------------|
| forall n in subs . |
| stash'(caller, n) = wrapper(caller, n) |
+---------------------------------------------------+
-- Post-condition (after CHECK, while compiling): wrap at the
-- end of the enclosing scope S
+-ImportPost_Deferred-------------------------------+
| post_check and compiling |
|---------------------------------------------------|
| at end_of_scope(S) : |
| (forall n in subs . defined(caller, n)) => |
| forall n in subs . |
| stash'(caller, n) = wrapper(caller, n) |
| (exists n in subs . not defined(caller, n)) => |
| croak at the "use" line |
+---------------------------------------------------+
-- In every case, a sub that is not defined when wrapping is
-- attempted croaks:
-- "Sub::Private: " ++ caller ++ "::" ++ n ++ " is not defined"
AUTHOR
Original Author: Peter Makholm, <peter at makholm.net>
Current maintainer: Nigel Horne, <njh at nigelhorne.com>
BUGS
Please report any bugs or feature requests to bug-sub-private at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Sub-Private.
SUPPORT
perldoc Sub::Private
RT: CPAN's request tracker
MetaCPAN
COPYRIGHT & LICENSE
Copyright 2009 Peter Makholm, all rights reserved. Portions copyright 2024-2026 Nigel Horne.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.