view release on metacpan or search on metacpan
- Added support for native Perl array indexing on pointers and arrays. You can now use `$ptr->[$i]` to read or write memory at an offset without manual casting.
- New `Affix::Pointer` objects for structs and unions now allow direct field access like `$ptr->{field}` without explicit casting to `LiveStruct`.
- Recursive Liveness: Unified access and `LiveStruct` now work recursively. Accessing a nested struct member returns a live view or pointer tied to the original memory block.
- All pointers returned by `malloc`, `calloc`, `cast`, etc., are now blessed into the `Affix::Pointer` class, which provides several new methods:
- `address()`: Returns the virtual memory address.
- `type()`: Returns the L<infix> signature of the pointer.
- `element_type()`: Returns the signature of the pointed-to elements.
- `count()`: Returns the element count for Arrays, or byte size for Void pointers.
- `size()`: Returns the total allocated size for managed pointers.
- `cast($type)`: Reinterprets the pointer.
- `attach_destructor($destructor, [$lib])`: Attaches a custom C cleanup routine to the pointer.
- Added `attach_destructor( $pin, $destructor, [$lib] )` to allow attaching custom C cleanup routines to managed pointers.
- Improved `VarArgs` support to automatically promote Perl strings to `char*`.
- Experimental Zero-Copy 'Live' Aggregates:
- `LiveStruct`: A new helper to return zero-copy, live views of C structs. Modifications to the returned blessed hash reflect immediately in C memory.
- `LiveArray`: A new helper to return live `Affix::Pointer` objects instead of deeply copied array references.
- Implemented the `TIEHASH` interface for `Affix::Live` so perl can treat them as standard Perl hashes (`keys %$live`, `each %$live`, etc.).
- Fully implemented marshalling for `Int128` and `UInt128` (sint128/uint128) primitive types.
- Added `Affix::Wrap->generate( $lib, $pkg, $file )` for static binding generation. This emits standalone Perl modules that depend only on `Affix`, eliminating the need for `Clang` or header files at runtime.
- Recursive macro resolution support in `Affix::Wrap` for bitwise OR expressions like `(FLAG_A | FLAG_B)`.
- Support for passing string names of enum constants directly to functions.
- Added `params()` method to `Affix::Type::Callback` to allow inspecting and modifying callback parameters.
- `own($ptr, 1)`: Perl takes ownership. `free()` will be called automatically when `$ptr` is garbage collected.
- `own($ptr, 0)`: Perl releases ownership. You (or the C library) are now responsible for freeing the memory.
```perl
# Take ownership of a pointer returned from C
my $c_string = get_string_from_c();
own($c_string, 1);
```
### `attach_destructor( $pin, $func_ptr, [$lib] )`
Attaches a custom C function to be called when the Pin is destroyed. This is incredibly useful for C libraries that
require specific cleanup routines (e.g., `SDL_DestroyWindow`, `sqlite3_free`).
```perl
# Find the address of the library's custom free function
my $free_func = find_symbol($my_lib, 'custom_free');
# When $ptr goes out of scope, Affix will call custom_free($ptr)
attach_destructor($ptr, $free_func, $my_lib);
```
## Type Casting
### `cast( $ptr, $type )`
Reinterprets a memory address as a new type. The behavior depends on the requested `$type`:
- **Casting to a Value (Primitives/Strings):** Reads the memory immediately and returns a Perl scalar copy.
- **Casting to a Reference (Pointers/Live Views):** Returns a **new unmanaged Pin** that aliases the same memory address, allowing you to interact with it using the new type's rules.
- `affix( ... )` - Binding new functions.
- `typedef( ... )` - Registering new types.
## 2. Callbacks
When passing a Perl subroutine as a `Callback`, avoid performing complex Perl operations like loading modules or
defining subs inside callbacks triggered on a foreign thread. Such callbacks should remain simple: process data, update
a shared variable, and return.
If the library executes the callback from a background thread (e.g., window managers, audio callbacks), Affix attempts
to attach a temporary Perl context to that thread. This should be sufficient but Perl is gonna be Perl.
# RECIPES & EXAMPLES
See [The Affix Cookbook](https://github.com/sanko/Affix.pm/discussions/categories/recipes) for comprehensive guides to
using Affix.
## Linked List Implementation
```perl
# C equivalent:
infix/src/core/type_registry.c view on Meta::CPAN
(size_t)(defs_found[i].def_body_start - definitions) + relative_pos);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
infix_arena_destroy(parser_arena);
goto cleanup;
}
}
// Prepare the new definition.
infix_type * new_def = nullptr;
if (!type_to_alias->is_arena_allocated) {
// This is a static type (e.g., primitive). We MUST create a mutable
// copy in the registry's arena before we can attach a name to it.
// This prevents corrupting the global static singletons.
new_def = infix_arena_alloc(registry->arena, sizeof(infix_type), _Alignof(infix_type));
if (new_def) {
*new_def = *type_to_alias;
new_def->is_arena_allocated = true;
new_def->arena = registry->arena;
}
}
else // Dynamic type: deep copy.
new_def = _copy_type_graph_to_arena(registry->arena, type_to_alias);
infix/src/core/type_registry.c view on Meta::CPAN
// Restore the self-reference if the copy logic pointed the arena elsewhere.
entry->type->arena = registry->arena;
// The new definition is complete, so ensure the flag is cleared.
entry->type->is_incomplete = false;
}
else {
entry->type = new_def;
entry->type->is_incomplete = false;
}
// Ensure the name is attached and the flag is cleared.
entry->type->name = entry->name;
entry->is_forward_declaration = false;
}
infix_arena_destroy(parser_arena);
// Pass 3: Resolve and layout all the newly defined types.
for (size_t i = 0; i < num_defs_found; ++i) {
_infix_registry_entry_t * entry = defs_found[i].entry;
if (entry->type) {
lib/Affix.c view on Meta::CPAN
// Ensure we point to the content type, not Pointer[Content]
pin->type = _unwrap_pin_type(type);
pin->managed = false;
SV * obj_data = newSV(0);
sv_setiv(obj_data, PTR2IV(pin));
// Create the Reference
SV * rv = sv_2mortal(newRV_noinc(obj_data));
// Bless into Affix::Pointer BEFORE attaching magic to avoid triggering 'set' during blessing
(void)sv_bless(rv, gv_stashpv("Affix::Pointer", GV_ADD));
MAGIC * mg = sv_magicext(obj_data, nullptr, PERL_MAGIC_ext, &Affix_pin_vtbl, nullptr, 0);
mg->mg_ptr = (char *)pin;
// Update the target SV
sv_setsv(sv, rv);
}
static void pull_sv(pTHX_ Affix * affix, SV * sv, const infix_type * type, void * ptr) {
lib/Affix.c view on Meta::CPAN
if (infix_type_create_pointer_to(pin->type_arena, &void_ptr_type, infix_type_create_void()) != INFIX_SUCCESS) {
safefree(pin);
infix_arena_destroy(pin->type_arena);
croak("Internal error: Failed to create pointer type for pin");
}
pin->type = void_ptr_type;
SV * obj_data = newSV(0);
sv_setiv(obj_data, PTR2IV(pin));
SV * rv = newRV_inc(obj_data);
// Bless into Affix::Pointer BEFORE attaching magic to avoid triggering 'set' during blessing
(void)sv_bless(rv, gv_stashpv("Affix::Pointer", GV_ADD));
MAGIC * mg = sv_magicext(obj_data, nullptr, PERL_MAGIC_ext, &Affix_pin_vtbl, nullptr, 0);
mg->mg_ptr = (char *)pin;
ST(0) = sv_2mortal(rv);
XSRETURN(1);
}
XSRETURN_UNDEF;
}
lib/Affix.c view on Meta::CPAN
bool new_val = SvTRUE(ST(1));
if (pin->managed != new_val)
pin->managed = new_val;
}
// Return current state as fast booleans (PL_sv_yes/no are essentially singletons)
ST(0) = pin->managed ? &PL_sv_yes : &PL_sv_no;
XSRETURN(1);
}
XS_INTERNAL(Affix_attach_destructor) {
dXSARGS;
if (items < 2)
croak_xs_usage(cv, "pin, destructor_ptr, [lib_obj]");
Affix_Pin * pin = _get_pin_from_sv(aTHX_ ST(0));
if (!pin) {
warn("First argument to attach_destructor must be a pinned pointer");
XSRETURN_UNDEF;
}
void * destructor_ptr = nullptr;
if (SvIOK(ST(1)))
destructor_ptr = INT2PTR(void *, SvUV(ST(1)));
else {
Affix_Pin * dpin = _get_pin_from_sv(aTHX_ ST(1));
if (dpin)
destructor_ptr = dpin->pointer;
lib/Affix.c view on Meta::CPAN
XSUB_EXPORT(strnlen, "$$", "memory");
XSUB_EXPORT(is_null, "$", "memory");
// Pin internals (for Affix::Pointer)
(void)newXSproto_portable("Affix::_pin_type", Affix_pin_type, __FILE__, "$");
(void)newXSproto_portable("Affix::_pin_element_type", Affix_pin_element_type, __FILE__, "$");
(void)newXSproto_portable("Affix::_pin_count", Affix_pin_count, __FILE__, "$");
(void)newXSproto_portable("Affix::_pin_size", Affix_pin_size, __FILE__, "$");
(void)newXSproto_portable("Affix::_pin_get_at", Affix_pin_get_at, __FILE__, "$$");
(void)newXSproto_portable("Affix::_pin_set_at", Affix_pin_set_at, __FILE__, "$$$");
(void)newXSproto_portable("Affix::_attach_destructor", Affix_attach_destructor, __FILE__, "$$;$");
}
XSUB_EXPORT(coerce, "$$", "core");
XSUB_EXPORT(errno, "", "core");
(void)newXSproto_portable("Affix::set_destruct_level", Affix_set_destruct_level, __FILE__, "$");
#undef XSUB_EXPORT
Perl_xs_boot_epilog(aTHX_ ax);
lib/Affix.h view on Meta::CPAN
OP_RET_CUSTOM // Complex types (structs, arrays)
} Affix_Opcode;
/// A single step in the pre-compiled execution plan.
struct Affix_Plan_Step {
Affix_Step_Executor executor; // Function pointer to the executor for this step.
Affix_Opcode opcode; // The instruction for the VM
Affix_Step_Data data; // Pre-calculated data needed by the executor.
};
/// Represents a forward FFI call (a Perl sub that calls a C function).
/// This struct holds the pre-compiled execution plan and is attached to the generated XS subroutine.
struct Affix {
infix_forward_t * infix; ///< Handle to the infix trampoline and type info.
infix_arena_t * args_arena; ///< Fast memory allocator for arguments during a call.
infix_arena_t * ret_arena; ///< Fast memory allocator for return value during a call.
infix_cif_func cif; ///< A direct function pointer to the JIT-compiled trampoline code.
infix_library_t * lib_handle; ///< If affix() loaded a library itself, stores the handle for cleanup.
SV * return_sv; ///< Pre-allocated, reusable SV to hold the return value.
Affix_Plan_Step * plan; ///< The linear array of operations (the "execution plan").
size_t plan_length; ///< The total number of steps in the plan.
size_t num_args; ///< Cached number of arguments for faster access.
lib/Affix.h view on Meta::CPAN
SV * destructor_lib_sv; ///< Perl object (Affix::Lib) to keep alive for the destructor.
SV * owner_sv; ///< Perl object that owns the memory, kept alive by this pin.
size_t bit_offset; ///< Bit offset (for bitfields)
size_t bit_width; ///< Bit width (for bitfields, 0 = not a bitfield)
} Affix_Pin;
/// Holds the necessary data for a callback, specifically the Perl subroutine to call.
typedef struct {
SV * coderef_rv; ///< A reference (RV) to the Perl coderef. We hold this to keep it alive.
dTHXfield(perl) ///< The thread context in which the callback was created.
} Affix_Callback_Data;
/// Internal struct holding the C resources that are magically attached
/// to a user's coderef (CV*) when it is first used as a callback.
typedef struct {
infix_reverse_t * reverse_ctx; ///< Handle to the infix reverse-call trampoline.
} Implicit_Callback_Magic;
/// An entry in the thread-local library registry hash.
typedef struct {
infix_library_t * lib; ///< The handle to the opened library.
UV ref_count; ///< Reference count. The library is closed only when this reaches 0.
} LibRegistryEntry;
lib/Affix.pm view on Meta::CPAN
Short UShort
Int UInt
Long ULong
LongLong ULongLong
Float16 Float Double LongDouble
Int8 SInt8 UInt8 Int16 SInt16 UInt16 Int32 SInt32 UInt32 Int64 SInt64 UInt64 Int128 SInt128 UInt128
Float32 Float64
Size_t SSize_t
String WString
Pointer Array Live Struct Union Enum Callback CodeRef Complex Vector Live
ThisCall attach_destructor
Packed VarArgs
SV
File PerlIO
StringList
Buffer SockAddr
M256 M256d M512 M512d M512i
]
];
{
my %seen;
lib/Affix.pm view on Meta::CPAN
my %seen;
push @{ $EXPORT_TAGS{all} }, grep { !$seen{$_}++ } @{ $EXPORT_TAGS{$_} } for keys %EXPORT_TAGS;
}
#
@EXPORT = sort @{ $EXPORT_TAGS{default} }; # XXX: Don't do this...
@EXPORT_OK = sort @{ $EXPORT_TAGS{all} };
#
sub libm() { CORE::state $m //= find_library('m'); $m }
sub libc() { CORE::state $c //= find_library('c'); $c }
sub attach_destructor ( $pin, $destructor, $lib //= () ) {
Affix::_attach_destructor( $pin, $destructor, $lib );
}
#
our $OS = $^O;
my $is_win = $OS eq 'MSWin32';
my $is_mac = $OS eq 'darwin';
my $is_bsd = $OS =~ /bsd/;
my $is_sun = $OS =~ /(solaris|sunos)/;
#
sub locate_libs ( $lib, $version //= () ) {
$lib =~ s[^lib][];
lib/Affix.pm view on Meta::CPAN
use v5.40;
use overload '""' => \&address, '@{}' => \&_as_array, '%{}' => \&_as_hash, fallback => 1;
sub address { Affix::address(shift) }
sub type { Affix::_pin_type(shift) }
sub element_type { Affix::_pin_element_type(shift) }
sub size { Affix::_pin_size(shift) }
sub count { Affix::_pin_count(shift) }
sub cast { Affix::cast( shift, shift ) }
sub _as_array { my $self = shift; my @proxy; tie @proxy, 'Affix::Pointer::TiedArray', $self; return \@proxy; }
sub _as_hash { my $self = shift; my %proxy; tie %proxy, 'Affix::Pointer::TiedHash', $self; return \%proxy; }
sub attach_destructor { my ( $self, $destructor, $lib ) = @_; Affix::attach_destructor( $self, $destructor, $lib ); }
}
package #
Affix::Pointer::TiedHash {
use v5.40;
sub TIEHASH { my ( $class, $ptr ) = @_; my $obj = $ptr->cast( "+" . $ptr->element_type ); return $obj; }
sub FETCH { my ( $self, $key ) = @_; return $self->{$key}; }
sub STORE { my ( $self, $key, $val ) = @_; $self->{$key} = $val; }
sub EXISTS { my ( $self, $key ) = @_; return exists $self->{$key}; }
sub FIRSTKEY { my ($self) = @_; keys %$self; return each %$self; }
sub NEXTKEY { my ( $self, $last ) = @_; return each %$self; }
lib/Affix.pod view on Meta::CPAN
=item * C<own($ptr, 1)>: Perl takes ownership. C<free()> will be called automatically when C<$ptr> is garbage collected.
=item * C<own($ptr, 0)>: Perl releases ownership. You (or the C library) are now responsible for freeing the memory.
=back
# Take ownership of a pointer returned from C
my $c_string = get_string_from_c();
own($c_string, 1);
=head3 C<attach_destructor( $pin, $func_ptr, [$lib] )>
Attaches a custom C function to be called when the Pin is destroyed. This is incredibly useful for C libraries that
require specific cleanup routines (e.g., C<SDL_DestroyWindow>, C<sqlite3_free>).
# Find the address of the library's custom free function
my $free_func = find_symbol($my_lib, 'custom_free');
# When $ptr goes out of scope, Affix will call custom_free($ptr)
attach_destructor($ptr, $free_func, $my_lib);
=head2 Type Casting
=head3 C<cast( $ptr, $type )>
Reinterprets a memory address as a new type. The behavior depends on the requested C<$type>:
=over
=item * B<Casting to a Value (Primitives/Strings):> Reads the memory immediately and returns a Perl scalar copy.
lib/Affix.pod view on Meta::CPAN
=back
=head2 2. Callbacks
When passing a Perl subroutine as a C<Callback>, avoid performing complex Perl operations like loading modules or
defining subs inside callbacks triggered on a foreign thread. Such callbacks should remain simple: process data, update
a shared variable, and return.
If the library executes the callback from a background thread (e.g., window managers, audio callbacks), Affix attempts
to attach a temporary Perl context to that thread. This should be sufficient but Perl is gonna be Perl.
=head1 RECIPES & EXAMPLES
See L<The Affix Cookbook|https://github.com/sanko/Affix.pm/discussions/categories/recipes> for comprehensive guides to
using Affix.
=head2 Linked List Implementation
# C equivalent:
# typedef struct Node {
t/005_varargs.t view on Meta::CPAN
my $p2 = { x => 3, y => 4 }; # Sum = 7
# Coerced Struct
# Affix should generate JIT for: (*char; {x:int,y:int}, {x:int,y:int})->int
is $fn->( "PP", coerce( Point__(), $p1 ), coerce( Point__(), $p2 ) ), 10, 'Dynamically generated signature for coerced structs';
};
# Test Safety / Constraints
subtest 'Safety' => sub {
# coerce() modifies the SV (attaches magic).
# It cannot function on read-only constants.
like dies {
$fn->( "i", coerce( Int, 10 ) );
}, qr/read-only/, 'coerce(Int, 10) correctly dies on read-only value';
# Correct usage with variable
my $v = 100;
is $fn->( "i", coerce( Int, $v ) ), 100, 'coerce(Int, $var) works';
};
subtest coercion => sub {
typedef Point_ => Struct [ x => Int, y => Int ];
# Without coerce(), Affix wouldn't know to pass this as a Point_ struct by value.
my $pt = { x => 10, y => 20 };
# coerce() attaches magic to $pt telling Affix to treat it as a @Point.
# The JIT generates a trampoline expecting a struct {int,int} on the stack/registers.
# 10 + 20 = 30.
is $fn->( 'P', coerce( Point_(), $pt ) ), 30, 'Struct passed by value using coerce()';
# Reuse the trampoline (Cache Hit check)
# The signature generated for ('P', Point) should be cached.
my $pt2 = { x => 5, y => 5 };
is $fn->( 'P', coerce( Point_(), $pt2 ) ), 10, 'Repeated call with coerced struct works';
# Ensure previous calls didn't "lock" the signature to a specific set of types.
t/007_pointers.t view on Meta::CPAN
my $libc = load_library( libc() );
my $malloc_ptr = find_symbol( $libc, 'malloc' );
my $free_ptr = find_symbol( $libc, 'free' );
{
# Allocate memory using libc's malloc, not Affix's managed malloc
my $malloc = wrap( undef, $malloc_ptr, [Size_t] => Pointer [Void] );
my $p = $malloc->(16);
# Attaching free() as a destructor.
# When $p goes out of scope, Affix will call free($p).
attach_destructor( $p, $free_ptr, $libc );
}
pass 'Pin with custom destructor went out of scope without crashing';
};
#
done_testing;