view release on metacpan or search on metacpan
## [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
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
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
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
# 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();