When bridging Perl with C, the most common stumbling block is the pointer.

In the C language, a pointer is just a memory address. It's a number. But in the context of a program, that number has a strict identity: it might point to a single 32-bit integer, a null-terminated string, an array of 10,000 floats, or a complex Employee struct.

Most FFIs for Perl (including FFI::Platypus) treat pointers essentially as opaque integers. If a C function returns a pointer, Perl receives a scalar holding a memory address like 140732731535360. To do anything useful with that number in traditional FFI, you have to explicitly cast it, use unpack to extract bytes, or wrap it in a generated class that provides accessor methods.

Affix takes a radically different approach: Pointers with Personality.

Because Affix is built on top of the infix JIT and type-introspection engine, an Affix pointer isn't just an opaque memory address. It is a "Pinned" magical scalar that carries its complete Abstract Syntax Tree (AST) type definition directly inside its payload.

Let's look at how this completely changes the ergonomics of writing FFI code in Perl when compared to the industry standard.

The Magic of Native Indexing

Suppose we have a C library that allocates and returns an array of Task structs.

typedef struct {
    int id;
    char name[32];
} Task;

// Returns a pointer to an array of 10 Tasks
Task* get_tasks();

The FFI::Platypus Way

To handle arrays of structs in Platypus, the modern approach is to use the FFI::C companion module to generate wrapper classes.

use FFI::Platypus 2.00;
use FFI::C;

my $ffi = FFI::Platypus->new(api => 2);
$ffi->lib($lib);

# Define the struct and array wrappers
FFI::C->struct(Task =>[
    id   => 'int',
    name => 'string(32)',
]);
FFI::C->array('TaskArray' => 'Task', 10);

$ffi->attach(get_tasks =>[] => 'TaskArray');

my $tasks_ptr = get_tasks();

# Platypus uses generated method calls for accessors
$tasks_ptr->[5]->name("Write Documentation");
$tasks_ptr->[5]->id(404);

say "Task 5 is: " . $tasks_ptr->[5]->name();

While FFI::C makes this readable, every time you call ->name(...) or ->id(...), Perl is performing a method dispatch. It looks up the method in the generated class, sets up a call frame, and executes the underlying memory write. In a tight loop, this overhead adds up.

The Affix Way

In Affix, pointers to arrays and structs are natively traversable Perl Arrays and Hashes. Because the pointer knows it is an Array[Task(), 10], it knows exactly how to calculate the byte offset.

use v5.40;
use Affix qw[:all];

typedef Task => Struct[ id => Int, name => Array[ Char, 32 ] ];
affix $lib, 'get_tasks', [] => Pointer[ Array[ Task(), 10 ] ];

my $tasks_ptr = get_tasks();

# Native Perl indexing directly into C memory!
$tasks_ptr->[5]{name} = "Write Documentation";
$tasks_ptr->[5]{id}   = 404;

say "Task 5 is: " . $tasks_ptr->[5]{name};

Which prints:

Task 5 is: Write Documentation

There are no method calls here. When you request $tasks_ptr->[5], Perl's internal magic fires vtbl_array, calculates the exact offset using the C-level AST, and returns a new magically bound HashRef for that specific Task. When you assign to {name}, the bytes are written instantly and safely into the C string buffer via a C-level VTable hook (svt_set). **Zero method dispatch!Note on buffer sizes: the name field is char[32]. Writing a string longer than the buffer does not overflow — Affix copies up to the field length and NULL-terminates, truncating the remainder (standard C semantics).

Deep Null Safety

In C, attempting to traverse a NULL pointer results in an immediate Segmentation Fault.

The FFI::Platypus Way

If you receive an opaque pointer, or a Record that wraps a pointer, you must manually check if the address is 0 before interacting with it, or risk crashing the Perl interpreter.

my $manager_ptr = $comp->manager(); # Returns an opaque pointer or object

if (defined $manager_ptr && $$manager_ptr != 0) {
    say $manager_ptr->name();
}
else {
    die "Manager is null!";
}

The Affix Way

Because Affix pointers are smart, they handle C's danger zones with Perl's safety rails. If you traverse a struct that contains a NULL pointer, Affix intercepts it.

# $comp is an Affix Company struct pin; set its manager pointer to NULL
$comp->{manager} = undef;

# In C, this would segfault. In Affix:
say $comp->{manager}{name};
# Throws a standard Perl exception: "Can't use an undefined value as a HASH reference"

Memory Lifecycle and C++ Destructors

Memory leaks are the bane of FFI development. If you ask a C library to allocate a struct, you are usually responsible for calling the corresponding free() function.

The FFI::Platypus Way

To automatically clean up a C pointer in Platypus, you typically map it to an object type. This requires you to create a dedicated Perl package and implement a DESTROY block that calls the C function.

package MockObj {
    sub DESTROY {
        my $self = shift;
        # Call the C-level destructor
        $ffi->function(mock_delete => ['opaque'] => 'void')->call($self);
    }
}

$ffi->type('object(MockObj)' => 'mock_obj');
$ffi->attach(mock_new => ['int'] => 'mock_obj');

{
    my $obj = mock_new(42);
} # $obj falls out of scope, Perl calls MockObj::DESTROY, which calls C

This works, but it requires boilerplate packaging and introduces Perl-level method execution during global destruction.

The Affix Way

Affix manages memory ownership via Affix::Memory objects. You can attach native C destructors directly to your Perl variables in a single line, entirely bypassing Perl-level DESTROY blocks. (This is the exact pattern we built in the chapter entitled 'The "Universal Object" Pattern'.)

# Look up the native destructor function address
my $dtor = Affix::find_symbol( $lib, 'mock_delete' );

# Get our native object pointer from a C function
my $raw_ptr = mock_new(42);

# Wrap it, passing the destructor address
my $managed_obj = wrap_owned( address($raw_ptr), address($dtor) );

# When $managed_obj is garbage collected by Perl,
# Affix will automatically execute the C function `mock_delete(raw_ptr)` natively!

This guarantees that native resources are cleaned up deterministically by Perl's reference counting, bridging the gap between C's manual memory management and Perl's automatic lifetime tracking without writing wrapper classes.

Read-Only Enforcement

C developers frequently use the const keyword to mark memory that should not be modified.

The FFI::Platypus Way

Traditional FFIs struggle to enforce const at runtime. If you define an FFI::C struct, the setter methods are generated regardless of whether the underlying C memory was marked const.Note on Const[...]: the Const[Type] qualifier is meaningful in signatures (e.g., Pointer[Const[Char]] for const char*), but inside a typedef'd Struct it currently renders to the plain type — it does not add runtime enforcement. Use Affix::readonly() on the pin instead. Overwriting read-only C memory from Perl will likely result in a silent corruption or a segmentation fault, forcing you to manually override setters to throw exceptions.

The Affix Way

Affix lets you lock a pin at runtime with Affix::readonly($pin, 1)Note on laziness: struct member pins vivify on first access, and Affix::readonly($pin, 1) marks whatever pins exist at that moment. Touch each member you care about (or lock the specific member pin directly) before relying on the whole-struct lock.. The read-only flag is stored inside the Affix_Pin_2_Point_Oh payload, and the C-level VTable hooks (set_sint32, set_float, and friends) check it before touching C memory. So a forbidden write raises a Perl exception instead of corrupting memory.

use v5.40;
use Affix qw[:all];

typedef HardwareInfo => Struct[
    device_id   => Int,
    temperature => Float,
    name        => Array[ Char, 32 ]
];

my $mem  = alloc_owned( sizeof( HardwareInfo() ) );
my $info = cast( $mem, HardwareInfo() );

# Vivify the member pins first (they bind lazily on first touch)
$info->{device_id}   = 0;
$info->{temperature} = 36.6;
$info->{name}        = 'thermal-1';

# Lock the whole struct (recursively marks the member pins)
Affix::readonly( $info, 1 );

$info->{temperature} = 40.0;
# ERROR: Modification of a read-only C value attempted!

And if you ever truly need to bypass this protection (akin to C++'s const_cast), Affix provides a runtime escape hatch:

Affix::readonly( $info->{temperature}, 0 );   # unlock this one field
$info->{temperature} = 40.0;                  # now allowed

Deep Dive: The Dual-Nature of Affix Pointers

Affix pointers adapt to the syntax that fits their type:

Conclusion

Pointers don't have to be opaque, dangerous integers. By leveraging Perl's internal Magic system and coupling it with a robust AST type engine, Affix gives C pointers personality.

They know their size, they know their bounds, they know how to natively clean themselves up, and they respond to standard Perl array and hash syntax with zero-copy performance.

With Affix, the barrier between Perl and C has never been thinner.