view release on metacpan or search on metacpan
This release introduces a modernization of pointer handling, turning pins into first-class objects with native indexing support. In other words, you can now use `$ptr->[$n]` to access the nth element.
Support for passing 128bit integers around is now complete. Additionally, functions expecting an enumeration now accept the string name of a constant; `state('PLASMA')` is the same as `state(PLASMA)` where `state` expects values defined like this: `t...
### Added
- Added support for native Perl array indexing on pointers and arrays. You can now use `$ptr->[$i]` to read or write memory at an offset without manual casting.
- New `Affix::Pointer` objects for structs and unions now allow direct field access like `$ptr->{field}` without explicit casting to `LiveStruct`.
- Recursive Liveness: Unified access and `LiveStruct` now work recursively. Accessing a nested struct member returns a live view or pointer tied to the original memory block.
- All pointers returned by `malloc`, `calloc`, `cast`, etc., are now blessed into the `Affix::Pointer` class, which provides several new methods:
- `address()`: Returns the virtual memory address.
- `type()`: Returns the L<infix> signature of the pointer.
- `element_type()`: Returns the signature of the pointed-to elements.
- `count()`: Returns the element count for Arrays, or byte size for Void pointers.
- `size()`: Returns the total allocated size for managed pointers.
- `cast($type)`: Reinterprets the pointer.
- `attach_destructor($destructor, [$lib])`: Attaches a custom C cleanup routine to the pointer.
- Added `attach_destructor( $pin, $destructor, [$lib] )` to allow attaching custom C cleanup routines to managed pointers.
- Improved `VarArgs` support to automatically promote Perl strings to `char*`.
- Experimental Zero-Copy 'Live' Aggregates:
- `LiveStruct`: A new helper to return zero-copy, live views of C structs. Modifications to the returned blessed hash reflect immediately in C memory.
- `LiveArray`: A new helper to return live `Affix::Pointer` objects instead of deeply copied array references.
- Implemented the `TIEHASH` interface for `Affix::Live` so perl can treat them as standard Perl hashes (`keys %$live`, `each %$live`, etc.).
- Fully implemented marshalling for `Int128` and `UInt128` (sint128/uint128) primitive types.
- Added `Affix::Wrap->generate( $lib, $pkg, $file )` for static binding generation. This emits standalone Perl modules that depend only on `Affix`, eliminating the need for `Clang` or header files at runtime.
- Recursive macro resolution support in `Affix::Wrap` for bitwise OR expressions like `(FLAG_A | FLAG_B)`.
- 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
Valgrind directed the work in Affix itself but infix got a lot of platform stability fixes which found their way into Affix by way of new Float16 support, bitfield width support, and SIMD improvements.
### Fixed
- Anonymous wrapper functions created via wrap() were leaking because of a redundant SvREFCNT_inc call and the use of newRV_inc instead of newRV_noinc. This prevented the underlying CV and its associated Affix struct (including its memory arenas) fro...
- Implicitly loaded libraries (by path) were not having their reference counts decremented because the handle was not stored in the Affix struct. Additionally, using Affix::Lib objects did not increment the registry reference count, potentially leadi...
- [[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] Fixed 128-bit vector truncation on System V x64 (Linux/macOS). Reverse trampolines previously used 64-bit moves (`MOVSD`) for all SSE arguments, corrupting the upper half of vector arguments. They now correctly use `MOVUPS`.
- \[infix] Fixed vector argument corruption on AArch64. The reverse trampoline generator now correctly identifies vector types and uses 128-bit stores (`STR Qn`) instead of falling back to 64-bit/32-bit stores or GPRs.
- \[infix] Fixed floating-point corruption on Windows on ARM64. Reverse trampolines now force full 128-bit register saves for all floating-point arguments to ensure robust handling of volatile register states.
builder/Affix/Builder.pm view on Meta::CPAN
require CPAN::Requirements::Dynamic;
my $dynamic_parser = CPAN::Requirements::Dynamic->new();
my $prereq = $dynamic_parser->evaluate($dynamic);
$meta{prereqs} = $meta->effective_prereqs->with_merged_prereqs($prereq)->as_string_hash;
$meta = CPAN::Meta->new( \%meta );
}
$meta->save(@$_) for ['MYMETA.json'];
}
sub find ( $pattern, $base ) {
$base = path($base) unless builtin::blessed $base;
my $blah = $base->visit(
sub ( $path, $state ) {
$state->{$path} = $path if $path =~ $pattern;
},
{ recurse => 1 }
);
values %$blah;
}
# infix builder
lib/Affix.c view on Meta::CPAN
Newxz(pin, 1, Affix_Pin); \
pin->pointer = c_ptr; \
pin->type = affix->unwrapped_ret_type; \
\
SV * obj_data = newSViv(PTR2IV(pin)); \
SV * rv = newRV_noinc(obj_data); \
\
dMY_CXT; \
if (UNLIKELY(MY_CXT.stash_pointer == nullptr)) \
MY_CXT.stash_pointer = gv_stashpv("Affix::Pointer", GV_ADD); \
(void)sv_bless(rv, MY_CXT.stash_pointer); \
\
(void)sv_magicext(obj_data, nullptr, PERL_MAGIC_ext, &Affix_pin_vtbl, (char *)pin, 0); \
sv_setsv(TARG, sv_2mortal(rv)); \
} \
break; \
} \
case OP_RET_PTR_CHAR: \
{ \
char * p = *(char **)ret_buffer; \
if (p) \
lib/Affix.c view on Meta::CPAN
}
backend->lib_handle = created_implicit_handle ? lib_handle_for_symbol : nullptr;
CV * cv_new =
newXSproto_portable((ix == 0 || ix == 2) ? rename_str : nullptr, Affix_trigger_backend, __FILE__, nullptr);
CvXSUBANY(cv_new).any_ptr = (void *)backend;
SV * obj = (ix == 1 || ix == 3) ? newRV_noinc(MUTABLE_SV(cv_new)) : newRV_inc(MUTABLE_SV(cv_new));
sv_bless(obj, gv_stashpv("Affix::Bundled", GV_ADD));
ST(0) = sv_2mortal(obj);
XSRETURN(1);
}
// Standard path (parse & prepare types)
infix_arena_t * parse_arena = NULL;
infix_type * ret_type = NULL;
infix_function_argument * args = NULL;
size_t num_args = 0, num_fixed = 0;
lib/Affix.c view on Meta::CPAN
// We MUST use nullptr/0 here and assign mg_ptr manually, otherwise sv_magicext treats 'affix' as a string and
// copies truncated garbage.
MAGIC * mg = sv_magicext((SV *)cv_new, nullptr, PERL_MAGIC_ext, &Affix_cv_vtbl, nullptr, 0);
mg->mg_ptr = (char *)affix;
// Set optimization pointer
CvXSUBANY(cv_new).any_ptr = (void *)affix;
//
SV * obj = (ix == 1 || ix == 3) ? newRV_noinc(MUTABLE_SV(cv_new)) : newRV_inc(MUTABLE_SV(cv_new));
sv_bless(obj, gv_stashpv("Affix", GV_ADD));
ST(0) = sv_2mortal(obj);
infix_arena_destroy(parse_arena); // Now safe to destroy as we deep-copied everything
XSRETURN(1);
}
XS_INTERNAL(Affix_Bundled_DESTROY) {
dXSARGS;
dMY_CXT;
PERL_UNUSED_VAR(items);
Affix_Backend * backend;
lib/Affix.c view on Meta::CPAN
static void pull_union(pTHX_ Affix * affix, SV * sv, const infix_type * type, void * p) {
HV * hv;
if (SvROK(sv) && SvTYPE(SvRV(sv)) == SVt_PVHV) {
hv = (HV *)SvRV(sv);
hv_clear(hv);
}
else {
hv = newHV();
SV * rv = newRV_noinc(MUTABLE_SV(hv));
sv_bless(rv, gv_stashpv("Affix::Live", GV_ADD));
sv_setsv(sv, sv_2mortal(rv));
}
_populate_hv_from_c_struct(aTHX_ affix, hv, type, p, true, nullptr);
}
// Helper for portability if strnlen isn't available
static size_t _safe_strnlen(const char * s, size_t maxlen) {
size_t len;
for (len = 0; len < maxlen; len++, s++)
if (!*s)
lib/Affix.c view on Meta::CPAN
static void pull_struct_as_live(pTHX_ Affix * affix, SV * sv, const infix_type * type, void * ptr) {
void * c_ptr = *(void **)ptr;
if (c_ptr == nullptr) {
sv_setsv(sv, &PL_sv_undef);
return;
}
const infix_type * pointee_type = type->meta.pointer_info.pointee_type;
HV * hv = newHV();
SV * rv = newRV_noinc(MUTABLE_SV(hv));
sv_bless(rv, gv_stashpv("Affix::Live", GV_ADD));
_populate_hv_from_c_struct(aTHX_ affix, hv, pointee_type, c_ptr, true, nullptr);
sv_setsv(sv, rv);
SvREFCNT_dec(rv);
}
static void pull_pointer_as_array(pTHX_ Affix * affix, SV * sv, const infix_type * type, void * ptr) {
void * c_ptr = *(void **)ptr;
if (c_ptr == nullptr)
sv_setsv(sv, &PL_sv_undef);
else {
lib/Affix.c view on Meta::CPAN
// Ensure we point to the content type, not Pointer[Content]
pin->type = _unwrap_pin_type(type);
pin->managed = false;
SV * obj_data = newSV(0);
sv_setiv(obj_data, PTR2IV(pin));
// Create the Reference
SV * rv = sv_2mortal(newRV_noinc(obj_data));
// Bless into Affix::Pointer BEFORE attaching magic to avoid triggering 'set' during blessing
(void)sv_bless(rv, gv_stashpv("Affix::Pointer", GV_ADD));
MAGIC * mg = sv_magicext(obj_data, nullptr, PERL_MAGIC_ext, &Affix_pin_vtbl, nullptr, 0);
mg->mg_ptr = (char *)pin;
// Update the target SV
sv_setsv(sv, rv);
}
static void pull_sv(pTHX_ Affix * affix, SV * sv, const infix_type * type, void * ptr) {
void * c_ptr = *(void **)ptr;
lib/Affix.c view on Meta::CPAN
dMY_CXT;
if (items != 1)
croak_xs_usage(cv, "library_path");
const char * path = SvPV_nolen(ST(0));
SV ** entry_sv_ptr = hv_fetch(MY_CXT.lib_registry, path, strlen(path), 0);
if (entry_sv_ptr) {
LibRegistryEntry * entry = INT2PTR(LibRegistryEntry *, SvIV(*entry_sv_ptr));
entry->ref_count++;
SV * obj_data = newSV(0);
sv_setiv(obj_data, PTR2IV(entry->lib));
ST(0) = sv_2mortal(sv_bless(newRV_inc(obj_data), gv_stashpv("Affix::Lib", GV_ADD)));
XSRETURN(1);
}
infix_library_t * lib = infix_library_open(path);
if (lib) {
LibRegistryEntry * new_entry;
Newxz(new_entry, 1, LibRegistryEntry);
new_entry->lib = lib;
new_entry->ref_count = 1;
hv_store(MY_CXT.lib_registry, path, strlen(path), newSViv(PTR2IV(new_entry)), 0);
SV * obj_data = newSV(0);
sv_setiv(obj_data, PTR2IV(lib));
ST(0) = sv_2mortal(sv_bless(newRV_inc(obj_data), gv_stashpv("Affix::Lib", GV_ADD)));
XSRETURN(1);
}
XSRETURN_UNDEF;
}
XS_INTERNAL(Affix_get_last_error_message) {
dXSARGS;
PERL_UNUSED_VAR(items);
infix_error_details_t err = infix_get_last_error();
if (err.message[0] != '\0')
ST(0) = sv_2mortal(newSVpv(err.message, 0));
lib/Affix.c view on Meta::CPAN
}
if (pointee->category == INFIX_TYPE_VOID) {
sv_setuv(sv, PTR2UV(pin->pointer));
return 0;
}
if (pointee->category == INFIX_TYPE_STRUCT || pointee->category == INFIX_TYPE_UNION) {
HV * hv = newHV();
SV * rv = newRV_noinc(MUTABLE_SV(hv));
sv_bless(rv, gv_stashpv("Affix::Live", GV_ADD));
_populate_hv_from_c_struct(
aTHX_ nullptr, hv, pointee, pin->pointer, true, pin->owner_sv ? pin->owner_sv : sv);
sv_setsv(sv, rv);
SvREFCNT_dec(rv);
return 0;
}
if (pointee->category == INFIX_TYPE_ARRAY) {
Affix_Pin * new_pin;
Newxz(new_pin, 1, Affix_Pin);
lib/Affix.c view on Meta::CPAN
if (infix_type_create_pointer_to(pin->type_arena, &void_ptr_type, infix_type_create_void()) != INFIX_SUCCESS) {
safefree(pin);
infix_arena_destroy(pin->type_arena);
croak("Internal error: Failed to create pointer type for pin");
}
pin->type = void_ptr_type;
SV * obj_data = newSV(0);
sv_setiv(obj_data, PTR2IV(pin));
SV * rv = newRV_inc(obj_data);
// Bless into Affix::Pointer BEFORE attaching magic to avoid triggering 'set' during blessing
(void)sv_bless(rv, gv_stashpv("Affix::Pointer", GV_ADD));
MAGIC * mg = sv_magicext(obj_data, nullptr, PERL_MAGIC_ext, &Affix_pin_vtbl, nullptr, 0);
mg->mg_ptr = (char *)pin;
ST(0) = sv_2mortal(rv);
XSRETURN(1);
}
XSRETURN_UNDEF;
}
XS_INTERNAL(Affix_pin) {
lib/Affix.c view on Meta::CPAN
}
SV * _new_pointer_obj(pTHX_ Affix_Pin * pin) {
SV * data_sv = newSV(0);
sv_setiv(data_sv, PTR2IV(pin));
SvUPGRADE(data_sv, SVt_PVMG);
SV * rv = newRV_noinc(data_sv);
// Bless into Affix::Pointer
(void)sv_bless(rv, gv_stashpv("Affix::Pointer", GV_ADD));
MAGIC * mg = sv_magicext(data_sv, nullptr, PERL_MAGIC_ext, &Affix_pin_vtbl, nullptr, 0);
mg->mg_ptr = (char *)pin;
return rv;
}
XS_INTERNAL(Affix_malloc) {
dXSARGS;
dMY_CXT;
lib/Affix.c view on Meta::CPAN
const infix_type * resolved = _resolve_type(aTHX_ new_type);
if (is_string_type) {
/*
* String pull handlers expect a pointer-to-pointer (char**).
* 'ptr_val' IS the char*. So we pass '&ptr_val'.
* The handler reads *(&ptr_val) -> ptr_val, then reads the string.
*/
ptr2sv(aTHX_ nullptr, &ptr_val, ret_val, new_type);
}
else if (live_hint && (resolved->category == INFIX_TYPE_STRUCT || resolved->category == INFIX_TYPE_UNION)) {
// Live struct return from cast: bypass ptr2sv and create blessed HV
HV * hv = newHV();
SV * rv = newRV_noinc(MUTABLE_SV(hv));
sv_bless(rv, gv_stashpv("Affix::Live", GV_ADD));
_populate_hv_from_c_struct(
aTHX_ nullptr, hv, resolved, ptr_val, true, pin ? (pin->owner_sv ? pin->owner_sv : arg) : nullptr);
ret_val = sv_2mortal(rv);
}
else if (resolved->category == INFIX_TYPE_UNION) {
// Unions are always live!
pull_union(aTHX_ nullptr, ret_val, resolved, ptr_val);
}
else {
/*
lib/Affix.c view on Meta::CPAN
if (member->name) {
void * member_ptr = (char *)p + member->offset;
SV * member_sv = nullptr;
if (live) {
// Live mode: Create a Pin or Live view for this member
const infix_type * resolved = _resolve_type(aTHX_ member->type);
if (resolved->category == INFIX_TYPE_STRUCT || resolved->category == INFIX_TYPE_UNION) {
HV * sub_hv = newHV();
SV * sub_rv = newRV_noinc(MUTABLE_SV(sub_hv));
sv_bless(sub_rv, gv_stashpv("Affix::Live", GV_ADD));
_populate_hv_from_c_struct(aTHX_ affix, sub_hv, resolved, member_ptr, true, owner_sv);
member_sv = sub_rv;
}
else if (resolved->category == INFIX_TYPE_ARRAY) {
Affix_Pin * sub_pin;
Newxz(sub_pin, 1, Affix_Pin);
sub_pin->pointer = member_ptr;
sub_pin->managed = false;
if (owner_sv) {
lib/Affix.c view on Meta::CPAN
elem_size = 1; // Byte-indexed for void*
elem_type = infix_type_create_primitive(INFIX_PRIMITIVE_UINT8);
}
if (elem_size == 0)
croak("Cannot index into zero-sized type");
void * target = (char *)pin->pointer + (index * elem_size);
if (elem_type->category == INFIX_TYPE_STRUCT || elem_type->category == INFIX_TYPE_UNION) {
HV * hv = newHV();
SV * rv = newRV_noinc(MUTABLE_SV(hv));
sv_bless(rv, gv_stashpv("Affix::Live", GV_ADD));
// We might need to pass the owner here too, but let's start with pins
_populate_hv_from_c_struct(aTHX_ nullptr, hv, elem_type, target, true, pin->owner_sv ? pin->owner_sv : ST(0));
ST(0) = sv_2mortal(rv);
}
else if (elem_type->category == INFIX_TYPE_ARRAY) {
// Return a new Affix::Pointer for this sub-array (Live view)
Affix_Pin * new_pin;
Newxz(new_pin, 1, Affix_Pin);
new_pin->pointer = target;
new_pin->managed = false;
lib/Affix.h view on Meta::CPAN
char * sig_str;
char * sym_name;
void * target_addr;
char * lib_path;
dTHXfield(owner_perl)
// Variadic demo
HV * variadic_cache;
size_t num_fixed_args;
};
/// Represents an Affix::Pin object, a blessed Perl scalar that wraps a raw C pointer.
typedef struct {
void * pointer; ///< The raw C memory address.
const infix_type * type; ///< Infix's description of the data type at 'pointer'. Used for dereferencing.
infix_arena_t * type_arena; ///< Memory arena that owns the 'type' structure.
bool managed; ///< If true, Perl owns the 'pointer' and will safefree() it on DESTROY.
UV ref_count; ///< Refcount to prevent premature freeing when SVs are copied.
size_t size; ///< Size of malloc'd void pointers.
void (*destructor)(void *); ///< Custom destructor function (e.g. SDL_DestroyWindow).
SV * destructor_lib_sv; ///< Perl object (Affix::Lib) to keep alive for the destructor.
SV * owner_sv; ///< Perl object that owns the memory, kept alive by this pin.
lib/Affix.pm view on Meta::CPAN
#~ ddx \@_;
#~ ddx $cache;
if (@libs) {
( $cache->{$name}{ $version // '' } ) = @libs;
return $cache->{$name}{ $version // '' }->{path};
}
();
}
sub _is_type ($thing) {
return 1 if builtin::blessed($thing) && $thing->isa('Affix::Type');
return 0 if !defined $thing || ref $thing;
# Strictly check for signature characters
return 1 if $thing =~ /^[\*\[\{\!<@]/;
# Primitive types must match exactly or be followed by a delimiter
return 1
if $thing
=~ /^(?:void|bool|[usw]?char|u?short|u?int|u?long(?:long)?|float|double|longdouble|s?size_t|s?int\d+|uint\d+|float\d+|m\d+[a-z]*)$/;
return 0;
lib/Affix.pm view on Meta::CPAN
}
sub Struct : prototype($) { Affix::Type::Struct->new( members => $_[0] ) }
sub Live : prototype($) {
my $t = $_[0];
$t = $t->() if ref($t) eq 'CODE';
if ( ref($t) eq 'ARRAY' ) {
if ( @$t == 1 ) { $t = $t->[0]; }
else { $t = ( @$t == 2 && !ref( $t->[1] ) && $t->[1] =~ /^\d+$/ ) ? Array($t) : Struct($t); }
}
if ( builtin::blessed($t) ) {
return '+' . $t->signature if $t->isa('Affix::Type::Struct') || $t->isa('Affix::Type::Union');
return Pointer [$t] if $t->isa('Affix::Type::Array');
return Pointer [$t];
}
if ( !ref $t ) {
return '+' . $t if $t =~ /^[\{\<@]/;
return '*' . $t if $t =~ /^\[/;
}
return $t;
}
lib/Affix.pm view on Meta::CPAN
}
# Vector[ 4, Float ] -> v[4:float]
sub Vector : prototype($) {
my ( $size, $type ) = @{ $_[0] };
return "v[$size:$type]";
}
sub ThisCall : prototype($) {
my $cb = $_[0];
if ( builtin::blessed($cb) && $cb->isa('Affix::Type::Callback') ) {
# Prepend 'this' pointer
unshift @{ $cb->params }, Pointer [Void];
return $cb;
}
elsif ( !ref $cb && $cb =~ /^\*\(\((.*)\)->(.*)\)$/ ) {
my ( $args, $ret ) = ( $1, $2 );
$args = $args ? "*void,$args" : "*void";
return "*(($args)->$ret)";
}
lib/Affix.pm view on Meta::CPAN
sub SockAddr () {'@SockAddr'}
# Helper for Struct/Union to handle "Name => Type" syntax
sub _build_aggregate {
my ( $args, $wrapper ) = @_;
my @parts;
for ( my $i = 0; $i < @$args; $i++ ) {
my $curr = $args->[$i];
my $next = $args->[ $i + 1 ];
if ( defined $next &&
( !ref($curr) || !builtin::blessed($curr) || !$curr->isa('Affix::Type') ) &&
builtin::blessed($next) &&
$next->isa('Affix::Type') ) {
push @parts, "$curr:$next";
$i++;
}
else {
push @parts, "$curr";
}
}
my $content = join( ',', @parts );
return sprintf( $wrapper, $content );
}
sub typedef ( $name, $type //= () ) {
( my $clean_name = $name ) =~ s/^@//;
if ( !defined $type ) {
Affix::_typedef($clean_name);
}
else {
if ( builtin::blessed($type) && $type->isa('Affix::Type::Enum') ) {
my ( $const_map, $val_map ) = $type->resolve();
my $pkg = caller;
no strict 'refs';
while ( my ( $const_name, $val ) = each %$const_map ) {
*{"${pkg}::${const_name}"} = sub () {$val};
}
&Affix::_register_enum_values( $clean_name, $val_map, $const_map );
}
if ( builtin::blessed($type) && $type->isa('Affix::Type') ) {
Affix::_typedef("$clean_name = $type");
}
else {
if ( $type =~ /^@/ ) {
Affix::_typedef($type);
}
else {
Affix::_typedef("$clean_name = $type");
}
}
lib/Affix.pm view on Meta::CPAN
*{"${pkg}::${name}"} = sub {
return Affix::Type::Reference->new( name => $clean_name );
};
}
}
return 1;
}
package #
Affix::Type {
use overload '""' => sub { shift->signature() }, fallback => 1;
sub new { my ( $class, %args ) = @_; bless \%args, $class }
sub signature { die "Abstract method" }
}
package #
Affix::Type::Reference {
our @ISA = qw[Affix::Type];
sub signature { '@' . shift->{name} }
}
package #
Affix::Type::Primitive {
our @ISA = qw[Affix::Type];
lib/Affix.pm view on Meta::CPAN
sub signature {
my $self = shift;
my $members = $self->{members};
my $kind = $self->{kind} // '{%s}';
my @parts;
for ( my $i = 0; $i < @$members; $i++ ) {
my $curr = $members->[$i];
my $next = $members->[ $i + 1 ];
if ( defined $next &&
builtin::blessed($next) &&
$next->isa('Affix::Type') &&
( !builtin::blessed($curr) || !$curr->isa('Affix::Type') ) ) {
my $name = $curr;
my $type = $next;
$i++;
my $width = $members->[ $i + 1 ];
if ( defined $width && !ref($width) && $width =~ /^\d+$/ ) { push @parts, "$name:$type:$width"; $i++; }
else { push @parts, "$name:$type"; }
}
else { push @parts, "$curr"; }
}
return sprintf( $kind, join( ',', @parts ) );
}
}
package #
Affix::Type::Struct {
our @ISA = qw[Affix::Type::Aggregate];
sub new { my $class = shift; my %args = @_; $args{kind} = '{%s}'; bless \%args, $class }
}
package #
Affix::Type::Union {
our @ISA = qw[Affix::Type::Aggregate];
sub new { my $class = shift; my %args = @_; $args{kind} = '<%s>'; bless \%args, $class }
}
package #
Affix::Type::Array {
our @ISA = qw[Affix::Type];
sub signature { my $self = shift; my $c = $self->{count} // '?'; return "[$c:" . $self->{type} . "]"; }
}
package #
Affix::Type::Pointer {
our @ISA = qw[Affix::Type];
sub signature { '*' . ( shift->{subtype} // 'void' ) }
}
package #
Affix::Type::Callback {
our @ISA = qw[Affix::Type];
sub params { shift->{params} }
sub signature {
my $self = shift;
my @args = map { builtin::blessed($_) ? $_->signature : $_ } @{ $self->{params} };
my $args = join( ',', @args );
$args =~ s/,\;,/;/g;
$args =~ s/,\;$/;/;
my $r = builtin::blessed( $self->{ret} ) ? $self->{ret}->signature : $self->{ret};
return "*(($args)->$r)";
}
}
package #
Affix::Pointer {
use v5.40;
use overload '""' => \&address, '@{}' => \&_as_array, '%{}' => \&_as_hash, fallback => 1;
sub address { Affix::address(shift) }
sub type { Affix::_pin_type(shift) }
sub element_type { Affix::_pin_element_type(shift) }
lib/Affix.pm view on Meta::CPAN
sub FETCH { my ( $self, $key ) = @_; return $self->{$key}; }
sub STORE { my ( $self, $key, $val ) = @_; $self->{$key} = $val; }
sub EXISTS { my ( $self, $key ) = @_; return exists $self->{$key}; }
sub FIRSTKEY { my ($self) = @_; keys %$self; return each %$self; }
sub NEXTKEY { my ( $self, $last ) = @_; return each %$self; }
sub SCALAR { my ($self) = @_; return scalar %$self; }
};
package #
Affix::Pointer::TiedArray {
use v5.40;
sub TIEARRAY { bless { pin => $_[1] }, $_[0] }
sub FETCH { my ( $self, $index ) = @_; Affix::_pin_get_at( $self->{pin}, $index ); }
sub STORE { my ( $self, $index, $value ) = @_; Affix::_pin_set_at( $self->{pin}, $index, $value ); }
sub FETCHSIZE { my $self = shift; Affix::_pin_count( $self->{pin} ) // 0x7FFFFFFF; }
sub EXISTS { my ( $self, $index ) = @_; my $count = Affix::_pin_count( $self->{pin} ); return defined($count) ? ( $index < $count ) : 1; }
sub DELETE { die "Cannot delete elements from a C array" }
sub CLEAR { die "Cannot clear a C array" }
};
package #
Affix::Live {
use v5.40;
sub new { my ( $class, $ref ) = @_; return bless $ref // {}, $class; }
sub FETCH { my ( $self, $key ) = @_; return $self->{$key}; }
sub STORE { my ( $self, $key, $val ) = @_; $self->{$key} = $val; }
sub EXISTS { my ( $self, $key ) = @_; return exists $self->{$key}; }
sub FIRSTKEY { my ($self) = @_; keys %$self; return each %$self; }
sub NEXTKEY { my ( $self, $last ) = @_; return each %$self; }
sub SCALAR { my ($self) = @_; return scalar %$self; }
}
};
1;
__END__
lib/Affix/Build.pm view on Meta::CPAN
field $linker : reader;
# 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" : '';
$libname = $build_dir->child("$prefix$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/Wrap.pm view on Meta::CPAN
class #
Affix::Wrap::Member {
use Affix qw[Void];
field $name : reader : param //= '';
field $type : reader : param //= '';
field $doc : reader : param //= ();
field $definition : reader : param //= ();
method affix_type {
return $definition->affix_type if defined $definition;
return $type->affix_type if builtin::blessed($type);
return 'Void';
}
method affix {
return $definition->affix if defined $definition;
builtin::blessed($type) ? $type->affix : Void;
}
}
class #
Affix::Wrap::Macro : isa(Affix::Wrap::Entity) {
field $value : reader : param //= ();
method set_value ($v) { $value = $v }
method affix_type {
$value // return '';
my $v = $value // '';
lib/Affix/Wrap.pm view on Meta::CPAN
Affix::Wrap::Typedef : isa(Affix::Wrap::Entity) {
field $underlying : reader : param;
method affix_type { 'typedef \'' . $self->name . '\' => ' . $underlying->affix_type }
method affix ( $lib //= (), $pkg //= () ) {
my $t = $underlying->affix;
Affix::typedef $self->name, $t;
# If the underlying type is an Enum, we must manually export the constants to the target package.
# Affix::typedef only installs them into the *caller* (which is this class).
if ( $pkg && builtin::blessed($t) && $t->isa('Affix::Type::Enum') ) {
my ( $const_map, $val_map ) = $t->resolve();
no strict 'refs';
while ( my ( $const_name, $val ) = each %$const_map ) {
*{"${pkg}::${const_name}"} = sub () {$val};
}
}
}
} class Affix::Wrap::Struct : isa(Affix::Wrap::Entity) {
field $tag : reader : param //= 'struct';
field $members : reader : param //= [];
lib/Affix/Wrap.pm view on Meta::CPAN
}
}
class Affix::Wrap {
field $driver : param //= ();
field $project_files : param //= $driver->project_files;
field $include_dirs : param //= [];
field $types : param //= {};
#
ADJUST {
if ( defined $driver && !builtin::blessed($driver) ) {
if ( $driver eq 'Clang' ) { $driver = Affix::Wrap::Driver::Clang->new( project_files => $project_files ); }
elsif ( $driver eq 'Regex' ) { $driver = Affix::Wrap::Driver::Regex->new( project_files => $project_files ); }
else { die "Unknown driver '$driver'"; }
}
elsif ( !defined $driver ) {
my ( $out, $err, $exit ) = Capture::Tiny::capture { system( 'clang', '--version' ); };
my $use_clang = $exit == 0;
$driver = $use_clang ? Affix::Wrap::Driver::Clang->new( project_files => $project_files ) :
Affix::Wrap::Driver::Regex->new( project_files => $project_files );
}
lib/Affix/Wrap.pm view on Meta::CPAN
$self->_resolve_macros( \@nodes );
my $out =<<~"";
package $pkg {
use v5.36;
use Affix;
#
my \$lib = '$lib';
for my $name ( keys %$types ) {
my $type = $types->{$name};
my $type_str = builtin::blessed($type) ? $type : "'$type'"; # Quote user types
$out .= "typedef '$name' => $type_str;\n";
}
for my $node (@nodes) {
if ( ( $node isa Affix::Wrap::Typedef || $node isa Affix::Wrap::Struct || $node isa Affix::Wrap::Enum ) &&
exists $types->{ $node->name } ) {
next;
}
my $code = $node->affix_type;
if ($code) { $out .= "$code;\n"; }
}
lib/Affix/Wrap.pm view on Meta::CPAN
Path::Tiny::path($file)->spew_utf8($out);
}
method wrap ( $lib, $target //= [caller]->[0] ) {
# Pre-register User Types
# This ensures they are available in the Affix registry before signatures are parsed,
# and allows using them in recursive definitions or opaque handles.
for my $name ( keys %$types ) {
my $type = $types->{$name};
my $type_str = builtin::blessed($type) ? $type : "$type";
Affix::typedef( $name, $type_str );
}
my @nodes = $self->parse;
# Macro resolution pass
$self->_resolve_macros( \@nodes );
# Generation pass
my @installed;
for my $node (@nodes) {
t/020_deep_types.t view on Meta::CPAN
# N1: 100 + 0 + 0 = 100
# Total = 121.5
my $total = $sum_nodes->($node1);
is $total, 121.5, 'Complex recursive struct with union and arrays marshalled correctly';
};
subtest 'Pinning / Dereferencing Deep Structures' => sub {
# Get a pointer to the static C struct
my $ptr = $get_static_rect->();
# Verify we got a pointer-like SV (unblessed ref with magic, per new implementation)
ok $ptr, 'Got a pointer';
# Dereference to read (Deep copy from C -> Perl)
my $val = $$ptr;
is $val, { top_left => { x => 0, y => 0 }, bottom_right => { x => 10, y => 20 } }, 'Dereferenced nested struct pointer correctly';
# Modify via pointer using pinning syntax logic (write to C)
# Since $ptr is magic, assigning to $$ptr should marshal data back to C memory
$$ptr = { top_left => { x => 99, y => 99 }, bottom_right => { x => 100, y => 100 } };
t/028_pointer_indexing.t view on Meta::CPAN
use v5.40;
use lib 'lib', 'blib/arch', 'blib/lib';
use blib;
use Affix qw[:all];
use Test2::Tools::Affix qw[:all];
use Config;
#
subtest 'Objectification' => sub {
my $ptr = malloc(32);
isa_ok $ptr, ['Affix::Pointer'], 'malloc return is blessed';
ok ref($ptr), 'malloc returns a reference';
is ref($ptr), 'Affix::Pointer', 'malloc return ref type is Affix::Pointer';
can_ok $ptr, [qw(address type element_type size count cast)], 'Pointer methods exist';
ok $ptr->address > 0, 'address() works';
is $ptr->type, '*void', 'type() works for void*';
is $ptr->element_type, 'void', 'element_type() works for void*';
};
subtest 'Indexing (Primitives)' => sub {
my $ptr = calloc( 4, Int );
diag "calloc pointer type: " . $ptr->type();
diag "calloc pointer element_type: " . $ptr->element_type();
isa_ok $ptr, ['Affix::Pointer'], 'calloc return is blessed';
like $ptr->type, qr/^\[4:(s?int(32)?)\]$/, 'type() works for Array[Int, 4]';
like $ptr->element_type, qr/^(s?int(32)?)$/, 'element_type() works for Array[Int, 4]';
is $ptr->count, 4, 'count() works for fixed array';
# Test FETCH
is $ptr->[0], 0, 'FETCH index 0';
is $ptr->[3], 0, 'FETCH index 3';
# Test STORE
$ptr->[0] = 42;
t/030_live_struct.t view on Meta::CPAN
my $ptr = get_struct_ptr();
my $copy = $$ptr;
is $copy->{id}, 1, 'Copy has correct ID';
$copy->{id} = 100;
affix $lib_path, 'get_id', [] => Int32;
is get_id(), 1, 'Modifying deep copy did NOT affect C memory';
# Live: zero-copy view
# We use cast() with Live() to get a live view
my $live = cast( $ptr, Live( Struct [ id => Int32, value => Double ] ) );
isa_ok $live, ['Affix::Live'], 'Live struct is blessed as Affix::Live';
is $live->{id}, 1, 'Live view has correct ID';
# Write to live view
$live->{id} = 42;
is get_id(), 42, 'Modifying live view affected C memory immediately';
# C-side modification
affix $lib_path, 'set_vals', [ Int32, Double ] => Void;
set_vals( 99, 3.14 );
is $live->{id}, 99, 'C modification visible in live view ID';
t/032_recursive_liveness.t view on Meta::CPAN
};
subtest 'Recursive Liveness: Live array of Structs' => sub {
my $Point = Struct [ x => Int, y => Int ];
my $ptr = malloc( sizeof($Point) * 2 );
my $live_arr = cast( $ptr, Live [ Array [ $Point, 2 ] ] );
# Set values
$live_arr->[0]{x} = 10;
$live_arr->[0]{y} = 20;
# Accessing $live_arr->[0] returns an Affix::Live blessed hash
my $p0 = $live_arr->[0];
isa_ok $p0, ['Affix::Live'], 'Element of live array should be Affix::Live';
$p0->{x} = 30;
is $live_arr->[0]{x}, 30, 'Changes to live element reflect in original memory';
free($ptr);
};
subtest 'Recursive Liveness: Live struct with Array' => sub {
# Using typedef to ensure member names are preserved in the infix registry
typedef ListStruct => Struct [ items => Array [ Int, 3 ] ];
t/034_live_classifier.t view on Meta::CPAN
DLLEXPORT int get_array_val(int i) { return g_array[i]; }
END_C
my $lib_path = compile_ok($C_CODE);
affix $lib_path, 'get_point_ptr', [] => Pointer [Void];
affix $lib_path, 'get_array_ptr', [] => Pointer [Void];
affix $lib_path, 'get_x', [] => Int;
affix $lib_path, 'get_array_val', [Int] => Int;
subtest 'Live Struct' => sub {
my $ptr = get_point_ptr();
my $live = cast $ptr, Live [ Struct [ x => Int, y => Int ] ];
isa_ok $live, ['Affix::Live'], 'Live struct is blessed as Affix::Live';
is $live->{x}, 10, 'Initial X is 10';
$live->{x} = 42;
is get_x(), 42, 'Modifying live struct affected C memory';
};
subtest 'Live Array' => sub {
my $ptr = get_array_ptr();
my $live = cast $ptr, Live [ Array [ Int, 3 ] ];
isa_ok $live, ['Affix::Pointer'], 'Live array is an Affix::Pointer';
is $live->[0], 1, 'Initial [0] is 1';
$live->[0] = 99;