view release on metacpan or search on metacpan
- 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
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 ...
- \[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.
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
if (infix_register_types(registry, "@Buffer = *void;") != INFIX_SUCCESS)
croak("Failed to register internal type alias '@Buffer'");
if (infix_register_types(registry, "@SockAddr = *void;") != INFIX_SUCCESS)
croak("Failed to register internal type alias '@SockAddr'");
}
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;
}
void boot_Affix(pTHX_ CV * cv) {
dVAR;
dXSBOOTARGSXSAPIVERCHK;
PERL_UNUSED_VAR(items);
#ifdef USE_ITHREADS
lib/Affix/Platform/BSD.pm view on Meta::CPAN
package Affix::Platform::BSD v0.12.0 {
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$name\.[^\s]+.+\s*=>\s*(.+)$];
lib/Affix/Platform/MacOS.pm view on Meta::CPAN
package Affix::Platform::MacOS v0.12.0 {
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 v0.12.0 {
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 v0.12.0 {
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 v0.12.0 {
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/Wrap.pm view on Meta::CPAN
# Check cache (recursion guard)
return $cache{$token} if exists $cache{$token};
local $cache{$token} = undef;
# Look up definition
my $expr = $macros{$token};
return undef unless defined $expr; # Not found (maybe a string or unknown)
# Parse expression
# Strip outer parentheses recursively: ((A|B)) -> A|B
1 while $expr =~ s/^\((.*)\)$/$1/;
# Handle bitwise OR chains (e.g. "FLAG_A | FLAG_B")
if ( $expr =~ /\|/ ) {
my $accum = 0;
for my $part ( split /\|/, $expr ) {
my $val = $resolve->($part);
return undef unless defined $val; # Abort if any part is non-numeric
$accum |= $val;
}
lib/Test2/Tools/Affix.pm view on Meta::CPAN
U D T F DNE array string float number bool hash etc end
refcount
can_ok isa_ok
capture imported_ok warns
path tempfile tempdir]
]
);
#
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;
}