Affix
view release on metacpan or search on metacpan
### Fixed
- `Struct[...]` fields of type `WString` can now be assigned to (or as?) Perl strings (`SvPV`) rather than falling through and becoming null pointers
## [v1.2.2] - 2026-08-05
The headline is support for RISC-V!
### Fixed
- `malloc()`/`calloc()` pins no longer lose their `Affix::Memory` lifeline: the memory is kept alive for as long as any derived pin exists (fixes a use-after-free where the block could be handed out to a later allocation), and `free()` now works on p...
- `realloc()` no longer corrupts the `Affix::Memory` reference header when updating the stored address.
- [infix] Layout recalculation now descends into reverse trampolines, so a `Struct[...]` declared inline in a signature whose members referenced named types no longer has corrupted offsets.
### Added
- [infix] Support for RISC-V (rv64)
## [v1.2.1] - 2026-08-02
### Fixed
lib/Affix.c view on Meta::CPAN
if (pointee->category == INFIX_TYPE_PRIMITIVE &&
(pointee->meta.primitive_id == INFIX_PRIMITIVE_SINT8 ||
pointee->meta.primitive_id == INFIX_PRIMITIVE_UINT8)) {
return_as_value = true;
is_string_type = true;
}
}
/* Determine the owner lifeline */
/* This ensures that if we cast memory inside an Affix::Memory block,
the block stays alive as long as this casted variable exists. */
SV * owner = _borrow_lifeline(aTHX_ arg);
/* Execution */
if (return_as_value) {
SV * ret_val = sv_newmortal();
if (is_string_type) // String pullers expect char**, so we pass the address of our pointer
ptr2sv(aTHX_ nullptr, &ptr_val, ret_val, new_type, readonly);
else // Primitives expect the address of the data
ptr2sv(aTHX_ nullptr, ptr_val, ret_val, new_type, readonly);
lib/Affix.h view on Meta::CPAN
infix_arena_t * arena; /**< Local arena for anonymous types. */
uint8_t bit_offset; /**< Bitfield offset (0 for standard types). */
uint8_t bit_width; /**< Bitfield width in bits (0 for standard types). */
bool readonly; /**< True if the pin is marked const/readonly. */
bool absolute; /**< True if ptr is the target address, false if it's the address of a variable */
void (*destructor)(void *);
SV * destructor_lib_sv;
} Affix_Pin_2_Point_Oh;
/// 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.
lib/Affix/marshal.c view on Meta::CPAN
* @param name The struct or primitive type name to cast the memory into.
* @return A magic-bound SV tracking the memory block natively.
*/
SV * cast(pTHX_ SV * in, const char * name) {
dMY_CXT;
void * addr = get_address_v2(aTHX_ in);
if (!addr)
return &PL_sv_undef;
/* Keep the blessed Affix::Memory object itself as the lifeline so the pin
holds a strong reference to it. This keeps the memory alive for as long as
any derived pin exists and lets free()/DESTROY locate the owner. */
SV * owner = (SvROK(in) && sv_derived_from(in, "Affix::Memory")) ? in : nullptr;
infix_type * new_type = nullptr;
infix_arena_t * local_arena = nullptr;
if (infix_type_from_signature(&new_type, &local_arena, name, MY_CXT.registry) != INFIX_SUCCESS)
croak("Type not found: %s", name);
const infix_type * resolved = resolve_type(aTHX_ new_type);
infix_type_category cat = infix_type_get_category(resolved);
t/007_pointers.t view on Meta::CPAN
DLLEXPORT void libc_free(void * ptr){ free(ptr); }
END_C
#
my $lib_path = compile_ok($C_CODE);
ok( $lib_path && -e $lib_path, 'Compiled a test shared library successfully' );
#
affix $lib_path, 'read_int_from_void_ptr', [ Pointer [Void] ], Int;
ok my $mem = malloc(8), 'malloc(8)';
# Cast returns a new pin. We must assign it or use the returned object.
# Also, we keep $mem alive to ensure the memory isn't freed if $int_ptr assumes
# $mem owns it (though cast usually creates unmanaged aliases, so we need $mem to stay alive).
my $int_ptr = cast( malloc( sizeof(Int) ), Array [ Int, 1 ] );
# Test magical 'set' via dereferencing
# $int_ptr is an array ref bound directly to the C memory
$int_ptr->[0] = 42;
# Use the original $mem pointer for reading (verifying they point to the same place)
is read_int_from_void_ptr($int_ptr), 42, 'Magical set via deref wrote to C memory';
# Test cast again
t/007_pointers.t view on Meta::CPAN
# Correct cleanup: Use the allocator that created it.
c_free($string);
pass('freed via c_free');
};
subtest 'deep pointers' => sub {
# Deep Indirection (***int)
isa_ok my $set_deep = wrap( $lib_path, 'set_int_deep', [ Pointer [ Pointer [ Pointer [Int] ] ], Int ] => Void ), ['Affix'];
# Manually construct the pointer chain with correct types
# Keep original 'malloc' pointers alive (managed) while using 'cast' aliases
# Layer 1: The int value (int*)
my $p_mem = malloc(8);
my $p_val = Affix::cast( $p_mem, Pointer [Int] );
# Assigning directly ($p_val = 0) would overwrite the magic scalar with a normal SV*
$$p_val = 0;
# Layer 2: Pointer to Layer 1 (int**)
my $pp_mem = malloc(8);
my $pp_val = Affix::cast( $pp_mem, Pointer [ Pointer [Int] ] );
t/019_fileio.t view on Meta::CPAN
# Write from Perl using retrieved handle
# print {$retrieved_fh} "From Perl"; # Careful, might double-close if not careful
# Check file content
open my $check, '<', $filename;
my @lines = <$check>;
close $check;
is scalar(@lines), 2, 'File has 2 lines';
like $lines[0], qr/\[1\] First message/, 'Line 1 matches';
like $lines[1], qr/\[2\] Second message/, 'Line 2 matches';
# Keep $fh alive until test end to avoid closing underneath C
close $fh;
};
subtest 'File inside Struct (Value Return)' => sub {
my $file = tempfile( { realpath => 1 } );
my $filename = $file->stringify;
my $fh = $file->filehandle('+>');
my $old_fh = select($fh);
$| = 1;
select($old_fh);
t/035_magic_struct.t view on Meta::CPAN
my $element_ref;
{
my $root = Affix::malloc( sizeof(Int) * 10 );
$root = cast( $root, Array [Int] );
$root->[5] = 12345;
# Take a reference to a specific element's magical SV
$element_ref = \( $root->[5] );
# $root goes out of scope here.
# In the new system, $element_ref's magic keeps the root alive.
}
# ASSERT: We can still read and write to the element
is $$element_ref, 12345, 'Magical element SV kept root memory alive';
$$element_ref = 54321;
is $$element_ref, 54321, 'Magical element SV still writable after root object scope ended';
};
subtest 'Out of bounds (C-style)' => sub {
# In C, pointers don't have bounds. Our magical system should behave like C.
# To prevent ACTUAL heap corruption (which crashes the Perl allocator
# during global destruction), we allocate enough backing memory here.
my $ptr = Affix::malloc( 15 * sizeof(Int) );
$ptr = cast( $ptr, Pointer [Int] );
t/093_malloc_lifeline.t view on Meta::CPAN
total += arr[i];
return total;
}
END_C
#
my $lib_path = compile_ok($C_CODE);
ok $lib_path && -e $lib_path, 'Compiled a test shared library successfully';
affix $lib_path, 'read_int_from_void_ptr', [ Pointer [Void] ], Int;
affix $lib_path, 'sum_int_array', [ Pointer [Int], Int ], Int;
#
subtest 'malloc: memory survives while the pin is alive' => sub {
my $a = malloc(64);
my $a_addr = address($a);
my $collisions = 0;
for ( 1 .. 10_000 ) {
my $p = malloc(64);
$collisions++ if address($p) == $a_addr;
}
is $collisions, 0, 'live block is never handed out again';
};
#
( run in 2.551 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )