Affix

 view release on metacpan or  search on metacpan

Changes.md  view on Meta::CPAN


- `SAVEVPTR`/`SAVEDESTRUCTOR_X` 'fix' broke thread safety. When a second thread or fiber entered the same Affix-generated XSUB, its `SAVEVPTR` captured the first fiber's temp arena as "old", so on scope exit the second fiber restored a dangling point...

## [v1.2.4] - 2026-08-15

Plugging leaks...

### Fixed

- Use `SAVEVPTR` and `SAVEDESTRUCTOR_X` to swap out arenas to fix leaky allocator in situations where tons of structs are passed in a list and need to be marshalled in only one direction
- Casting or binding an aggregate (`Affix::cast`, member pins) no longer leaks: member pins borrowed the freshly created parent hash/array as their lifeline, forming a strong reference cycle that Perl's refcounting cannot collect, so the whole pin tr...
- Passing a union to a wrapped call no longer segfaults: the argument sync read back *every* union member, and reading an inactive pointer/string member dereferenced the active member's float bytes as a C string pointer. Deep writes now skip members ...
- The library probe in `Affix::Platform::Unix` (`_findLib_gcc`) no longer prints linker errors (`undefined reference to WinMain`/`main`) while searching: it probes with `-shared`, which needs no entry point.
- Bitfields inside `Struct[...]` are no longer read or written out of bounds: `member->offset` now points at the storage unit base (with `bit_offset` relative to the unit) instead of the bitfield's own byte, so the unit-sized load/store in `push_stru...
- Reading and writing packed struct members (and pinned primitives) no longer uses unaligned native loads/stores: the dispatch vtables, bitfield vtables, pull handlers, and push handlers now round-trip through `memcpy`, which is safe on strict-alignm...
- Passing a wide string (`WString()`, i.e. `*wchar_t`) to a wrapped function now works on all platforms instead of croaking `Don't know how to handle this type of scalar as a pointer argument yet` on non-Windows systems, where the wide-string push op...
- Returning a `WString` no longer crashes: the wide-string pull handler called `SvGROW` on an uninitialized target SV, faulting before any buffer was allocated.
- [infix] Passing a 5-7 byte `Struct[...]` by value to a wrapped function no longer drops the trailing members on ARM64. The forward trampoline emitted a 32-bit register load unless the struct was exactly 8 bytes, so a `Struct[ arr => Array[2, UInt16...

## [v1.2.3] - 2026-08-08

Changes.md  view on Meta::CPAN

- 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.
- Added string-to-integer conversion when passing Perl strings to C functions expecting enums.

### Fixed

- Optimized `Pointer` returns in the XSUB dispatcher for performance by inlining the marshalling path and caching the stash.
- Fixed several issues in `CLONE` where metadata, managed memory, and enum registries were not correctly duplicated across perl's ithreads.
- Improved `_get_pin_from_sv` and `is_pin` to safely handle both references to pins and direct magical scalars like those found in Unions.
- Fixed potential double-frees and leaks in `Affix_Lib_DESTROY` and `Affix_free_pin` by improving reference counting and ownership tracking.
- Symbols found via `find_symbol` now correctly track the parent `Affix::Lib` object to prevent the library from being unloaded while symbols are still in use.
- Corrected a memory corruption bug in `Affix_malloc` and `Affix_strdup` caused by uninitialized internal `Affix_Pin` structures.
- Fixed `dualvar` behavior for enums returned from C, ensuring they correctly function as both strings and integers in Perl.
- Fixed the `clean` action in `Affix::Builder` which was failing due to an undefined `rmtree` call.
- Fixed an issue where blessing a return value could prematurely trigger 'set' magic on the underlying SV.
- Fixed `typedef` parsing: Named types now return proper `Affix::Type::Reference` objects instead of strings, ensuring they are correctly resolved when nested in other aggregates.
- Fixed `cast` to correctly return blessed `Affix::Live` objects when the `+` hint is used for live struct views.
- Hardened pointer indexing: Added strict type checks to `$ptr->[$i]` to ensure indexing is only performed on `Array` types or `Void*` (byte-indexed).

## [v1.0.7] - 2026-02-15

Changes.md  view on Meta::CPAN

Most of this version's work went into threading stability, ABI correctness, and security within the JIT engine.

### Changed

  - [[infix]] The JIT memory allocator on Linux now uses `memfd_create` (on kernels 3.17+) to create anonymous file descriptors for dual-mapped W^X memory. This avoids creating visible temporary files in `/dev/shm` and improves hygiene and security. ...
  - [infix] On dual-mapped platforms (Linux/BSD), the Read-Write view of the JIT memory is now **unmapped immediately** after code generation. This closes a security window where an attacker with a heap read/write primitive could potentially modify e...
  - [infix] `infix_library_open` now uses `RTLD_LOCAL` instead of `RTLD_GLOBAL` on POSIX systems. This prevents symbols from loaded libraries from polluting the global namespace and causing conflicts with other plugins or the host application.

### Fixed

  - Fixed `CLONE` to correctly copy user-defined types (typedefs, structs) to new threads. Previously, child threads started with an empty registry, causing lookup failures for types defined in the parent.
  - Thread safety: Fixed a crash when callbacks are invoked from foreign threads. Affix now correctly injects the Perl interpreter context into the TLS before executing the callback.
  - Added stack overflow protection to the FFI trigger. Argument marshalling buffers larger than 2KB are now allocated on the heap (arena) instead of the stack, preventing crashes on Windows and other platforms with limited stack sizes.
  - Type resolution: Fixed a logic bug where `Pointer[SV]` types were incorrectly treated as generic pointers if `typedef`'d. They are now correctly unwrapped into Perl CODE refs or blessed objects.
  - Process exit: Disabled explicit library unloading (`dlclose`/`FreeLibrary`) during global destruction. This prevents segmentation faults when background threads from loaded libraries try to execute code that has been unmapped from memory during s...
    I tried to just limit it to Go lang libs but it's just more trouble than it's worth until I resolve a few more things.
  - [infix] Fixed stack corruption on macOS ARM64 (Apple Silicon). `long double` on this platform is 8 bytes (an alias for `double`), unlike standard AAPCS64 where it is 16 bytes. The JIT previously emitted 16-byte stores (`STR Qn`) for these types, ...
  - [infix] Fixed `long double` handling on macOS Intel (Darwin). Verified that Apple adheres to the System V ABI for this type: it requires 16-byte stack alignment and returns values on the x87 FPU stack (`ST(0)`).
  - [infix] Fixed a generic System V ABI bug where 128-bit types (vectors, `__int128`) were not correctly aligned to 16 bytes on the stack relative to the return address, causing data corruption when mixed with odd numbers of 8-byte arguments.
  - [infix] Enforced natural alignment for stack arguments in the AAPCS64 implementation. Previously, arguments were packed to 8-byte boundaries, which violated alignment requirements for 128-bit types.
  - [infix] Fixed a critical deployment issue where the public `infix.h` header included an internal file (`common/compat_c23.h`). The header is now fully self-contained and defines `INFIX_NODISCARD` for attribute compatibility.

README.md  view on Meta::CPAN

raw memory safely is critical. Affix uses **Pins** to manage this boundary.

Affix now features a completely reimagined memory access system using Perl's internal magic to map Perl variables
directly to native C memory. This provides zero-copy performance with the ergonomics of native Perl Hashes and Arrays.

## Managed vs. Unmanaged Memory

Memory in Affix is handled by life lines.

- **Affix::Memory:** Created via `malloc()` or `calloc()`. These are root objects. When the Perl variable is destroyed, `safefree()` is called automatically.
- **Pins:** Created via `cast()` or pointer dereferencing. These variables do not own the memory, but they hold a reference to a life line to prevent the parent memory from being freed prematurely.

## Allocation & Deallocation

These functions allocate memory on the C heap. Memory allocated via these functions is **managed by Perl** by default.

### `malloc( $size )`

Allocates `$size` bytes of uninitialized memory. Returns a `Pointer[Void]` pin.

```perl

README.md  view on Meta::CPAN


## Recursive Protection

When an aggregate (Struct or Array) is marked as read-only, Affix automatically propagates that protection to all of
its members.

```perl
my $rect = cast($addr, Const[Struct[top => Struct[ x => Int, y => Int ], bottom => Struct[ x => Int, y => Int ] ]]);

# Even though 'x' wasn't explicitly marked Const, it inherited protection
# from the parent struct.
$rect->{top}{x} = 5; # FATAL ERROR
```

## Casting with Const

When using `cast( ... )`, you can prepend a `+` to the type signature to create an immutable view of a raw memory
address.

```perl
my $view = cast($raw_addr, Const[MyStruct]);

infix/src/common/double_tap.h  view on Meta::CPAN

static void print_indent(FILE * stream) {
    _tap_ensure_initialized();
    for (int i = 0; i < current_state->indent_level; ++i)
        fprintf(stream, "    ");
}

/** @internal Pushes a new state onto the thread-local stack for entering a subtest. */
static void push_state(void) {
    if (current_state >= &state_stack[MAX_DEPTH - 1])
        tap_bail_out("Exceeded maximum subtest depth of %d", MAX_DEPTH);
    tap_state_t * parent = current_state;
    current_state++;
    memset(current_state, 0, sizeof(tap_state_t));
    current_state->plan = NO_PLAN;
    current_state->indent_level = parent->indent_level + 1;
    // A subtest inherits the 'todo' state from its parent.
    if (parent->todo) {
        current_state->todo = true;
        snprintf(current_state->todo_reason, sizeof(current_state->todo_reason), "%s", parent->todo_reason);
    }
}

/** @internal Pops the current state from the stack when a subtest ends. */
static void pop_state(void) {
    if (current_state <= &state_stack[0])
        tap_bail_out("Internal error: Attempted to pop base test state");
    current_state--;
}

infix/src/common/double_tap.h  view on Meta::CPAN

    if (!current_state->has_plan) {
        // If no plan was declared, implicitly plan for the number of tests that ran.
        current_state->plan = current_state->count;
        print_indent(stdout);
        printf("1..%llu\n", (unsigned long long)current_state->plan);
    }
    bool plan_ok = (current_state->plan == current_state->count);
    bool subtest_ok = (current_state->failed == 0) && plan_ok;
    char name_buffer[256];
    snprintf(name_buffer, sizeof(name_buffer), "%s", current_state->subtest_name);
    pop_state();  // Return to the parent's state.
    // Report the subtest's success or failure as a single test point in the parent scope.
    ok(subtest_ok, "%s", name_buffer);
    return false;  // Exits the `for` loop.
}

int tap_done(void) {
    _tap_ensure_initialized();
    if (current_state != &state_stack[0])
        tap_bail_out("tap_done() called inside a subtest");
    if (!current_state->has_plan) {
        current_state->plan = current_state->count;

infix/src/core/signature.c  view on Meta::CPAN

    // If it wasn't a `name:`, backtrack to the original position.
    state->p = p_before;
    return nullptr;
}
/**
 * @internal
 * @brief A lookahead function to disambiguate a grouped type `(type)` from a
 *        function signature `(...) -> type`.
 *
 * @details This is a classic parser "lookahead". When the parser encounters an opening
 * parenthesis `(`, it calls this function to peek ahead in the string without
 * consuming any input. By scanning for a matching `)` and checking if it is
 * followed by a `->` token, it can decide whether to parse the content as a
 * single, parenthesized type or as a full function signature.
 *
 * @param[in] state The current parser state (read-only).
 * @return `true` if a `->` token follows the closing parenthesis.
 */
static bool is_function_signature_ahead(const parser_state * state) {
    const char * p = state->p;
    if (*p != '(')
        return false;
    p++;
    // Find the matching ')' by tracking nesting depth.
    int depth = 1;
    while (*p != '\0' && depth > 0) {
        if (*p == '(')
            depth++;
        else if (*p == ')')
            depth--;
        p++;
    }
    if (depth != 0)
        return false;  // Mismatched parentheses.
    // Skip any whitespace or comments after the ')'
    while (isspace((unsigned char)*p) || *p == '#') {
        if (*p == '#')
            while (*p != '\n' && *p != '\0')
                p++;
        else
            p++;
    }
    // Check for the '->' arrow.
    return (p[0] == '-' && p[1] == '>');

infix/src/core/types.c  view on Meta::CPAN

INFIX_API c23_nodiscard infix_type * infix_type_create_pointer(void) { return &_infix_type_pointer; }
/**
 * @brief Creates a static descriptor for the `void` type.
 * @return A pointer to the static `infix_type` descriptor. Does not need to be freed.
 */
INFIX_API c23_nodiscard infix_type * infix_type_create_void(void) { return &_infix_type_void; }
/**
 * @brief A factory function to create an `infix_struct_member`.
 * @param[in] name The name of the member (optional, can be `nullptr`).
 * @param[in] type The `infix_type` of the member.
 * @param[in] offset The byte offset of the member from the start of its parent aggregate.
 * @return An initialized `infix_struct_member` object.
 */
INFIX_API infix_struct_member infix_type_create_member(const char * name, infix_type * type, size_t offset) {
    return (infix_struct_member){name, type, offset, 0, 0, false};
}
/**
 * @brief A factory function to create a bitfield `infix_struct_member`.
 * @param[in] name The name of the member.
 * @param[in] type The integer `infix_type` of the bitfield.
 * @param[in] offset The byte offset (usually 0 for automatic layout).

infix/src/core/types.c  view on Meta::CPAN

 * @internal
 * @brief Recursively recalculates the size, alignment, and member offsets for a type graph.
 *
 * @details This function is the implementation of the **"Layout"** stage of the
 * "Parse -> Copy -> Resolve -> Layout" data pipeline. It is designed to be called
 * *after* a type graph has been fully resolved, ensuring that all
 * `INFIX_TYPE_NAMED_REFERENCE` nodes have been replaced with concrete types.
 *
 * The function performs a **post-order traversal** of the type graph. This is critical,
 * as it ensures that the layout of nested types (like a struct member) is correctly
 * calculated *before* the layout of the parent container that depends on it.
 *
 * It correctly handles cyclic graphs by using a `visited_head` linked list to track
 * nodes currently in the recursion stack, preventing infinite loops.
 *
 * @param[in,out] type The `infix_type` object to recalculate. Its `size`, `alignment`, and
 *        (if applicable) member `offset` fields are modified in-place. The function
 *        does nothing if `type` is `nullptr` or a static primitive (`is_arena_allocated` is false).
 * @param[in,out] visited_head A pointer to the head of the visited list for cycle detection.
 *        The list is modified during the traversal.
 */

lib/Affix.c  view on Meta::CPAN


    if (status != INFIX_SUCCESS) {
        infix_arena_destroy(parse_arena);
        croak("Affix failed to rebuild trampoline in new thread");
    }

    affix->cif = infix_forward_get_code(affix->infix);
    affix->ret_type = infix_forward_get_return_type(affix->infix);
    affix->unwrapped_ret_type = _unwrap_pin_type(affix->ret_type);
    affix->ret_pull_handler = get_pull_handler(aTHX_ affix->ret_type);
    // affix->ret_opcode is already set from parent, but safe to assume it matches

    // Allocate arenas & SV
    affix->args_arena = infix_arena_create(4096);
    affix->ret_arena = infix_arena_create(1024);
    affix->return_sv = newSV(0);
    if (affix->num_args > 0)
        Newx(affix->c_args, affix->num_args, void *);

    affix->variadic_cache = newHV();

lib/Affix.c  view on Meta::CPAN

        XSRETURN(1);
    }

    XSRETURN_UNDEF;
}

XS_INTERNAL(Affix_CLONE) {
    dXSARGS;
    PERL_UNUSED_VAR(items);

    // Initialize the new thread's context (copies bitwise from parent)
    MY_CXT_CLONE;

    // Capture the parent's registry pointer.
    // After MY_CXT_CLONE, MY_CXT refers to the new thread's context,
    // which has been initialized as a bitwise copy of the parent's context.
    infix_registry_t * parent_registry = MY_CXT.registry;

    // Overwrite shared pointers with fresh objects for the new thread
    MY_CXT.lib_registry = newHV();
    MY_CXT.callback_registry = newHV();
    MY_CXT.enum_registry = newHV();
    MY_CXT.coercion_cache = newHV();
    MY_CXT.stash_pointer = nullptr;

    // Deep copy the type registry.
    // This ensures typedefs and structs defined in the parent thread exist in the child thread,
    // but the child owns its own memory arena, making it thread-safe.
    if (parent_registry)
        MY_CXT.registry = infix_registry_clone(parent_registry);
    else
        MY_CXT.registry = infix_registry_create();

    if (!MY_CXT.registry)
        warn("Failed to initialize the global type registry in new thread");

    // Don't ccall _register_core_types here if we cloned, because the clone already contains @SV, @File, etc.
    if (!parent_registry)
        _register_core_types(MY_CXT.registry);

    XSRETURN_EMPTY;
}


#include "Affix/marshal.c"

// Runtime allocator callbacks that route infix's memory through Perl's
// allocator. They are installed once at load time via infix_set_allocator() in

lib/Affix.pod  view on Meta::CPAN

directly to native C memory. This provides zero-copy performance with the ergonomics of native Perl Hashes and Arrays.

=head2 Managed vs. Unmanaged Memory

Memory in Affix is handled by life lines.

=over

=item * B<Affix::Memory:> Created via C<malloc()> or C<calloc()>. These are root objects. When the Perl variable is destroyed, C<safefree()> is called automatically.

=item * B<Pins:> Created via C<cast()> or pointer dereferencing. These variables do not own the memory, but they hold a reference to a life line to prevent the parent memory from being freed prematurely.

=back

=head2 Allocation & Deallocation

These functions allocate memory on the C heap. Memory allocated via these functions is B<managed by Perl> by default.

=head3 C<malloc( $size )>

Allocates C<$size> bytes of uninitialized memory. Returns a C<Pointer[Void]> pin.

lib/Affix.pod  view on Meta::CPAN

    $point->{x} = 10;    # FATAL ERROR

=head2 Recursive Protection

When an aggregate (Struct or Array) is marked as read-only, Affix automatically propagates that protection to all of
its members.

    my $rect = cast($addr, Const[Struct[top => Struct[ x => Int, y => Int ], bottom => Struct[ x => Int, y => Int ] ]]);

    # Even though 'x' wasn't explicitly marked Const, it inherited protection
    # from the parent struct.
    $rect->{top}{x} = 5; # FATAL ERROR

=head2 Casting with Const

When using C<cast( ... )>, you can prepend a C<+> to the type signature to create an immutable view of a raw memory
address.

    my $view = cast($raw_addr, Const[MyStruct]);
    # $view is now a read-only HashRef mapping to C memory.

lib/Affix/Platform/BSD.pm  view on Meta::CPAN

package Affix::Platform::BSD v1.2.5 {
    use v5.40;
    use parent 'Affix::Platform::Unix';
    use parent 'Exporter';
    our @EXPORT_OK   = qw[find_library];
    our %EXPORT_TAGS = ( all => \@EXPORT_OK );

    sub find_library ( $name, $version //= '' ) {    # TODO: actually feed version to diff methods
        if ( -f $name ) {
            $name = readlink $name if -l $name;      # Handle symbolic links
            return $name                             # if is_elf($name);
        }
        CORE::state $cache;
        my $regex = qr[-l\Q$name\E\.[^\s]+.+\s*=>\s*(.+)$];

lib/Affix/Platform/MacOS.pm  view on Meta::CPAN

package Affix::Platform::MacOS v1.2.5 {
    use v5.40;
    use DynaLoader;
    use parent 'Affix::Platform::Unix';
    use parent 'Exporter';
    our @EXPORT_OK   = qw[find_library];
    our %EXPORT_TAGS = ( all => \@EXPORT_OK );

    sub find_library ($name) {
        return $name if -f $name;
        for my $file ( "lib$name.dylib", "$name.dylib", "$name.framework/$name" ) {
            my $path = DynaLoader::dl_findfile($file);
            return $path if $path;
        }
    }

lib/Affix/Platform/Solaris.pm  view on Meta::CPAN

package Affix::Platform::Solaris v1.2.5 {
    use v5.40;
    use parent 'Affix::Platform::Unix';
    use parent 'Exporter';
    our @EXPORT_OK   = qw[find_library];
    our %EXPORT_TAGS = ( all => \@EXPORT_OK );
};
1;

lib/Affix/Platform/Unix.pm  view on Meta::CPAN

package Affix::Platform::Unix v1.2.5 {
    use v5.40;
    use Path::Tiny qw[path];
    use Config     qw[%Config];
    use DynaLoader;
    use parent 'Exporter';
    our @EXPORT_OK   = qw[find_library];
    our %EXPORT_TAGS = ( all => \@EXPORT_OK );
    my $so = $Config{so};

    sub is_elf ($filename) {
        my $elf_header = "\x7fELF";                        # ELF header in binary format
        open( my $fh, '<:raw', $filename ) or return 0;    # Open in binary mode
        sysread( $fh, my $header, 4 ) || return;
        close($fh);
        return $header eq $elf_header;

lib/Affix/Platform/Windows.pm  view on Meta::CPAN

package Affix::Platform::Windows v1.2.5 {
    use v5.40;
    use DynaLoader;
    use Win32;    # Core on Windows
    use File::Spec;
    use parent 'Exporter';
    our @EXPORT_OK   = qw[find_library];
    our %EXPORT_TAGS = ( all => \@EXPORT_OK );

    sub find_msvcrt () {
        my $version = get_msvcrt_version();    # Assuming _get_build_version is defined elsewhere
        if ( !$version ) {
            my @possible_dlls = (
                'msvcrt.dll',

                #~ sprintf( 'msvcr%d.dll', $version * 10 )

lib/Affix/Wrap.pm  view on Meta::CPAN

            $abs =~ s{\\}{/}g;
            return $abs;
        }
        ADJUST {
            my %seen_dirs;
            for my $f (@$project_files) {
                next unless defined $f && length $f;
                my $abs = $self->_normalize($f);
                next unless length $abs;
                $allowed_files->{$abs} = 1;
                my $dir = Path::Tiny::path($abs)->parent->stringify;
                $dir =~ s{\\}{/}g;
                unless ( $seen_dirs{$dir}++ ) { push @$project_dirs, $dir; }
            }
        }

        method parse ( $entry_point, $include_dirs //= [] ) {
            if ( !defined $entry_point || !length $entry_point ) {
                ($entry_point) = grep { defined $_ && length $_ } @$project_files;
            }
            return () unless defined $entry_point && length $entry_point;
            my $ep_abs = $self->_normalize($entry_point);
            return () unless length $ep_abs;
            $allowed_files->{$ep_abs} = 1;
            $last_seen_file = $ep_abs;
            my $ep_dir = Path::Tiny::path($ep_abs)->parent->stringify;
            $ep_dir =~ s{\\}{/}g;
            my $found = 0;

            for my $pd (@$project_dirs) {
                if ( $ep_dir eq $pd ) { $found = 1; last; }
            }
            push @$project_dirs, $ep_dir unless $found;
            my @includes = map { "-I" . $self->_normalize($_) } @$include_dirs;
            for my $d (@$project_dirs) { push @includes, "-I$d"; }
            my @cmd = (

lib/Affix/Wrap.pm  view on Meta::CPAN

            ) {
                next if $2 =~ /^(if|while|for|return|switch|typedef)$/ || $1 =~ /static/;
                my $s = $-[0];
                my $e = $+[0];
                my ( $ret_str, $func_name, $args_str ) = ( $1, $2, substr( $3, 1, -1 ) );
                #
                $ret_str =~ s/\b[A-Z_][A-Z0-9_]*\b//g;
                $ret_str =~ s/^\s+|\s+$//g;
                my $ret_obj = Affix::Wrap::Type->parse($ret_str);

                # Split args respecting commas inside parentheses (function pointers, etc.)
                my @args_raw = grep {length} map { s/^\s+|\s+$//g; $_ } split /,(?![^(]*\))/, $args_str;
                if ( @args_raw == 1 && $args_raw[0] =~ /^void$/ ) { @args_raw = (); }
                my @args;
                for my $raw (@args_raw) {
                    if ( $raw =~ /^(.+?)\s*\(\*\s*(\w+)\)\s*\((.*)\)$/ ) {
                        my ( $r_type, $cb_name, $cb_args ) = ( $1, $2, $3 );
                        my $ret = Affix::Wrap::Type->parse($r_type);
                        my @p;
                        if ( $cb_args ne '' && $cb_args ne 'void' ) {
                            @p = map { Affix::Wrap::Type->parse($_) } split /,(?![^(]*\))/, $cb_args;

lib/Affix/marshal.c  view on Meta::CPAN

        sv_magicext((SV *)hv, owner, PERL_MAGIC_ext, &vtbl_lazy_aggregate, (char *)&m, sizeof(Affix_Pin_2_Point_Oh));

        size_t count = infix_type_get_member_count(res);
        for (size_t i = 0; i < count; i++) {
            const infix_struct_member * m = infix_type_get_member(res, i);
            SV * v = newSV(0);
            /* Propagate nullptr safely so getters don't read from 0x0 + offset */
            void * child_ptr = ptr ? ((char *)ptr + m->offset) : nullptr;
            // Don't read the memory now. Wait until the user accesses the hash key.
            // NOTE: member pins borrow the *external* lifeline (`owner`), never the
            // freshly created parent HV. Referencing the parent here creates a strong
            // reference cycle (HV owns the members, each member's mg_obj owns the HV)
            // that Perl's refcounting cannot collect, leaking the whole pin tree and
            // its arena on every bind/cast.
            bind_placeholder(
                aTHX_ v, child_ptr, m->type, m->bit_offset, m->bit_width, false, owner, NULL, readonly, false);
            hv_store(hv, m->name ? m->name : "", strlen(m->name ? m->name : ""), v, 0);
        }
        return newRV_noinc((SV *)hv);
    }
    else if (cat == INFIX_TYPE_ARRAY || cat == INFIX_TYPE_VECTOR || cat == INFIX_TYPE_COMPLEX) {

lib/Affix/marshal.c  view on Meta::CPAN

        }

        AV * av = newAV();
        Affix_Pin_2_Point_Oh am = {.ptr = ptr, .type = type, .arena = arena, .readonly = readonly};
        sv_magicext((SV *)av, owner, PERL_MAGIC_ext, &vtbl_array, (char *)&am, sizeof(Affix_Pin_2_Point_Oh));

        for (size_t i = 0; i < n; i++) {
            SV * el = newSV(0);
            void * child_ptr = ptr ? ((char *)ptr + (i * step)) : nullptr;
            // Borrow the external lifeline (see struct branch above): referencing the
            // parent AV here creates an uncollectable refcount cycle.
            bind_placeholder(aTHX_ el, child_ptr, el_type, 0, 0, false, owner, nullptr, readonly, false);
            av_push(av, el);
        }
        return newRV_noinc((SV *)av);
    }
    return newSV(0);
}

/**
 * @brief Recursively constructs a Perl tree (Hash/Array) mapping to a C aggregate.

lib/Test2/Tools/Affix.pm  view on Meta::CPAN

    use Test2::IPC;
    use Path::Tiny qw[path tempfile];
    use Exporter 'import';
    use Capture::Tiny ':all';
    our @CARP_NOT;
    our @EXPORT      = qw[compile_ok affix_ok leaks];
    our %EXPORT_TAGS = ( all => \@EXPORT );
    #
    my $OS  = $^O;
    my $Inc = path($0)->absolute;
    $Inc = $Inc->parent while !$Inc->child('t')->is_dir;
    $Inc = $Inc->child( 't', 'src' );
    my @cleanup;

    END {
        for my $file ( grep {-f} @cleanup ) {
            unlink $file;
        }
        for my $dir ( grep {-d} @cleanup ) {
            $dir->remove_tree;
        }

t/035_magic_struct.t  view on Meta::CPAN

subtest 'Reference counting and persistence' => sub {
    my $sub_hash;
    {
        my $root_ptr = Affix::malloc( sizeof( Transform() ) );
        my $tx       = cast $root_ptr, Transform();

        # Grab a reference to a nested struct
        $sub_hash = $tx->{origin};
    }

    # Attempt to write to the sub-hash after parent is technically "gone"
    $sub_hash->{y} = 77;

    # If we are here and haven't crashed, success.
    # We can verify by re-pinning a pointer to see if the value is there.
    is $sub_hash->{y}, 77, 'Sub-struct remains valid and writable after parent scope ends';
};
subtest 'Magical Array Indexing (Primitives)' => sub {

    # Allocate memory for 5 integers
    my $ptr = Affix::malloc( sizeof( Array [ Int, 5 ] ) );
    memset( $ptr, 0, sizeof( Array [ Int, 5 ] ) );

    # Cast to Pointer[Int] so Affix knows the element size
    $ptr = cast( $ptr, Array [ Int, 5 ] );

t/094_cast_cycle.t  view on Meta::CPAN

use lib './lib', '../lib', '../blib/arch/', 'blib/arch', '../', '.';
use Affix qw[:all];
use Test2::V0 -no_srand => 1;
use v5.36;

# Regression test for the pin-tree refcount cycle that leaked memory on every
# Affix::cast() of an aggregate. The rope.pl SDL3 demo grows ~5.4KB/iter
# whenever a mouse-move event is polled (every event runs Affix::cast()):
# the per-cast parse arena (4096B) plus ~290B per struct member was never
# reclaimed because member pins strongly referenced the freshly created parent
# HV/AV, forming an uncollectable cycle. Member pins now borrow the external
# lifeline instead, so free_v2_pin() runs on drop.
#
# Valgrind cannot see this (infix allocations route through Perl's allocator
# and the loss is a Perl SV cycle), so we assert flat RSS instead.
plan skip_all => '/proc/self/status is not available' unless -r '/proc/self/status';

sub rss_kb {
    open my $f, '<', '/proc/self/status' or die "open /proc/self/status: $!";
    my ($rss) = map { /^VmRSS:\s+(\d+)/ ? $1 : () } <$f>;



( run in 1.562 second using v1.01-cache-2.11-cpan-364913b4093 )