Affix

 view release on metacpan or  search on metacpan

Changes.md  view on Meta::CPAN


## [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

### Fixed

MANIFEST  view on Meta::CPAN

infix/src/jit/trampoline.c
lib/Affix.c
lib/Affix.h
lib/Affix.pm
lib/Affix.pod
lib/Affix/Build.pm
lib/Affix/Build.pod
lib/Affix/Platform/BSD.pm
lib/Affix/Platform/MacOS.pm
lib/Affix/Platform/Solaris.pm
lib/Affix/Platform/Unix.pm
lib/Affix/Platform/Windows.pm
lib/Affix/Wrap.pm
lib/Affix/Wrap.pod
lib/Affix/marshal.c
lib/Test2/Tools/Affix.pm
t/001_affix.t
t/002_synopsis.t
t/003_pin.t
t/004_typedef.t
t/005_varargs.t

MANIFEST  view on Meta::CPAN

t/026_context.t
t/027_thread_safety.t
t/028_pointer_indexing.t
t/029_union_pins.t
t/030_live_struct.t
t/031_live_array.t
t/032_recursive_liveness.t
t/033_unified_access.t
t/035_magic_struct.t
t/040_error.t
t/060_platform_unix_security.t
t/070_security_fixes.t
t/075_error_negative.t
t/076_locate_libs.t
t/077_unicode.t
t/078_cast_coerce.t
t/079_boundary.t
t/080_stress.t
t/081_packed.t
t/082_own.t
t/083_pin_conventions.t

README.md  view on Meta::CPAN


Load and inspect dynamic libraries across platforms. Affix's smart discovery engine handles varying extensions,
prefixes, and search paths automatically.

## Library Discovery

When you provide a bare library name (e.g., `'z'`, `'ssl'`, `'user32'`) rather than an absolute path, Affix
automatically formats the name for the current platform (e.g., `libz.so`, `libz.dylib`, `z.dll`) and searches the
following locations in order:

- 1. **Standard System Paths:** Windows `System32`/`SysWOW64`; Unix `/usr/local/lib`, `/usr/lib`, `/lib`, `/usr/lib/system`.
- 2. **Environment Variables:** Paths defined in `LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`, `DYLD_FALLBACK_LIBRARY_PATH`, or `PATH`.
- 3. **Local Paths:** The current working directory (`.`) and its `lib/` subdirectory.

## Functions

### `load_library( $path_or_name )`

Locates and loads a dynamic library into memory, returning an opaque `Affix::Lib` handle.

```perl

README.md  view on Meta::CPAN


# ERROR HANDLING & DEBUGGING

Diagnose FFI issues with built-in error reporting, memory inspection, and hex dumps. Bridging two entirely different
runtimes can lead to spectacular crashes if types or memory boundaries are mismatched.

## Error Handling

### `errno()`

Accesses the system error code from the most recent FFI or standard library call (reads `errno` on Unix and
`GetLastError` on Windows).

This function returns a **dualvar**. It behaves as an integer in numeric context, and magically resolves to the
human-readable system error message (via `strerror` or `FormatMessage`) in string context.

```perl
# Suppose a C file-open function fails
my $fd = c_open("/does/not/exist");
if (!$fd) {
    my $err = errno();

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

 */
#pragma once
#ifdef DBLTAP_ENABLE
#define TAP_VERSION 13
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__unix__) || defined(__APPLE__) || defined(__OpenBSD__)
#include <unistd.h>
#endif
#if defined(_WIN32) || defined(__CYGWIN__)
#include <windows.h>
#elif (defined(__unix__) || defined(__APPLE__)) && !defined(__OpenBSD__)
// Do not include pthread.h on OpenBSD to prevent linking/cleanup issues if -pthread is not used.
#include <pthread.h>
#endif

// C++ Headers must be included BEFORE extern "C"
#if defined(__cplusplus)
#include <atomic>
#endif

#ifdef __cplusplus

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

#define TAP_THREAD_LOCAL
#elif defined(__cplusplus)
#define TAP_THREAD_LOCAL thread_local
#elif defined(_MSC_VER)
// Microsoft Visual C++
#define TAP_THREAD_LOCAL __declspec(thread)
#elif defined(_WIN32) && defined(__clang__)
// Clang on Windows
#define TAP_THREAD_LOCAL __declspec(thread)
#elif defined(__GNUC__) || defined(__clang__)
// GCC (including MinGW) and Clang on *nix
#define TAP_THREAD_LOCAL __thread
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__)
#define TAP_THREAD_LOCAL _Thread_local
#else
#define TAP_THREAD_LOCAL
#if !defined(_MSC_VER)
#warning "Compiler does not support thread-local storage; tests will not be thread-safe."
#endif
#endif

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

#if defined(_WIN32) || defined(__CYGWIN__)
static INIT_ONCE g_tap_init_once = INIT_ONCE_STATIC_INIT;
static BOOL CALLBACK _tap_init_routine(PINIT_ONCE initOnce, PVOID param, PVOID * context) {
    (void)initOnce;
    (void)param;
    (void)context;
    printf("TAP version %d\n", TAP_VERSION);
    fflush(stdout);
    return TRUE;
}
#elif (defined(__unix__) || defined(__APPLE__)) && !defined(__OpenBSD__)
static pthread_once_t g_tap_init_once = PTHREAD_ONCE_INIT;
static void _tap_init_routine(void) {
    printf("TAP version %d\n", TAP_VERSION);
    fflush(stdout);
}
#else  // OpenBSD or other platforms without robust pthread_once support in this context
static bool g_tap_initialized = false;
#endif

/**
 * @internal
 * @brief Ensures the TAP header has been printed and thread-local state is initialized.
 * Uses `pthread_once` or `InitOnceExecuteOnce` to guarantee the TAP version header
 * is printed exactly once per process, even with multiple threads. It also initializes
 * the thread-local state for the current thread if it's the first test call on that thread.
 */
static void _tap_ensure_initialized(void) {
#if defined(_WIN32) || defined(__CYGWIN__)
    InitOnceExecuteOnce(&g_tap_init_once, _tap_init_routine, NULL, NULL);
#elif (defined(__unix__) || defined(__APPLE__)) && !defined(__OpenBSD__)
    pthread_once(&g_tap_init_once, _tap_init_routine);
#else
    // Fallback for OpenBSD/single-threaded builds
    if (!g_tap_initialized) {
        printf("TAP version %d\n", TAP_VERSION);
        fflush(stdout);
        g_tap_initialized = true;
    }
#endif
    if (!current_state) {

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

// Disable TLS entirely on this platform to ensure stability, at the cost of thread-safety.
#define INFIX_TLS
#elif defined(INFIX_COMPILER_MSVC)
// Microsoft Visual C++
#define INFIX_TLS __declspec(thread)
#elif defined(INFIX_OS_WINDOWS) && defined(INFIX_COMPILER_CLANG)
// Clang on Windows: check if behaving like MSVC or GCC.
// If using MSVC codegen/headers, use declspec.
#define INFIX_TLS __declspec(thread)
#elif defined(INFIX_COMPILER_GCC)
// MinGW (GCC on Windows) and standard GCC/Clang on *nix.
// MinGW prefers __thread or _Thread_local over __declspec(thread).
#define INFIX_TLS __thread
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__)
// Fallback to C11 standard
#define INFIX_TLS _Thread_local
#else
// Fallback for compilers that do not support TLS. This is not thread-safe.
#warning "Compiler does not support thread-local storage; error handling will not be thread-safe."
#define INFIX_TLS
#endif

lib/Affix.pm  view on Meta::CPAN


    BEGIN {
        use XSLoader;
        $DynaLoader::dl_debug = 0;
        $okay                 = XSLoader::load();
        my $platform
            = 'Affix::Platform::' .
            ( ( $^O eq 'MSWin32' ) ? 'Windows' :
                $^O eq 'darwin'                                                                   ? 'MacOS' :
                ( $^O eq 'freebsd' || $^O eq 'openbsd' || $^O eq 'netbsd' || $^O eq 'dragonfly' ) ? 'BSD' :
                'Unix' );

        #~ warn $platform;
        #~ use base $platform;
        eval "use $platform qw[:all]";
        $@ && die $@;
        our @ISA = ($platform);
    }
    push @{ $EXPORT_TAGS{lib} }, qw[libm libc];
    $EXPORT_TAGS{types} = [
        qw[ typedef

lib/Affix.pod  view on Meta::CPAN

prefixes, and search paths automatically.

=head2 Library Discovery

When you provide a bare library name (e.g., C<'z'>, C<'ssl'>, C<'user32'>) rather than an absolute path, Affix
automatically formats the name for the current platform (e.g., C<libz.so>, C<libz.dylib>, C<z.dll>) and searches the
following locations in order:

=over

=item 1. B<Standard System Paths:> Windows C<System32>/C<SysWOW64>; Unix C</usr/local/lib>, C</usr/lib>, C</lib>, C</usr/lib/system>.

=item 2. B<Environment Variables:> Paths defined in C<LD_LIBRARY_PATH>, C<DYLD_LIBRARY_PATH>, C<DYLD_FALLBACK_LIBRARY_PATH>, or C<PATH>.

=item 3. B<Local Paths:> The current working directory (C<.>) and its C<lib/> subdirectory.

=back

=head2 Functions

=head3 C<load_library( $path_or_name )>

lib/Affix.pod  view on Meta::CPAN


=head1 ERROR HANDLING & DEBUGGING

Diagnose FFI issues with built-in error reporting, memory inspection, and hex dumps. Bridging two entirely different
runtimes can lead to spectacular crashes if types or memory boundaries are mismatched.

=head2 Error Handling

=head3 C<errno()>

Accesses the system error code from the most recent FFI or standard library call (reads C<errno> on Unix and
C<GetLastError> on Windows).

This function returns a B<dualvar>. It behaves as an integer in numeric context, and magically resolves to the
human-readable system error message (via C<strerror> or C<FormatMessage>) in string context.

    # Suppose a C file-open function fails
    my $fd = c_open("/does/not/exist");
    if (!$fd) {
        my $err = errno();

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

        # Cached Flag Arrays
        field @cflags;
        field @cxxflags;
        field @ldflags;
        field $_lib;
        #
        ADJUST {
            my $so_ext = $Config{so} // 'so';
            $build_dir = Path::Tiny->new($build_dir) unless builtin::blessed $build_dir;

            # Standard convention: Windows DLLs don't need 'lib' prefix, Unix SOs do.
            my $prefix    = ( $os eq 'MSWin32' || $name =~ /^lib/ ) ? ''          : 'lib';
            my $suffix    = defined $version                        ? ".$version" : '';
            my $safe_name = $name;
            $safe_name =~ s/[^\w.-]/_/g;
            $libname = $build_dir->child("$prefix$safe_name.$so_ext$suffix")->absolute;

            # We prefer C++ drivers (g++, clang++) to handle standard libraries for mixed code (C+Rust, C+C++)
            $linker = $self->_can_run(qw[g++ clang++ c++ icpx]) || $self->_can_run(qw[cc gcc clang icx cl]) || 'c++';

            # Parse global flags...

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;

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) {

t/060_platform_unix_security.t  view on Meta::CPAN

use v5.40;
use blib;
use Test2::V0 -no_srand => 1;
use Capture::Tiny qw[capture];

# Load the Unix platform module directly (works cross-platform for unit testing)
use Affix::Platform::Unix qw[find_library];
$|++;
#
subtest '_findLib_ld: safe command execution (C2)' => sub {

    # Normal call — may return error output on Windows (no /dev/null) or empty on
    # Unix without ld, but must not die
    my ( $out, $err, $exit ) = capture { Affix::Platform::Unix::_findLib_ld('m') };
    pass '_findLib_ld does not die on normal input';

    # Malicious input must not execute shell commands.
    # With list-form open, the entire string "-lm; echo INJECTED" is passed as a
    # single argument to ld — never interpreted by a shell.
    for my $evil ( 'm; echo INJECTED', 'm && touch /tmp/pwned', 'm | cat /etc/passwd', 'm $(id)', 'm`id`', ) {
        ( $out, $err, $exit ) = capture { Affix::Platform::Unix::_findLib_ld($evil) };
        my $combined = ( $out // '' ) . ( $err // '' );

        # The shell injection payload must NOT appear as executed output
        unlike $combined, qr/^INJECTED$/m, "No shell injection via ld: $evil";

        # On systems where ld exists, verify the malicious string is passed as a
        # single argument (ld will complain about "cannot find -l<entire string>")
        if ( $combined =~ /cannot find/ ) {
            like $combined, qr/\Q$evil\E/, "Malicious string passed as single arg to ld: $evil";
        }
    }
};
subtest '_findLib_gcc: safe command execution (C3)' => sub {

    # Normal call — may return empty without gcc, must not die
    my @result = eval { Affix::Platform::Unix::_findLib_gcc('m') };
    is ref \@result, 'ARRAY', '_findLib_gcc returns array';

    # Malicious input must not execute shell commands
    for my $evil ( 'm; echo INJECTED', 'm && touch /tmp/pwned', 'm | cat /etc/passwd', 'm $(id)', 'm`id`', ) {
        my @ret = eval { Affix::Platform::Unix::_findLib_gcc($evil) };
        ok !@ret, "Malicious gcc input rejected safely: $evil";
    }

    # lib prefix is stripped
    my @stripped = eval { Affix::Platform::Unix::_findLib_gcc('libfoo') };
    is ref \@stripped, 'ARRAY', 'lib prefix stripped without error';
};
subtest '_get_soname: safe command execution (C4)' => sub {

    # Nonexistent file returns undef
    my $result = eval { Affix::Platform::Unix::_get_soname('/nonexistent/file.so') };
    is $result, undef, '_get_soname returns undef for nonexistent file';

    # undef input returns undef
    $result = eval { Affix::Platform::Unix::_get_soname(undef) };
    is $result, undef, '_get_soname returns undef for undef input';

    # Empty string returns undef
    $result = eval { Affix::Platform::Unix::_get_soname('') };
    is $result, undef, '_get_soname returns undef for empty string';

    # Malicious input must not execute shell commands
    for my $evil ( '/tmp/fake; echo INJECTED', '/tmp/fake && touch /tmp/pwned', '/tmp/fake | cat /etc/passwd', '/tmp/fake $(id)', '/tmp/fake`id`', ) {
        $result = eval { Affix::Platform::Unix::_get_soname($evil) };
        is $result, undef, "Malicious soname input rejected safely: $evil";
    }
};
subtest 'find_library: graceful handling' => sub {

    # find_library with normal names must not die (returns undef on error)
    my $result = eval { find_library('m') };
    ok !defined $result || -f $result, 'find_library(m) returns undef or valid path';
    $result = eval { find_library('nonexistent_lib_xyz_12345') };
    is $result, undef, 'find_library returns undef for unknown lib';

t/060_platform_unix_security.t  view on Meta::CPAN

    }
};
subtest 'is_elf: binary detection' => sub {
    skip_all 'No /tmp on Windows' if $^O eq 'MSWin32';

    # Create a fake ELF file
    my $fake_elf = '/tmp/_test_fake_elf_' . $$;
    open( my $fh, '>', $fake_elf ) or die "Cannot create temp file: $!";
    print $fh "\x7fELF" . "\x00" x 20;
    close $fh;
    ok Affix::Platform::Unix::is_elf($fake_elf), 'is_elf detects ELF header';

    # Create a non-ELF file
    my $fake_txt = '/tmp/_test_fake_txt_' . $$;
    open( $fh, '>', $fake_txt ) or die "Cannot create temp file: $!";
    print $fh "This is not an ELF file";
    close $fh;
    ok !Affix::Platform::Unix::is_elf($fake_txt), 'is_elf rejects non-ELF';

    # Nonexistent file
    ok !Affix::Platform::Unix::is_elf('/nonexistent/file'), 'is_elf handles nonexistent file';
    unlink $fake_elf, $fake_txt;
};
done_testing();



( run in 2.562 seconds using v1.01-cache-2.11-cpan-64ef6c95b5d )