NAME

Legba - global reactive state slots with optional watchers

SYNOPSIS

# Define and use slots
package Config;
use Legba qw(app_name debug);

app_name("MyApp");
debug(1);

# Access from another package (same underlying storage)
package Service;
use Legba qw(app_name);

print app_name();   # "MyApp"
app_name("Changed");

# Watchers (reactive)
Legba::watch('app_name', sub {
    my ($name, $value) = @_;
    print "app_name changed to: $value\n";
});

Legba::unwatch('app_name');   # Remove all watchers

DESCRIPTION

Legba (named after Papa Legba, the Vodou gatekeeper of crossroads) provides fast, globally shared named storage slots. Slots are shared across all packages — importing the same slot name in different packages gives access to the same underlying value.

Key features:

  • Fast - Custom ops with compile-time optimization

  • Global - Slots are shared across packages by name

  • Reactive - Optional watchers fire on value changes

  • Lazy watchers - No overhead unless you use watch()

  • Access control - Optional per-slot lock and freeze

COMPILE-TIME OPTIMIZATION

When you call any Legba::* function with a constant string name for a slot that exists at compile time (created via use Legba qw(...)), the call is optimized at compile time to a custom op or constant.

use Legba qw(counter);            # Creates slot at compile time

Legba::get('counter');            # Optimized to custom op (185% faster)
Legba::set('counter', 42);        # Optimized to custom op (283% faster)
my $idx = Legba::index('counter');# Constant-folded (no runtime code!)
Legba::watch('counter', \&cb);    # Optimized to custom op
Legba::unwatch('counter');        # Optimized to custom op
Legba::clear('counter');          # Optimized to custom op

Variable names are NOT optimized and use the XS fallback:

my $name = 'counter';
Legba::get($name);                # XS function call (slower)

Optimization Requirements

1. The slot name must be a literal string constant
2. The slot must exist at compile time (use use Legba qw(...))
3. Slots created at runtime with Legba::add() cannot be optimized

FUNCTIONS

import

use Legba qw(foo bar baz);

Imports slot accessors into the calling package. Each accessor is both a getter and setter:

foo();       # get
foo(42);     # set and returns value

Legba::add

Legba::add('name');
Legba::add('name1', 'name2', 'name3');

Create slots without importing accessors into the current package. Faster than use Legba qw(...) when you only need get/set access via the functional API. Idempotent — adding an existing slot is a no-op.

Legba::index

my $idx = Legba::index('name');

Get the numeric index of a slot. Use with get_by_idx/set_by_idx for maximum performance when you need repeated access.

Compile-time optimization: When called with a constant string, the index is computed at compile time and the call is replaced with a constant — no runtime code at all.

Legba::get

my $val = Legba::get('name');

Get a slot value by name (without importing an accessor).

Compile-time optimization: When called with a constant string for a slot that exists at compile time, this is optimized to a custom op and runs as fast as an accessor. Also available as Legba::_get.

Legba::set

Legba::set('name', $value);

Set a slot value by name. Creates the slot if it does not yet exist. Returns the stored value.

Compile-time optimization: Like Legba::get, optimized at compile time when called with a constant string. Also available as Legba::_set.

Legba::get_by_idx

my $idx = Legba::index('name');
my $val = Legba::get_by_idx($idx);

Get a slot value by numeric index. Faster than name-based lookup — a single array dereference with no hash lookup.

Best use case: When the slot name is a runtime variable and you need repeated access, cache the index once and use get_by_idx.

Legba::set_by_idx

Legba::set_by_idx($idx, $value);

Set a slot value by numeric index. Faster than name-based lookup. Respects lock/freeze and fires watchers. Returns the stored value.

Legba::watch

Legba::watch('name', sub { my ($name, $value) = @_; ... });

Register a callback that fires whenever the slot value is set (including setting to the same value). The callback receives the slot name and new value.

Compile-time optimization: When called with a constant string, optimized to a custom op.

Legba::unwatch

Legba::unwatch('name');             # Remove all watchers
Legba::unwatch('name', $coderef);   # Remove specific watcher

Compile-time optimization: When called with a constant string, optimized to a custom op.

Legba::clear

Legba::clear('name');
Legba::clear('name1', 'name2');

Reset slot value(s) to undef and remove all associated watchers. The slot still exists (can be set again), but its value and watchers are cleared. Silently skips locked or frozen slots.

Compile-time optimization: When called with a single constant string, optimized to a custom op.

Legba::clear_by_idx

Legba::clear_by_idx($idx);
Legba::clear_by_idx($idx1, $idx2);

Reset slot value(s) to undef and remove watchers by numeric index.

Legba::slots

my @names = Legba::slots();

Returns a list of all defined slot names. Also available as Legba::_keys.

Legba::exists

if (Legba::exists('config')) { ... }

Check if a slot with the given name has been defined. Returns true if the slot exists, false otherwise. Also available as Legba::_exists.

ACCESS CONTROL

Legba::_lock

Legba::_lock('name');

Reversibly prevents the slot from being set. Reads still work. Croaks if the slot does not exist or is frozen.

Legba::_unlock

Legba::_unlock('name');

Removes a lock placed by _lock. Croaks if the slot is frozen.

Legba::_freeze

Legba::_freeze('name');

Permanently prevents the slot from being set. Cannot be reversed. Frozen slots cannot be locked or unlocked.

Legba::_is_locked

if (Legba::_is_locked('name')) { ... }

Returns true if the slot is currently locked.

Legba::_is_frozen

if (Legba::_is_frozen('name')) { ... }

Returns true if the slot is frozen.

ADVANCED API

Legba::_delete

Legba::_delete('name');

Clears the slot value to undef without removing the slot from the index. Respects lock and freeze — croaks if the slot is locked or frozen.

Legba::_clear

Legba::_clear();

Clears all slot values to undef (skips locked and frozen slots). Preserves the slot index and any active watchers.

Legba::_install_accessor

Legba::_install_accessor($pkg, $slot_name);

Manually install an accessor function for $slot_name into package $pkg. Creates the slot if it does not already exist.

Legba::_slot_ptr

my $ptr = Legba::_slot_ptr('name');

Returns the raw SV* pointer (as a UV) for the slot's dedicated SV. The pointer is stable across value changes and registry resizes — useful for embedding in custom C ops.

Legba::_registry

my $hashref = Legba::_registry();

Returns a reference to the internal slot_name = index> hash. Intended for introspection and debugging.

Legba::_make_get_op

my $op_ptr = Legba::_make_get_op('name');

Allocates a getter OP* for the named slot and returns its address as a UV. Useful for injecting into an optree from another XS module.

Legba::_make_set_op

my $op_ptr = Legba::_make_set_op('name');

Allocates a setter OP* for the named slot and returns its address as a UV.

THREAD SAFETY

For thread-safe data sharing, store threads::shared variables in slots:

use threads;
use threads::shared;
use Legba qw(config);

my %shared :shared;
$shared{counter} = 0;
config(\%shared);

my @threads = map {
    threads->create(sub {
        my $cfg = config();
        lock(%$cfg);
        $cfg->{counter}++;
    });
} 1..10;

$_->join for @threads;
print config()->{counter};   # 10

The slot provides the global accessor; threads::shared provides the thread-safe storage.

FORK BEHAVIOR

After fork(), child processes get a copy of slot values (copy-on-write). Changes in child processes do not affect the parent, and vice versa.

BENCHMARKS

use Benchmark qw(timethese cmpthese);
use Legba qw(bench_slot);

my $r = timethese(1_000_000, {
    accessor_get => sub { bench_slot()         },
    accessor_set => sub { bench_slot(42)       },
    get_const    => sub { Legba::get('bench_slot') },
    set_const    => sub { Legba::set('bench_slot', 42) },
});
cmpthese($r);

Typical results (threaded perl, Apple Silicon):

Accessor getter:  ~55M ops/sec
Accessor setter: ~142M ops/sec
get constant:    ~55M ops/sec  (custom op — same as accessor)
set constant:   ~142M ops/sec  (custom op — same as accessor)

AUTHOR

LNATION <email@lnation.org>

LICENSE

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.