You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
typedefstruct {
intid;
charname[32];
} Task;
// Returns a pointer to an array of 10 TasksTask*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!1
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 objectif (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.
packageMockObj {
subDESTROY {
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 addressmy$dtor = Affix::find_symbol( $lib, 'mock_delete' );
# Get our native object pointer from a C functionmy$raw_ptr = mock_new(42);
# Wrap it, passing the destructor addressmy$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.2 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)3. 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:
Primitive pointers: a Pointer[Int] pin reads and writes through $$ptr.
my$ip = get_counter(); # Pointer[Int]$$ip += 1; # increment the C int in place
Struct / array pointers: use Perl's native container syntax ($ptr->{member}, $ptr->[i]) and the type AST tells Affix the byte offsets.
To re-interpret memory: use cast($ptr, NewType). Casting a reference re-interprets the memory it points to (not the address of the reference variable itself):
my$as_uint = cast( $ptr, UInt32 ); # same bytes, new 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.
Footnotes
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). ↩
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. ↩
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. ↩
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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
Employeestruct.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, useunpackto 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
Taskstructs.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.
While
FFI::Cmakes 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.Which prints:
There are no method calls here. When you request
$tasks_ptr->[5], Perl's internal magic firesvtbl_array, calculates the exact offset using the C-level AST, and returns a new magically bound HashRef for that specificTask. 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!1Deep Null Safety
In C, attempting to traverse a
NULLpointer 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
0before interacting with it, or risk crashing the Perl interpreter.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
NULLpointer, Affix intercepts it.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
objecttype. This requires you to create a dedicated Perl package and implement aDESTROYblock that calls the C function.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
DESTROYblocks. (This is the exact pattern we built in the chapter entitled 'The "Universal Object" Pattern'.)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
constkeyword to mark memory that should not be modified.The FFI::Platypus Way
Traditional FFIs struggle to enforce
constat runtime. If you define anFFI::Cstruct, the setter methods are generated regardless of whether the underlying C memory was markedconst.2 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)3. The read-only flag is stored inside theAffix_Pin_2_Point_Ohpayload, 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.And if you ever truly need to bypass this protection (akin to C++'s
const_cast), Affix provides a runtime escape hatch:Deep Dive: The Dual-Nature of Affix Pointers
Affix pointers adapt to the syntax that fits their type:
Pointer[Int]pin reads and writes through$$ptr.$ptr->{member},$ptr->[i]) and the type AST tells Affix the byte offsets.cast($ptr, NewType). Casting a reference re-interprets the memory it points to (not the address of the reference variable itself):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.
Footnotes
Note on buffer sizes: the
namefield ischar[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). ↩Note on
Const[...]: theConst[Type]qualifier is meaningful in signatures (e.g.,Pointer[Const[Char]]forconst char*), but inside atypedef'dStructit currently renders to the plain type — it does not add runtime enforcement. UseAffix::readonly()on the pin instead. ↩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. ↩All reactions